Take the gradient out of the inward glow's per-layer fills
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>
This commit is contained in:
+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.
|
||||
constexpr int kStopsPerSegment = 8;
|
||||
|
||||
// `alphaScale` multiplies every stop's own alpha - the glow layers below
|
||||
// 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) {
|
||||
cairo_pattern_t* CreateGradientPattern(const ChromeGradient& gradient, double w, double h) {
|
||||
const auto axis = gradient.AxisFor(w, h);
|
||||
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) {
|
||||
const double t = static_cast<double>(i) / steps;
|
||||
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;
|
||||
@@ -230,6 +226,15 @@ constexpr float kGlowCornerSmoothing = 0.5F;
|
||||
// this much per px of inset makes the offset properly parallel instead.
|
||||
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
|
||||
// pixels, since this decoration renders on DECORATION_LAYER_OVER - fading out
|
||||
// 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)
|
||||
//
|
||||
// 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) {
|
||||
if (glowPx < 1.F || strength <= 0.F || geo.innerW <= 0 || geo.innerH <= 0)
|
||||
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);
|
||||
};
|
||||
|
||||
cairo_save(cr);
|
||||
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
|
||||
// ARGB32 rather than A8 despite only the alpha channel being read: cairo has
|
||||
// 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
|
||||
// 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.
|
||||
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(cr, pattern);
|
||||
cairo_set_source_rgba(maskCr, 0, 0, 0, 1.0 - (1.0 - target) / (1.0 - deeperTarget));
|
||||
deeperTarget = target;
|
||||
|
||||
// Every layer's outer edge is the ring's inner boundary verbatim, so the
|
||||
// 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
|
||||
// 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
|
||||
@@ -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
|
||||
// deep the middle collapses to nothing and the layer just covers all of
|
||||
// it, which is the right answer anyway.
|
||||
AppendChamferedRect(cr,
|
||||
AppendChamferedRect(maskCr,
|
||||
geo.innerX0 + depth, geo.innerY0 + depth, geo.innerX1 - depth, geo.innerY1 - depth,
|
||||
geo.innerChamfer - depth * kChamferInsetShrink, depth * kGlowCornerSmoothing);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_pattern_destroy(pattern);
|
||||
cairo_fill(maskCr);
|
||||
}
|
||||
|
||||
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_surface_destroy(mask);
|
||||
}
|
||||
|
||||
// Traces the frame's outer silhouette with a solid `outlinePx`-thick line -
|
||||
|
||||
Reference in New Issue
Block a user