Performance optimizations #15
@@ -88,7 +88,7 @@ Per-window decoration (`src/ChromeDecoration.hpp/.cpp`): implements `IHyprWindow
|
|||||||
- `FullDecorationExtentGlobal()` computes the decoration's box in global logical coordinates, accounting for workspace animation offset and floating-window offset. It deliberately excludes the drop shadow (below), which is drawn outside it.
|
- `FullDecorationExtentGlobal()` computes the decoration's box in global logical coordinates, accounting for workspace animation offset and floating-window offset. It deliberately excludes the drop shadow (below), which is drawn outside it.
|
||||||
- `GetShadowTexture(...)` renders the drop shadow. The silhouette is `AppendFrameOuterPath` — the same outline `GetBorderTexture` fills, extracted specifically so the two can't drift apart — filled solid into an A8 mask, blurred, then tinted. Three things about it are load-bearing: (1) the window's interior is cleared *after* the blur, not before, since punching it first would smear shadow inward across the window's own content; the cut lands exactly on the ring's inner boundary so the frame's opaque pixels hide it. (2) The shadow is not part of `getPositioningInfo`'s reserved extents — reserving it would push neighbouring windows away by the shadow's width — so it's simply drawn past the decoration's box, which is why `damageEntire()` and `boundingBox()` have to expand by `ShadowMarginLogical()` by hand. (3) It's rendered at most `kShadowMaxDim` px on the long edge and upscaled by the GPU; a blurred blob loses nothing to that, and it caps a cost that would otherwise be paid per frame of a resize animation. The blur itself is three box passes (`BlurA8Surface`), transposing between each so the vertical pass reuses the horizontal one's cache-friendly row code.
|
- `GetShadowTexture(...)` renders the drop shadow. The silhouette is `AppendFrameOuterPath` — the same outline `GetBorderTexture` fills, extracted specifically so the two can't drift apart — filled solid into an A8 mask, blurred, then tinted. Three things about it are load-bearing: (1) the window's interior is cleared *after* the blur, not before, since punching it first would smear shadow inward across the window's own content; the cut lands exactly on the ring's inner boundary so the frame's opaque pixels hide it. (2) The shadow is not part of `getPositioningInfo`'s reserved extents — reserving it would push neighbouring windows away by the shadow's width — so it's simply drawn past the decoration's box, which is why `damageEntire()` and `boundingBox()` have to expand by `ShadowMarginLogical()` by hand. (3) It's rendered at most `kShadowMaxDim` px on the long edge and upscaled by the GPU; a blurred blob loses nothing to that, and it caps a cost that would otherwise be paid per frame of a resize animation. The blur itself is three box passes (`BlurA8Surface`), transposing between each so the vertical pass reuses the horizontal one's cache-friendly row code.
|
||||||
- The solid outline (`outline_size`/`outline_color`) is drawn by `DrawOutline`, tracing the frame's outer silhouette only. It sits *inside* the frame rather than centred on that edge: the outer path runs along the texture's own bounds, so half of a centred stroke would fall off the surface and vanish on those sides. Stroking at double width leaves exactly the inner half, which comes out to `outline_size` on every edge. The clip is the whole ring rather than just the outer path, so an outline thicker than the frame stops at the window's edge instead of spilling onto the window, and a miter spike at the plateau's dip stays confined to the frame.
|
- The solid outline (`outline_size`/`outline_color`) is drawn by `DrawOutline`, tracing the frame's outer silhouette only. It sits *inside* the frame rather than centred on that edge: the outer path runs along the texture's own bounds, so half of a centred stroke would fall off the surface and vanish on those sides. Stroking at double width leaves exactly the inner half, which comes out to `outline_size` on every edge. The clip is the whole ring rather than just the outer path, so an outline thicker than the frame stops at the window's edge instead of spilling onto the window, and a miter spike at the plateau's dip stays confined to the frame.
|
||||||
- The inward glow (`glow_size`/`glow_strength`) is drawn by `DrawInwardGlow` into the hole the ring's even-odd fill leaves behind, so it lands on the window's own pixels (this decoration is `DECORATION_LAYER_OVER`, and the texture spans the whole window box, not just the ring). Its falloff is built from *overlapping* fills (one layer per px of depth, clamped to `kGlowMinLayers`/`kGlowMaxLayers`) — layer *i* covers the window edge inward to depth `glowPx * i / layers`, so a pixel `d` from the edge is painted by every layer deeper than `d`. The profile is stated explicitly (`strength * (1 - d/glowPx)^kGlowFalloffExponent` — fast off the edge, easing into a tail that reaches zero tangentially so there's no ring where it stops), and since each layer composites over every deeper one, a layer's own alpha is *not* its target: it's solved outermost-inward as `1 - a_j = (1 - T_j) / (1 - T_j+1)`. Changing the profile means changing `targetAt`, not the per-layer alphas. Abutting disjoint bands instead would leave an antialiasing seam at every shared edge; that's the reason for the overlap, don't "optimize" it away. Every layer's *outer* edge is the ring's inner boundary verbatim, chamfer vertices and all — filleting or otherwise altering it detaches the glow from the frame and opens a sliver of unpainted window at each corner. The corner softening lives entirely on the layers' *inner* edges, which are what the accumulated falloff's contours actually follow: each is inset by its own depth, filleted by `kGlowCornerSmoothing` × that depth (`AppendFilletedPolygon`, a quadratic Bezier through each vertex), and has its chamfer shrunk by `kChamferInsetShrink` × that depth. That last correction is not optional cosmetics — insetting a chamfered rect while holding its chamfer constant moves the 45° face in by `d·√2` rather than `d`, so without it the glow runs ~41% deeper at every corner than along the sides. The per-layer alpha is baked into the gradient pattern (`CreateGradientPattern`'s `alphaScale`) specifically so each layer can be a `cairo_fill` of its own band rather than a clip + `cairo_paint_with_alpha`, which would rasterize the clip's full extents — i.e. the whole window area — once per layer.
|
- The inward glow (`glow_size`/`glow_strength`) is drawn by `DrawInwardGlow` into the hole the ring's even-odd fill leaves behind, so it lands on the window's own pixels (this decoration is `DECORATION_LAYER_OVER`, and the texture spans the whole window box, not just the ring). Its falloff is built from *overlapping* fills (one layer per px of depth, clamped to `kGlowMinLayers`/`kGlowMaxLayers`) — layer *i* covers the window edge inward to depth `glowPx * i / layers`, so a pixel `d` from the edge is painted by every layer deeper than `d`. The profile is stated explicitly (`strength * (1 - d/glowPx)^kGlowFalloffExponent` — fast off the edge, easing into a tail that reaches zero tangentially so there's no ring where it stops), and since each layer composites over every deeper one, a layer's own alpha is *not* its target: it's solved outermost-inward as `1 - a_j = (1 - T_j) / (1 - T_j+1)`. Changing the profile means changing `targetAt`, not the per-layer alphas. Abutting disjoint bands instead would leave an antialiasing seam at every shared edge; that's the reason for the overlap, don't "optimize" it away. Every layer's *outer* edge is the ring's inner boundary verbatim, chamfer vertices and all — filleting or otherwise altering it detaches the glow from the frame and opens a sliver of unpainted window at each corner. The corner softening lives entirely on the layers' *inner* edges, which are what the accumulated falloff's contours actually follow: each is inset by its own depth, filleted by `kGlowCornerSmoothing` × that depth (`AppendFilletedPolygon`, a quadratic Bezier through each vertex), and has its chamfer shrunk by `kChamferInsetShrink` × that depth. That last correction is not optional cosmetics — insetting a chamfered rect while holding its chamfer constant moves the 45° face in by `d·√2` rather than `d`, so without it the glow runs ~41% deeper at every corner than along the sides. The layers accumulate as *alpha only*, filled with a solid source into a scratch surface, and the gradient is applied to the finished falloff in one `cairo_mask_surface` pass — the product being the same `gradientAlpha(p) * accumulated(p)` the per-layer gradient fills used to produce. Three measured facts are load-bearing here and none are obvious: (1) cairo evaluates a gradient source roughly 8× slower than a solid one, so filling each of the ~`glowPx` layers with the gradient directly paid that cost per layer (33ms vs 4.3ms for the layers at 1920×1080, 20px glow); (2) the scratch surface is `ARGB32` even though only its alpha is ever read, because cairo has no optimized compositing path for `A8` *destinations* and rendering the layers into one is ~6× slower; (3) the colorizing pass is clipped to the glow band, because `cairo_mask_surface` otherwise evaluates the gradient across the mask's full extents — the whole window — instead of the perimeter-deep sliver that is actually non-zero (~5ms vs ~25ms). That clip is bounded by `kGlowClipSlack` on *both* edges and must stay that way: a clip edge sitting exactly on the mask's own antialiased edge multiplies the two coverages together and darkens that boundary by up to a third (measured max alpha error 27/255 → 6/255 once slackened).
|
||||||
|
|
||||||
Render-pass element (`src/ChromePassElement.hpp/.cpp`): an `EK_CUSTOM` pass element (`ChromePassElement::draw()`) that runs once per frame per window and:
|
Render-pass element (`src/ChromePassElement.hpp/.cpp`): an `EK_CUSTOM` pass element (`ChromePassElement::draw()`) that runs once per frame per window and:
|
||||||
- Derives the corner chamfer live from the window's own `rounding()` (in device px, scaled by monitor scale) — so the border's cut corners track the window's rounding through config reloads, per-window rules, and animations.
|
- Derives the corner chamfer live from the window's own `rounding()` (in device px, scaled by monitor scale) — so the border's cut corners track the window's rounding through config reloads, per-window rules, and animations.
|
||||||
|
|||||||
+59
-15
@@ -39,11 +39,7 @@ std::vector<size_t> Utf8CodepointStarts(const std::string& s) {
|
|||||||
// compositor's own border, and it only costs anything on a cache miss.
|
// compositor's own border, and it only costs anything on a cache miss.
|
||||||
constexpr int kStopsPerSegment = 8;
|
constexpr int kStopsPerSegment = 8;
|
||||||
|
|
||||||
// `alphaScale` multiplies every stop's own alpha - the glow layers below
|
cairo_pattern_t* CreateGradientPattern(const ChromeGradient& gradient, double w, double h) {
|
||||||
// reuse the border's gradient at a fraction of its opacity, and baking that
|
|
||||||
// into the pattern lets them cairo_fill() (which only touches the filled
|
|
||||||
// band) instead of clip+paint (which rasterizes the clip's whole extents).
|
|
||||||
cairo_pattern_t* CreateGradientPattern(const ChromeGradient& gradient, double w, double h, double alphaScale = 1.0) {
|
|
||||||
const auto axis = gradient.AxisFor(w, h);
|
const auto axis = gradient.AxisFor(w, h);
|
||||||
const auto pattern = cairo_pattern_create_linear(axis.x0, axis.y0, axis.x1, axis.y1);
|
const auto pattern = cairo_pattern_create_linear(axis.x0, axis.y0, axis.x1, axis.y1);
|
||||||
|
|
||||||
@@ -52,7 +48,7 @@ cairo_pattern_t* CreateGradientPattern(const ChromeGradient& gradient, double w,
|
|||||||
for (int i = 0; i <= steps; ++i) {
|
for (int i = 0; i <= steps; ++i) {
|
||||||
const double t = static_cast<double>(i) / steps;
|
const double t = static_cast<double>(i) / steps;
|
||||||
const auto color = gradient.SampleAt(static_cast<float>(t));
|
const auto color = gradient.SampleAt(static_cast<float>(t));
|
||||||
cairo_pattern_add_color_stop_rgba(pattern, t, color.r, color.g, color.b, color.a * alphaScale);
|
cairo_pattern_add_color_stop_rgba(pattern, t, color.r, color.g, color.b, color.a);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pattern;
|
return pattern;
|
||||||
@@ -230,6 +226,15 @@ constexpr float kGlowCornerSmoothing = 0.5F;
|
|||||||
// this much per px of inset makes the offset properly parallel instead.
|
// this much per px of inset makes the offset properly parallel instead.
|
||||||
constexpr float kChamferInsetShrink = 2.F - std::numbers::sqrt2_v<float>;
|
constexpr float kChamferInsetShrink = 2.F - std::numbers::sqrt2_v<float>;
|
||||||
|
|
||||||
|
// How far past the glow's own extent the colorizing pass' clip is pushed, on
|
||||||
|
// both of its edges. The clip is there purely to keep cairo from evaluating
|
||||||
|
// the gradient across the entire window (see DrawInwardGlow); it must never
|
||||||
|
// be what bounds the glow, because 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. Slack puts the clip where the mask is already
|
||||||
|
// zero, so it costs nothing and cuts nothing.
|
||||||
|
constexpr float kGlowClipSlack = 2.F;
|
||||||
|
|
||||||
// Bleeds the border color inward past the window edge - over the window's own
|
// Bleeds the border color inward past the window edge - over the window's own
|
||||||
// pixels, since this decoration renders on DECORATION_LAYER_OVER - fading out
|
// pixels, since this decoration renders on DECORATION_LAYER_OVER - fading out
|
||||||
// over `glowPx` and peaking at `strength` (times the gradient's own alpha) at
|
// over `glowPx` and peaking at `strength` (times the gradient's own alpha) at
|
||||||
@@ -249,6 +254,15 @@ constexpr float kChamferInsetShrink = 2.F - std::numbers::sqrt2_v<float>;
|
|||||||
// 1 - a_j = (1 - T_j) / (1 - T_j+1)
|
// 1 - a_j = (1 - T_j) / (1 - T_j+1)
|
||||||
//
|
//
|
||||||
// with T_j the target alpha sampled at band j's midpoint, and T_n+1 = 0.
|
// with T_j the target alpha sampled at band j's midpoint, and T_n+1 = 0.
|
||||||
|
//
|
||||||
|
// The layers are accumulated as pure *alpha*, into a scratch surface, with a
|
||||||
|
// solid source - then the gradient is applied to the result in a single
|
||||||
|
// masked pass. Filling each layer with the gradient directly, as this used to,
|
||||||
|
// makes every one of the ~glowPx layers pay for gradient evaluation, and cairo
|
||||||
|
// evaluates a gradient roughly eight times slower than a solid colour: at
|
||||||
|
// 1920x1080 with a 20px glow that measured 33ms per render against 4.3ms for
|
||||||
|
// the same layers filled solid. Splitting them costs one extra full-surface
|
||||||
|
// composite and still comes out ~3.5x ahead.
|
||||||
void DrawInwardGlow(cairo_t* cr, const ChromeDecorationGeometry& geo, const ChromeGradient& gradient, float glowPx, float strength, int w, int h) {
|
void DrawInwardGlow(cairo_t* cr, const ChromeDecorationGeometry& geo, const ChromeGradient& gradient, float glowPx, float strength, int w, int h) {
|
||||||
if (glowPx < 1.F || strength <= 0.F || geo.innerW <= 0 || geo.innerH <= 0)
|
if (glowPx < 1.F || strength <= 0.F || geo.innerW <= 0 || geo.innerH <= 0)
|
||||||
return;
|
return;
|
||||||
@@ -259,8 +273,12 @@ void DrawInwardGlow(cairo_t* cr, const ChromeDecorationGeometry& geo, const Chro
|
|||||||
return peak * std::pow(1.0 - std::clamp(depth / glowPx, 0.0, 1.0), kGlowFalloffExponent);
|
return peak * std::pow(1.0 - std::clamp(depth / glowPx, 0.0, 1.0), kGlowFalloffExponent);
|
||||||
};
|
};
|
||||||
|
|
||||||
cairo_save(cr);
|
// ARGB32 rather than A8 despite only the alpha channel being read: cairo has
|
||||||
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
|
// no optimized compositing path for A8 destinations, and rendering these same
|
||||||
|
// layers into one measured ~6x slower than into ARGB32.
|
||||||
|
const auto mask = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
|
||||||
|
const auto maskCr = cairo_create(mask);
|
||||||
|
cairo_set_fill_rule(maskCr, CAIRO_FILL_RULE_EVEN_ODD);
|
||||||
|
|
||||||
// Deepest (faintest) layer first, so each iteration already knows the
|
// Deepest (faintest) layer first, so each iteration already knows the
|
||||||
// accumulated target of everything that will composite under it.
|
// accumulated target of everything that will composite under it.
|
||||||
@@ -273,13 +291,12 @@ void DrawInwardGlow(cairo_t* cr, const ChromeDecorationGeometry& geo, const Chro
|
|||||||
// sub-pixel shift at these layer counts.
|
// sub-pixel shift at these layer counts.
|
||||||
const double target = targetAt(glowPx * (static_cast<double>(i) - 1.0) / layers);
|
const double target = targetAt(glowPx * (static_cast<double>(i) - 1.0) / layers);
|
||||||
|
|
||||||
const auto pattern = CreateGradientPattern(gradient, w, h, 1.0 - (1.0 - target) / (1.0 - deeperTarget));
|
cairo_set_source_rgba(maskCr, 0, 0, 0, 1.0 - (1.0 - target) / (1.0 - deeperTarget));
|
||||||
cairo_set_source(cr, pattern);
|
|
||||||
deeperTarget = target;
|
deeperTarget = target;
|
||||||
|
|
||||||
// Every layer's outer edge is the ring's inner boundary verbatim, so the
|
// Every layer's outer edge is the ring's inner boundary verbatim, so the
|
||||||
// glow always meets the frame exactly.
|
// glow always meets the frame exactly.
|
||||||
AppendChamferedRect(cr, geo.innerX0, geo.innerY0, geo.innerX1, geo.innerY1, geo.innerChamfer);
|
AppendChamferedRect(maskCr, geo.innerX0, geo.innerY0, geo.innerX1, geo.innerY1, geo.innerChamfer);
|
||||||
// Punches this layer's un-glowed middle back out - inset by `depth`, with
|
// Punches this layer's un-glowed middle back out - inset by `depth`, with
|
||||||
// the corner both parallel-corrected and rounded off in proportion to how
|
// the corner both parallel-corrected and rounded off in proportion to how
|
||||||
// deep it is. Since it's these edges that the accumulated falloff's
|
// deep it is. Since it's these edges that the accumulated falloff's
|
||||||
@@ -287,15 +304,42 @@ void DrawInwardGlow(cairo_t* cr, const ChromeDecorationGeometry& geo, const Chro
|
|||||||
// and progressively rounder inward. On a window smaller than the glow is
|
// and progressively rounder inward. On a window smaller than the glow is
|
||||||
// deep the middle collapses to nothing and the layer just covers all of
|
// deep the middle collapses to nothing and the layer just covers all of
|
||||||
// it, which is the right answer anyway.
|
// it, which is the right answer anyway.
|
||||||
AppendChamferedRect(cr,
|
AppendChamferedRect(maskCr,
|
||||||
geo.innerX0 + depth, geo.innerY0 + depth, geo.innerX1 - depth, geo.innerY1 - depth,
|
geo.innerX0 + depth, geo.innerY0 + depth, geo.innerX1 - depth, geo.innerY1 - depth,
|
||||||
geo.innerChamfer - depth * kChamferInsetShrink, depth * kGlowCornerSmoothing);
|
geo.innerChamfer - depth * kChamferInsetShrink, depth * kGlowCornerSmoothing);
|
||||||
cairo_fill(cr);
|
cairo_fill(maskCr);
|
||||||
|
|
||||||
cairo_pattern_destroy(pattern);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cairo_surface_flush(mask);
|
||||||
|
cairo_destroy(maskCr);
|
||||||
|
|
||||||
|
cairo_save(cr);
|
||||||
|
|
||||||
|
// Bound the gradient to the band it can actually land on. Without this,
|
||||||
|
// cairo_mask_surface evaluates the gradient over the mask's full extents -
|
||||||
|
// the whole window - rather than the perimeter-deep sliver that is non-zero,
|
||||||
|
// which at 1080p is the difference between ~5ms and ~25ms. kGlowClipSlack
|
||||||
|
// keeps both clip edges clear of the mask's own antialiasing.
|
||||||
|
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
|
||||||
|
AppendChamferedRect(cr,
|
||||||
|
geo.innerX0 - kGlowClipSlack, geo.innerY0 - kGlowClipSlack,
|
||||||
|
geo.innerX1 + kGlowClipSlack, geo.innerY1 + kGlowClipSlack, geo.innerChamfer);
|
||||||
|
AppendChamferedRect(cr,
|
||||||
|
geo.innerX0 + glowPx + kGlowClipSlack, geo.innerY0 + glowPx + kGlowClipSlack,
|
||||||
|
geo.innerX1 - glowPx - kGlowClipSlack, geo.innerY1 - glowPx - kGlowClipSlack,
|
||||||
|
geo.innerChamfer - glowPx * kChamferInsetShrink, glowPx * kGlowCornerSmoothing);
|
||||||
|
cairo_clip(cr);
|
||||||
|
|
||||||
|
// The gradient carries its own per-stop alpha and the mask carries the
|
||||||
|
// falloff, so the product is what the per-layer gradient fills produced
|
||||||
|
// before: gradientAlpha(p) * accumulated(p), in the gradient's own colour.
|
||||||
|
const auto pattern = CreateGradientPattern(gradient, w, h);
|
||||||
|
cairo_set_source(cr, pattern);
|
||||||
|
cairo_mask_surface(cr, mask, 0, 0);
|
||||||
|
cairo_pattern_destroy(pattern);
|
||||||
|
|
||||||
cairo_restore(cr);
|
cairo_restore(cr);
|
||||||
|
cairo_surface_destroy(mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traces the frame's outer silhouette with a solid `outlinePx`-thick line -
|
// Traces the frame's outer silhouette with a solid `outlinePx`-thick line -
|
||||||
|
|||||||
Reference in New Issue
Block a user