Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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