git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitf867cda14e5c0cd7c3fef90d6379b1d0bd3e17d3
parent2695dbce6d
authorLucas Galante <[email protected]>
date2026-08-28 11:46
fix(bevel): the focus glint adds light instead of cross-fading to it

The focused-window rim was dim in a way no amount of tuning fixed, and it
was two compounding mistakes rather than a taste problem.

The pass set GL_SRC_ALPHA/GL_ONE_MINUS_SRC_ALPHA while its own comment
said premultiplied, so the blend multiplied the fragment by alpha a second
time. Both branches emit colour already scaled by their intensity, so the
rim came out squared: the focus glint peaks at light_intensity (0.6), and
0.6 squared is 0.36 — nearly two thirds of the highlight thrown away
before it reached the screen.

Then the glint composited as a cross-fade TOWARD focus_color, which
ceilings it at the accent and darkens the rim wherever it crosses client
pixels brighter than the accent. That is why the band dipped BELOW the
window's own value partway across: the highlight was subtracting light.

So: the pass blends premultiplied (what the comment always claimed), and
the focus branch emits alpha 0 with premultiplied colour, which under that
blend is dst + focus_color*h — additive, linear in h. This is what cce-ui's
plate glint does (shader2d.wgsl adds its specular on top of the lit fill),
and matching it is the whole point of the branch.

Landed in two steps against a fixed headless scene, since a shader-only
attempt at this failed silently once already by assuming the wrong blend
func. The premultiplied conversion first, verified pixel-identical on both
branches (17205 pixels touched, zero differing by more than 1 — float
reassociation only); then the additive change, which lifts the peak rim
pixel from [86,120,127] to [178,242,255] and leaves the unfocused window
within 1 LSB.

Co-Authored-By: Claude Opus 5 <[email protected]>

 scenefx/render/fx_renderer/fx_pass.c          |  8 ++++++--
 scenefx/render/fx_renderer/shaders/bevel.frag | 19 +++++++++++++++++--
 2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/scenefx/render/fx_renderer/fx_pass.c b/scenefx/render/fx_renderer/fx_pass.c
index 9a97999..8f943ff 100644
--- a/scenefx/render/fx_renderer/fx_pass.c
+++ b/scenefx/render/fx_renderer/fx_pass.c
@@ -1134,9 +1134,13 @@ void fx_render_pass_add_bevel(struct fx_gles_render_pass *pass,
 	push_fx_debug(renderer);
 
 	// The highlight adds light and the shade subtracts it, both premultiplied
-	// into the same draw — ordinary source-over blending.
+	// into the same draw — ordinary source-over blending. The src factor is
+	// GL_ONE because the shader emits premultiplied colour: with GL_SRC_ALPHA
+	// the blend multiplied by alpha a SECOND time, squaring the rim's
+	// intensity, and left no way for a branch to composite additively (alpha
+	// 0 zeroed the whole contribution instead of adding it).
 	setup_blending(WLR_RENDER_BLEND_MODE_PREMULTIPLIED);
-	glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE);
+	glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE);
 
 	glUseProgram(renderer->shaders.bevel.program);
 
diff --git a/scenefx/render/fx_renderer/shaders/bevel.frag b/scenefx/render/fx_renderer/shaders/bevel.frag
index 387432d..086a0c1 100644
--- a/scenefx/render/fx_renderer/shaders/bevel.frag
+++ b/scenefx/render/fx_renderer/shaders/bevel.frag
@@ -91,13 +91,27 @@ void main() {
     // window's own rounded edge.
     float edge_aa = clamp(-dist, 0.0, 1.0);
 
+    // Overall scale on the effect: the tint's alpha times the outer feather.
+    float k = v_color.a * edge_aa;
+
     if (focus > 0.5) {
         // Focused window: the familiar lit-edge highlight, wrapped — every
         // edge shades as the one facing the light, in the accent color, no
         // shade side. Same slope profile, so band width and shoulder match
         // the unfocused rim exactly.
         float h = light_intensity * slope;
-        gl_FragColor = vec4(focus_color * h, h) * v_color.a * edge_aa;
+        // ADDITIVE: alpha 0 with premultiplied colour gives dst + rgb under
+        // the pass's GL_ONE/GL_ONE_MINUS_SRC_ALPHA blend, so the glint ADDS
+        // light to the window instead of cross-fading toward the accent.
+        // Two reasons that matters. A cross-fade ceilings the whole effect at
+        // focus_color and DARKENS the rim wherever it crosses client pixels
+        // brighter than the accent, which is why the old rim dipped below the
+        // window's own value. And the alpha term is gone, so the intensity is
+        // linear in h rather than squared — h peaked at light_intensity (0.6),
+        // so squaring cost the highlight nearly two thirds of its strength.
+        // This is what cce-ui's plate does: shader2d.wgsl adds its specular on
+        // top of the lit fill rather than blending toward it.
+        gl_FragColor = vec4(focus_color * h * k, 0.0);
         return;
     }
 
@@ -112,6 +126,7 @@ void main() {
     // the tint (and its alpha scales the whole effect).
     vec3 rgb = v_color.rgb * highlight;
     float alpha = highlight + shadow;
+    float a = alpha * k;
 
-    gl_FragColor = vec4(rgb, alpha) * v_color.a * edge_aa;
+    gl_FragColor = vec4(rgb * k * a, a);
 }