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

commitbab2ab347b86907961f89e2545c35cfaed470a1e
parentaeb826a6ad
authorLucas Galante <[email protected]>
date2026-08-25 08:14
fix: droplet lens orientation and content source

Two bugs from one wrong assumption, found live (black flipped boxes
behind the modules):

- The silhouette copied the corner shaders' extra y flip, which is
  unverifiable from the symmetric shapes that use it — equal radii
  look identical flipped. The droplet's asymmetric silhouette exposed
  it: attach taper rendered at the BOTTOM. gl_FragCoord minus the box
  position is already top-down box-local in this pipeline; the flip is
  gone, and the sample offset drops its y negation for the same
  reason.

- The lens sampled optimized_no_blur_buffer, which captures BOTTOM
  LAYERS only (the DE's frost design blurs only the bottom layer;
  windows reach translucent surfaces by ordinary alpha blending). An
  opaque lens fed from it replaced live window content with the
  desktop hiding behind it. It now snapshots the FRAME-SO-FAR: the
  drop's neighborhood (box + refraction reach + ghost span) is copied
  from pass->buffer into blur_saved_pixels_buffer via
  fx_render_pass_read_to_buffer immediately before the draw, so the
  drop refracts exactly what sits beneath it this frame, windows
  included, with no bake-staleness dependency.

Verified headless (agent-4): drops over a window show the LOCAL panel
bending, the window seam visibly kinks around the drop rim at
refr=40, silhouette hugs the client drop. The live session needs its
next restart to pick this up; refraction stays disabled in the user
config until then.

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

 scenefx/render/fx_renderer/fx_pass.c            | 30 ++++++++++++++++++++-----
 scenefx/render/fx_renderer/shaders/droplet.frag | 18 ++++++++++-----
 2 files changed, 37 insertions(+), 11 deletions(-)

diff --git a/scenefx/render/fx_renderer/fx_pass.c b/scenefx/render/fx_renderer/fx_pass.c
index 9fff1f9..06fd116 100644
--- a/scenefx/render/fx_renderer/fx_pass.c
+++ b/scenefx/render/fx_renderer/fx_pass.c
@@ -1171,17 +1171,37 @@ void fx_render_pass_add_droplet(struct fx_gles_render_pass *pass,
 	struct wlr_box box = options->box;
 	assert(box.width > 0 && box.height > 0);
 
-	// The lens samples the UNBLURRED snapshot of everything beneath the bar
-	// layer — the same capture the optimized blur bakes from. Without it
-	// (blur disabled DE-wide) there is nothing to refract; the client's own
-	// translucent drop still renders, so skipping is a graceful degrade.
+	// The lens samples the FRAME-SO-FAR — everything already rendered below
+	// this node (desktop, windows, all of it), copied 1:1 into the saved-
+	// pixels buffer just before the draw, because GL cannot sample the
+	// framebuffer it is rendering into. The optimized-blur bake is NOT used:
+	// it captures bottom layers only (that's the DE's frost design), so a
+	// drop over a window would refract the desktop hiding behind that
+	// window instead of the window itself. Without offscreen buffers there
+	// is nothing to refract; the client's own translucent drop still
+	// renders, so skipping is a graceful degrade.
 	if (pass->fx_offscreen_buffers == NULL) {
 		return;
 	}
-	struct fx_framebuffer *snap = pass->fx_offscreen_buffers->optimized_no_blur_buffer;
+	struct fx_framebuffer *snap = pass->fx_offscreen_buffers->blur_saved_pixels_buffer;
 	if (snap == NULL || snap->buffer == NULL) {
 		return;
 	}
+
+	// Copy the drop's neighborhood: the box grown by the refraction reach
+	// and the inverted ghost's sampling span (0.35 x the box about its
+	// center), clamped to the buffer.
+	int margin = (int)(options->refr + 1.0f
+			+ 0.35f * (float)(box.width > box.height ? box.width : box.height));
+	pixman_region32_t copy_region;
+	pixman_region32_init_rect(&copy_region,
+			box.x - margin, box.y - margin,
+			box.width + 2 * margin, box.height + 2 * margin);
+	pixman_region32_intersect_rect(&copy_region, &copy_region,
+			0, 0, pass->buffer->buffer->width, pass->buffer->buffer->height);
+	fx_render_pass_read_to_buffer(pass, &copy_region, snap, pass->buffer);
+	pixman_region32_fini(&copy_region);
+
 	struct wlr_texture *wlr_texture =
 		fx_texture_from_buffer(&renderer->wlr_renderer, snap->buffer);
 	if (wlr_texture == NULL) {
diff --git a/scenefx/render/fx_renderer/shaders/droplet.frag b/scenefx/render/fx_renderer/shaders/droplet.frag
index 0395d28..186909f 100644
--- a/scenefx/render/fx_renderer/shaders/droplet.frag
+++ b/scenefx/render/fx_renderer/shaders/droplet.frag
@@ -74,9 +74,14 @@ float drop_dist(vec2 p) {
 }
 
 void main() {
-	// Box-local coords with y down, mirroring the corner shaders' transform.
+	// Box-local coords with y down. NOTE: deliberately NOT the corner
+	// shaders' extra y flip — that transform is unverifiable from the
+	// symmetric shapes that use it (equal radii look the same flipped), and
+	// with this drop's asymmetric silhouette it rendered the attach taper at
+	// the BOTTOM (live bug: black flipped boxes behind the modules).
+	// gl_FragCoord minus the box position is already top-down box-local
+	// here.
 	vec2 rel = gl_FragCoord.xy - position;
-	rel.y = size.y - rel.y;
 
 	float d = drop_dist(rel);
 	float aa = clamp(-d + 0.5, 0.0, 1.0);
@@ -94,11 +99,12 @@ void main() {
 	float t = clamp(-d / max(band_px, 1.0), 0.0, 1.0);
 	float lens = (1.0 - t) * (1.0 - t);
 
-	// Refraction: offset the sample along the gradient (scene y-down offsets
-	// negate y into gl_FragCoord space). Positive refr samples outward — the
-	// background bends around the drop edge.
+	// Refraction: offset the sample along the gradient. gl_FragCoord shares
+	// the box-local frame's orientation (see `rel` above), so the offset
+	// applies directly. Positive refr samples outward — the background bends
+	// around the drop edge.
 	vec2 off = grad * (refr * lens);
-	vec2 uv = (gl_FragCoord.xy + vec2(off.x, -off.y)) / tex_size;
+	vec2 uv = (gl_FragCoord.xy + off) / tex_size;
 	vec4 col = texture2D(tex, clamp(uv, vec2(0.0), vec2(1.0)));
 
 	// Inverted lens ghost: the belly shows a faint upside-down, minified