GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(frost): snapshot for every fill prim, and a blur you can see
The frosted menus shipped without frost. Two independent causes, found by
measuring pixels rather than trusting the pipeline:
The frame snapshot only triggered for Plate/Bevel/Droplet prims with a
negative fill alpha. The Dropdown popover's face is a Border prim and the
menubar panels are plain Quads, so their sentinel reached the shader with
the STALE SCENE BACKDROP bound instead of the frame-so-far: resolve_blur
returned a flat tint — translucent-looking, but showing no content and
blurring nothing. (The context menu, a Plate, was the one flavor that
actually worked, and the earlier "20% show-through" measurement was the
List row's own hover tint masquerading as evidence.) The blur_behind
matcher now covers every fill-bearing prim — Quad, RoundedRect, and
Border's fill included.
And the kernel was too tight to read as blur at all: 7x7 taps at 2.5px
stride is ±7.5px of reach, which leaves fine detail legible through the
glass. The stride is now 5.5px (±16.5px, effective sigma ~11px); the
linear sampler still papers over the gaps.
Verified in cce-files: the selected row now diffuses through the open
dropdown as a soft glow (10,753 pixels changed in the popover region vs
the broken build), where both prior builds rendered pixel-identical
"frost".
Co-Authored-By: Claude Fable 5 <[email protected]>
src/backend/window_runner.rs | 17 ++++++++++++++---
src/vk/shader2d.wgsl | 9 ++++++---
2 files changed, 20 insertions(+), 6 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b959880..b81104b 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1742,13 +1742,24 @@ pub fn tessellate_display_list(
let start = verts.len() as u32;
let mut plate: Option<crate::vk::PlatePush> = None;
let mut made_plate: Option<crate::scene::layout::Rect> = None;
- // Blur-behind marker: a Plate/Bevel whose fill alpha is negative asks
- // the renderer to snapshot the frame-so-far before it draws.
+ // Blur-behind marker: a prim whose FILL alpha is negative asks the
+ // renderer to snapshot the frame-so-far before it draws. Every
+ // fill-bearing prim counts — the shader's a<0 branch runs for all of
+ // them, and a variant missing here still frosts, but against the
+ // stale scene backdrop instead of the frame: a flat tint with no
+ // content and no blur, which is how the Dropdown popover (Border)
+ // and the menubar panels (Quad) shipped visibly unfrosted while the
+ // context menu (Plate) worked.
let blur_behind = matches!(
&item.prim,
crate::scene::paint::Prim::Bevel { color, .. }
| crate::scene::paint::Prim::Plate { color, .. }
- | crate::scene::paint::Prim::Droplet { color, .. } if color[3] < 0.0
+ | crate::scene::paint::Prim::Droplet { color, .. }
+ | crate::scene::paint::Prim::Quad { color, .. }
+ | crate::scene::paint::Prim::RoundedRect { color, .. } if color[3] < 0.0
+ ) || matches!(
+ &item.prim,
+ crate::scene::paint::Prim::Border { fill, .. } if fill[3] < 0.0
);
// Logical [cx, cy, r] → the physical-pixel triple the vertex attribute carries.
let no = item
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 3880d47..f4f9e58 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -840,11 +840,14 @@ fn resolve_blur(pos: vec2f, color: vec4f) -> vec4f {
var blurred = vec4f(0.0);
var total_weight = 0.0;
- // 7x7 Gaussian blur kernel, samples every 2.5 px (~±7.5px reach); the
- // linear sampler between taps papers over the stride.
+ // 7x7 Gaussian blur kernel, samples every 5.5 px (±16.5px reach,
+ // effective sigma ~11px); the linear sampler between taps papers over
+ // the stride. The old 2.5px stride (±7.5px reach) was technically a
+ // blur but read as plain translucency — fine detail beneath a frosted
+ // menu stayed legible, which is not what frosted glass does.
for (var x = -3.0; x <= 3.0; x += 1.0) {
for (var y = -3.0; y <= 3.0; y += 1.0) {
- let offset = vec2f(x, y) * 2.5;
+ let offset = vec2f(x, y) * 5.5;
let sample_uv = (pos + offset) / tex_size;
let weight = exp(-(x*x + y*y) / (2.0 * 2.0 * 2.0));
blurred += textureSample(t_backdrop, s_backdrop, sample_uv) * weight;