hypr-chrome v0.1.4 #16

Merged
darman merged 5 commits from develop into master 2026-08-04 16:01:16 +02:00
Owner
No description provided.
darman added 5 commits 2026-08-04 16:00:56 +02:00
ChromePassElement::draw() rounded its device-space box with CBox::round(),
which derives the size from the two rounded corners - round(x + w) -
round(x) - making the rounded width and height a function of the
*position's* fractional part. A box merely sliding at a constant size
therefore has its size flip by a pixel every few frames.

Every one of those flips misses the cachedTexSize check in
GetBorderTexture, GetShadowTexture and GetTitleTexture alike, and each
miss is a full cairo re-render plus a fresh GPU texture allocation and
upload for content that did not change appearance at all. That is exactly
what a workspace switch, a window move, or any other position-only
animation does - every frame, for every window on screen - and it is why
those animations stutter. Measured against hyprutils, a sliding window
resized nothing yet rebuilt its textures on 18-40% of frames at scale
1.0/1.25/1.5/1.6 (integer scales happened to be stable); rounding the
size on its own takes all of those to zero.

What round()'s coupling buys is a far edge landing on the same device
pixel as an adjacent box's near edge. Nothing abuts this box - it is a
free-floating decoration drawn over everything - so there is no seam here
to keep closed.

damageEntire() gains a one-pixel margin to match: with position and size
now rounded separately, the drawn box's far edge can land up to a device
pixel past where the logical damage box scales to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DrawInwardGlow filled each of its ~glowPx overlapping layers with the
border gradient directly. cairo evaluates a gradient source roughly eight
times slower than a solid colour, so that cost was paid once per layer:
at 1920x1080 with a 20px glow, 33ms per render, against 4.3ms for the
identical layers filled solid. At 3840x2160 it was 124ms - eight frames.

The layers now accumulate as alpha only, with a solid source, into a
scratch surface; the gradient is applied to the finished falloff in a
single masked pass. The product is what the per-layer gradient fills
produced before - gradientAlpha(p) * accumulated(p), in the gradient's own
colour - so the falloff math, the layer overlap, and every edge of every
layer are untouched.

Two non-obvious details, both measured rather than reasoned:

The scratch surface is ARGB32 despite only its alpha ever being read.
cairo has no optimized compositing path for A8 destinations, and
rendering these same layers into an A8 surface measured ~6x slower than
into ARGB32 (26.5ms vs 4.3ms).

The colorizing pass is clipped to the glow band. Left unclipped,
cairo_mask_surface evaluates the gradient across the mask's full extents -
the entire window - rather than the perimeter-deep sliver that is actually
non-zero, which was ~25ms of the total on its own. The clip is pushed
kGlowClipSlack past the glow on both edges: a clip edge lying exactly on
the mask's own antialiased edge multiplies the two coverages together and
darkens that boundary by up to a third, which is precisely the corner
seam the layer geometry is built to avoid. Slackened, max alpha error
against the old output drops from 27/255 to 6/255, the remainder being
8-bit quantization through the mask.

Net ~3.5x at 1080p (33ms -> 9.4ms), ~3.2x at 1440p, ~2.6x at 4K.

CreateGradientPattern's alphaScale parameter existed only to serve the
per-layer fills and is now dead, so it and its rationale are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cache the glow's falloff separately from its colour
Version Bump / apply-version-bump (pull_request) Successful in 25s
Version Bump / check-bump-label (pull_request) Skipped
14394cdf50
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>
Reviewed-on: #15
darman added the
bump
skip
label 2026-08-04 16:01:02 +02:00
darman merged commit 192e05f100 into master 2026-08-04 16:01:16 +02:00
Sign in to join this conversation.