# HyprTile Compositor FX plugin

`hyprtile-screenfx-plugin` is a Hyprland-side runtime FX host. The C++ plugin
contains the compositor integration and generic multi-pass renderer, while the
actual effects, pass shaders and presets are loaded from disk at runtime.

Adding or editing an effect therefore does **not** require rebuilding the
plugin. The plugin itself remains ABI-sensitive and must be built against the
same Hyprland headers/version as the running compositor.

## Runtime locations

Installed Hyprland plugin host:

    /usr/local/share/hyprtile/plugins/hyprtile-screenfx-plugin.so

System effects:

    /usr/local/share/hyprtile/shader/fx/

Per-user overrides:

    ~/.hyprtile/shader/fx/

Stack state:

    ~/.hyprtile/compositor-fx.conf

The plugin watches stack config, manifests, presets and fragment shaders. A
valid edit is picked up while the plugin stays loaded. If a reload fails, the
last valid runtime stack remains active.

## CLI

    hyprtile-screenfx compositor list
    hyprtile-screenfx compositor presets bloom
    hyprtile-screenfx compositor chain bloom:default
    hyprtile-screenfx compositor chain bloom:soft blur:soft
    hyprtile-screenfx compositor chain bloom:soft chromatic-aberration:subtle
    hyprtile-screenfx compositor chain saturation:vivid grain:film crt-curvature:classic
    hyprtile-screenfx compositor chain vhs:tape dither:bayer4 posterize:8
    hyprtile-screenfx compositor chain duotone:amber-black temperature-tint:warm
    hyprtile-screenfx compositor test bloom:neon 10
    hyprtile-screenfx compositor off
    hyprtile-screenfx compositor reload
    hyprtile-screenfx compositor plugin-reload
    hyprtile-screenfx compositor status

`compositor test` accepts 1-60 seconds and restores the previous stack after the
test unless another explicit compositor command superseded it.

## Effect format

Each effect has a manifest:

    effects/<id>/effect.conf

Example:

    id=bloom
    label=Bloom
    pass=bright|bright.frag|0.25
    pass=blur-h|blur-h.frag|0.25
    pass=blur-v|blur-v.frag|0.25
    pass=composite|composite.frag|1.0

Every pass shader may use these built-in uniforms:

    uniform sampler2D u_source;       // original input of this effect
    uniform sampler2D u_previous;     // output of previous pass
    uniform vec2 u_source_size;
    uniform vec2 u_previous_size;
    uniform vec2 u_output_size;
    uniform int u_pass_index;
    uniform float u_time;

Preset keys become uniforms automatically. `threshold=0.72` maps to
`uniform float u_threshold`. Comma-separated values map to vec2/vec3/vec4.

## v0.8 final-pass architecture

Versions 0.5-0.7 tried to reconstruct a persistent full desktop from Hyprland's
partial-damage work buffers. That is the wrong ownership model: an offloaded
`currentFB` may only be authoritative for the damage of that render. The result
was exactly the visible symptom seen while moving the mouse: stale windows,
flicker and occasionally missing window contents.

v0.8 removes the persistent scene cache completely.

When compositor FX are requested, a hook on `IHyprRenderer::renderMonitor()`
runs **before Hyprland consumes the monitor damage ring**. It calls
`monitor->m_damage.damageEntire()` directly. Unlike `damageMonitor()`, this does
not schedule another frame; it only widens the frame Hyprland was already going
to render. Direct scanout is temporarily blocked for that monitor render so the
frame cannot bypass the compositor FX path.

The render sequence is therefore:

    already scheduled Hyprland frame
        -> monitor damage ring widened to full
        -> Hyprland renders a complete fresh scene
        -> CHyprOpenGLImpl::end() hook
        -> complete currentFB used read-only as FX source
        -> runtime FX passes in plugin-owned FBOs
        -> final result copied to plugin-owned CGLFramebuffer
        -> rd.currentFB pointer temporarily becomes that final framebuffer
        -> original CHyprOpenGLImpl::end()
        -> Hyprland final screen_shader / color management / output transform
        -> swapchain / monitor

The plugin never writes an FX result into Hyprland's work framebuffer, never
restores one after `end()`, and never seeds one from a private scene cache.

As a safety guard, v0.8 refuses to run the FX stack if the render reaching the
final-pass hook is not actually full-frame. A skipped FX frame is preferable to
presenting stale or missing windows.

This reference implementation intentionally favors correctness over GPU cost:
while compositor FX are enabled, every rendered FX frame is a full monitor render
and the FX stack runs full-screen. Static stacks remain damage/event driven. A
manifest that explicitly sets `continuous=1` asks the host to schedule subsequent
frames for temporal animation/history. Damage expansion/halo optimizations can be
added later after this pipeline is proven stable.

## Temporal/history effects (host 0.9+)

The effect manifest can opt into two generic runtime capabilities:

```ini
history=1
continuous=1
```

`history=1` allocates a private per-monitor history texture for that effect. The
last output of the effect is available to every pass as:

```glsl
uniform sampler2D u_history;
uniform vec2      u_history_size;
uniform float     u_history_valid;
```

The history texture is never a Hyprland work framebuffer, so temporal effects
cannot feed back into or corrupt Hyprland's damage buffers. The host updates the
history only after the effect has completed all of its passes.

`continuous=1` requests further compositor frames while the effect is active.
This is intended for temporal effects such as persistence/trails or rolling
scan. Static effects remain event/damage driven and do not create a continuous
render loop.

Additional built-ins available to runtime shaders are:

```glsl
uniform float u_delta_time; // seconds since this monitor's previous FX frame
uniform float u_frame;      // monotonically increasing per-monitor frame index
```

`u_delta_time` allows decay/animation rates to remain similar across different
monitor refresh rates.
