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

commitadaf405e24f428aeb3e17d3aa3afd4a5ef3636b9
parent3fb4404240
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-29 15:59
scenefx: rounded rects draw every pixel they claim as opaque

An alpha-1 wlr_scene_rect with a corner radius reports its box minus the
corner squares as opaque, so whatever lies beneath is culled there, the
black background clear included. quad_round.frag did not honour that:
its inlined distance flipped y against size.y where the rest of the math
works in size - 1, leaving the box's top row at alpha 0 and the row
under it at 0.5, and its AA ramp ran -0.5..0.5, leaving every straight
edge pixel at 0.5.

On the desktop grid (opaque black rounded cells over the gap-coloured
backdrop) those pixels blended over whatever the buffer last held: a
tint of the last thing drawn over a cell's top row, constant in the
first row and halving per repaint in the second. A full redraw hid it
only because the clear colour and the cells are both black.

The shader now takes its distance from corner_dist, like the clip and
the buffer corner cut. Verified in a shadow at 1280x720, zoom 0.404:
before/after screenshots of a rubber-band drag differ in 0 pixels (4188
before), also after a pan away and back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

 CLAUDE.md                                          | 19 +++++++++
 scenefx/render/fx_renderer/shaders/quad_round.frag | 47 +++++++++-------------
 2 files changed, 39 insertions(+), 27 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 39a5fc1e..2509fea7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -424,6 +424,25 @@ treats them as opaque.
 - Output: `output.rs`, `output_manager.rs`. Session: `lock_manager.rs`,
   `idle_inhibit_manager.rs`. Rendering: `scene.rs`, `scene_node_data.rs`.
 
+### A node must draw everything it claims as opaque
+
+`scene_node_opaque_region` (scenefx `wlr_scene.c`) is a promise: whatever a
+node reports there is culled from every node beneath it AND from the black
+background clear, so a pixel inside it that the node's shader draws at less
+than full alpha blends over whatever the buffer last held. An alpha-1
+`wlr_scene_rect` with a corner radius reports its box minus the corner
+squares, and until 2026-09-28 `quad_round.frag` did not honour that: its
+inlined distance was one pixel off vertically (top row alpha 0, the next
+0.5) and its AA ramp left every straight edge pixel at 0.5. On the desktop
+grid — opaque black rounded cells over the gap-coloured backdrop — that was
+a tint of whatever had last covered a cell's top row, constant in the first
+row and halving per repaint in the second, invisible after a full redraw
+only because the clear colour and the cells are both black. The shader now
+takes its distance from `corner_dist`, like the clip and the buffer corner
+cut. To look for this kind of residue, screenshot a shadow before and after
+drawing over the grid and diff; every desktop pixel should be a multiple of
+the gap colour.
+
 ### Window move/resize handles
 
 Pointer move and resize exist **only in adjust mode** (overview, or Super
diff --git a/scenefx/render/fx_renderer/shaders/quad_round.frag b/scenefx/render/fx_renderer/shaders/quad_round.frag
index 0097061d..de063f36 100644
--- a/scenefx/render/fx_renderer/shaders/quad_round.frag
+++ b/scenefx/render/fx_renderer/shaders/quad_round.frag
@@ -19,37 +19,30 @@ uniform float clip_radius_top_right;
 uniform float clip_radius_bottom_left;
 uniform float clip_radius_bottom_right;
 
-float get_dist(vec2 q, float radius);
+float corner_dist(vec2 size, vec2 position,
+		float radius_tl, float radius_tr, float radius_bl, float radius_br);
 float corner_alpha(vec2 size, vec2 position, bool is_cutout,
 		float radius_tl, float radius_tr, float radius_bl, float radius_br);
 
 void main() {
-	vec2 relative_pos = (gl_FragCoord.xy - (position + 0.5));
-	relative_pos.y = size.y - relative_pos.y;
-
-	// Bounding box check
-	/* if (relative_pos.x < -0.5 || relative_pos.y < -0.5
-			|| relative_pos.x > size.x - 0.5 || relative_pos.y > size.y - 0.5) {
-		discard;
-	} */
-
-	float r_tl = radius_top_left;
-	float r_tr = radius_top_right;
-	float r_bl = radius_bottom_left;
-	float r_br = radius_bottom_right;
-
-	// Calculate corner distance
-	vec2 top_left = abs(relative_pos - (size - 1.0)) - (size - 1.0) + r_tl;
-	vec2 top_right = abs(relative_pos - vec2(0.0, size.y - 1.0)) - (size - 1.0) + r_tr;
-	vec2 bottom_left = abs(relative_pos - vec2(size.x - 1.0, 0.0)) - (size - 1.0) + r_bl;
-	vec2 bottom_right = abs(relative_pos) - (size - 1.0) + r_br;
-
-	float dist = max(
-		max(get_dist(top_left, r_tl), get_dist(top_right, r_tr)),
-		max(get_dist(bottom_left, r_bl), get_dist(bottom_right, r_br))
-	);
-
-	float result = smoothstep(-0.5, 0.5, dist);
+	// The outline's signed distance, from the same function the clip below
+	// and the buffer corner cut use, so an edge pixel sits at dist 0.
+	//
+	// This was inlined here with two differences that made the node draw
+	// less than the scene thinks it covers. The y flip subtracted from
+	// size.y where the rest of the math works in size - 1, so the distance
+	// was one pixel off vertically: the box's top row got alpha 0 and the
+	// row under it 0.5. And the smoothstep ran -0.5..0.5, which left every
+	// straight edge pixel at half alpha. scene_node_opaque_region counts an
+	// alpha-1 rounded rect as opaque outside its corner squares, so what
+	// lay beneath those rows was culled and they blended over whatever the
+	// buffer last held — a tint that halved on every repaint, or never
+	// changed at all in the alpha-0 row.
+	float dist = corner_dist(size - 1.0, position + 0.5,
+		radius_top_left, radius_top_right,
+		radius_bottom_left, radius_bottom_right);
+
+	float result = smoothstep(0.0, 1.0, dist);
 	float quad_corner_alpha = 1.0 - result;
 
 	// Clipping