Splitting the gradient out of the per-layer fills left the falloff itself
independent of the border colour, so it no longer needs rebuilding when
only the colour changes - which is exactly what Hyprland does on every
focus change, animating m_realBorderColor across a fade. Every frame of
every focus change was rebuilding a falloff identical to the one it threw
away the frame before.
BuildGlowMask now returns the accumulated alpha, and GetGlowMask memoizes
it against only the geometry the falloff's shape depends on: size, extent,
chamfer, title bar height, glow size, glow strength. Not the gradient, not
the title bar width, not the outline. A focus fade re-renders the frame
and re-runs the masked gradient pass, and reuses everything expensive.
The mask is stored A8 rather than ARGB32 for two reasons. It is held for
the decoration's lifetime, so a quarter of the memory matters (~3.7MB at
1440p per window, and nothing at all with the glow off, which is the
default). And masking through A8 is itself faster than through ARGB32 -
enough that it more than pays for the extraction pass, making even the
cache-miss path cheaper than before. It is still rendered into an ARGB32
scratch, because cairo has no optimized compositing path for A8
destinations.
The extraction is exactly lossless: the layers are filled solid black, so
premultiplied ARGB32 carries the accumulated alpha verbatim in the alpha
byte. Verified - output is bit-identical to the previous commit, max
difference 0 across all three test sizes.
1080p 1440p 4K
focus fade 9.4 -> 3.4ms 17.7 -> 5.8ms 47.7 -> 15.1ms
size change 9.4 -> 9.3ms 17.7 -> 15.6ms 47.7 -> 35.6ms
Against the original per-layer-gradient implementation that is ~9.8x on a
focus fade and ~3.5x on a resize, at 1080p.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>