Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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(©_region,
+ box.x - margin, box.y - margin,
+ box.width + 2 * margin, box.height + 2 * margin);
+ pixman_region32_intersect_rect(©_region, ©_region,
+ 0, 0, pass->buffer->buffer->width, pass->buffer->buffer->height);
+ fx_render_pass_read_to_buffer(pass, ©_region, snap, pass->buffer);
+ pixman_region32_fini(©_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