git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit144c18b65d8cbf91f96cb17faf1f4c4e4c8c3dd3
parent5f478a61d4
authorLucas Galante <[email protected]>
date2026-09-20 15:02
The frost recipe rides the plate's push block, not the window (RFC material, step 3a)

Frost::pack puts compression and refraction in p_host.z as 12-bit fixed
point (an integer below 2^24, exact in f32; Frost::unpack is the Rust
twin, and the shader literals are checked against its text) and the
blur sigma in p_host.w in physical px. The plate branch unpacks its own
recipe; resolve_blur takes k and the kernel stride as arguments, with
stride 0 a clear plate (one clean sample). backdrop_meta leaves
WindowInfo (336 -> 320 bytes; the size test shrank with it) and the
renderer no longer tracks the two knobs for re-upload.

DEFAULT_RADIUS is 5.5 logical px, not 11: the old kernel was a fixed
5.5 PHYSICAL px stride, half the blur on the scale-2 panel everything
was tuned on, and a material cannot know the scale — 5.5 logical is 11
physical there, the panel exactly; a scale-1 display gets the same
logical blur instead of twice it. The droplet (push block full) and a
raw negative-alpha vertex from outside the display list use the same
stride as a literal, LEGACY_STRIDE, with no compression.

Golden diff against the pre-2a dump: 10 lines, the five frosted plate
batches at two scales, each differing only in host.w.

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

 src/backend/window_runner.rs | 10 ++++-
 src/scene/material.rs        | 82 +++++++++++++++++++++++++++++++++++----
 src/vk/renderer.rs           | 39 ++++---------------
 src/vk/shader2d.wgsl         | 91 ++++++++++++++++++++++++++++----------------
 4 files changed, 150 insertions(+), 72 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index a48b17c..3be55aa 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1923,6 +1923,10 @@ pub fn tessellate_display_list(
                 // curvature-matched span.
                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
                 let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, mat, false, None);
+                // The plate's own frost recipe rides host.zw (see PlatePush).
+                let [fz, fw] = material.frost.pack(scale);
+                p.host[2] = fz;
+                p.host[3] = fw;
                 // w = 1 marks an accent-tinted plate (the focused-pane
                 // treatment): the shader then colors the WHOLE rolled edge
                 // with the tint, not just the specular glint — matching the
@@ -1951,7 +1955,11 @@ pub fn tessellate_display_list(
                     // Same lit-plate branch; the cover quad is the exact rect so the
                     // silhouette and the compositor's rounded window corners agree.
                     verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, color));
-                    plate = Some(plate_push_raised(rect, *radii, *depth, scale, plate_light, mat, true, *shape));
+                    let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, mat, true, *shape);
+                    let [fz, fw] = material.frost.pack(scale);
+                    p.host[2] = fz;
+                    p.host[3] = fw;
+                    plate = Some(p);
                     made_plate = Some(*rect);
                 }
             }
diff --git a/src/scene/material.rs b/src/scene/material.rs
index 49ebd46..0603bd9 100644
--- a/src/scene/material.rs
+++ b/src/scene/material.rs
@@ -75,18 +75,53 @@ pub enum Frost {
         /// How far the plate's roll bends what it samples — the objecthood
         /// control. 0..1. `style.surface.plate.refraction` today.
         refraction: f32,
-        /// Blur radius (the kernel's sigma) in logical px. Carried from step 1
-        /// so `Frost` changes shape once (RFC § 11 (2)); the renderer reads
-        /// it from step 3. [`Frost::DEFAULT_RADIUS`] is today's literal
-        /// kernel; 0 will be a CLEAR plate — one clean sample, tinted.
+        /// Blur radius — the kernel's sigma — in logical px.
+        /// [`Frost::DEFAULT_RADIUS`] is the kernel every frosted plate had;
+        /// 0 is a CLEAR plate: one clean sample, tinted.
         radius: f32,
     },
 }
 
 impl Frost {
-    /// The sigma of `resolve_blur`'s kernel as shipped: 7×7 taps at a 5.5 px
-    /// stride, sigma 2 taps — ≈ 11 logical px at scale 1.
-    pub const DEFAULT_RADIUS: f32 = 11.0;
+    /// The kernel every frosted plate had, as a sigma in logical px.
+    ///
+    /// Before the recipe was per plate, `resolve_blur` sampled a 7×7 kernel
+    /// at a fixed 5.5 PHYSICAL px stride (sigma two taps = 11 physical px)
+    /// — half the blur on a scale-2 panel that it was on a scale-1 one, and
+    /// the panel every frosted surface was tuned on is scale 2. A material
+    /// cannot know the scale, so the default is stated in logical px at the
+    /// value that reproduces the panel exactly: 5.5 logical = 11 physical at
+    /// scale 2. A scale-1 display now gets the same logical blur instead of
+    /// twice it.
+    pub const DEFAULT_RADIUS: f32 = 5.5;
+
+    /// Fixed-point width of `compression` and `refraction` inside one push
+    /// float: `c·4095·4096 + r·4095` is an integer below 2²⁴, exact in f32.
+    /// Mirrors the shader's `FROST_PACK_MAX` / `FROST_PACK_BASE`.
+    pub const PACK_MAX: f32 = 4095.0;
+    pub const PACK_BASE: f32 = 4096.0;
+
+    /// The recipe as the plate branch reads it: `[p_host.z, p_host.w]` —
+    /// compression and refraction packed in `z`, the blur radius in `w` as
+    /// the kernel sigma in PHYSICAL px (the shader samples the backdrop in
+    /// physical px). `Opaque` packs to zeros: nothing reads them, and a
+    /// plate that was never frosted pushes the bytes it always did.
+    pub fn pack(&self, scale: f32) -> [f32; 2] {
+        match *self {
+            Frost::Opaque => [0.0, 0.0],
+            Frost::Frosted { compression, refraction, radius } => {
+                let q = |v: f32| (v.clamp(0.0, 1.0) * Self::PACK_MAX).round();
+                [q(compression) * Self::PACK_BASE + q(refraction), radius.max(0.0) * scale]
+            }
+        }
+    }
+
+    /// The Rust twin of the shader's unpack: `(compression, refraction)`
+    /// from a packed `z`.
+    pub fn unpack(z: f32) -> (f32, f32) {
+        let hi = (z / Self::PACK_BASE).floor();
+        (hi / Self::PACK_MAX, (z - hi * Self::PACK_BASE) / Self::PACK_MAX)
+    }
 
     /// The DE's frost, from the plate-rung keys every frosted surface reads
     /// today (the window-wide recipe, until step 3 makes it per plate).
@@ -389,6 +424,39 @@ mod tests {
         assert_eq!(Material::face([0.3, 0.3, 0.3, 0.7]).map(|m| m.tint), Some([0.3, 0.3, 0.3, 0.7]));
     }
 
+    /// The pack is exact on its own grid, monotone, and never mixes the two
+    /// halves; the shader's literals are the ones the Rust twin uses.
+    #[test]
+    fn frost_pack_round_trips() {
+        for i in [0u32, 1, 2, 613, 614, 2047, 2048, 4094, 4095] {
+            for j in [0u32, 1, 819, 4095] {
+                let (c, r) = (i as f32 / Frost::PACK_MAX, j as f32 / Frost::PACK_MAX);
+                let f = Frost::Frosted { compression: c, refraction: r, radius: 5.5 };
+                let [z, w] = f.pack(2.0);
+                let (c2, r2) = Frost::unpack(z);
+                assert!((c2 - c).abs() < 1e-6 && (r2 - r).abs() < 1e-6, "{i},{j}: {c},{r} -> {c2},{r2}");
+                assert_eq!(w, 11.0);
+                assert!(z < (1u32 << 24) as f32, "packed value must stay an exact f32 integer");
+            }
+        }
+        // 0.6 / 0.3 (the designer's recipe) survive to better than a 1/255 step.
+        let (c, r) = Frost::unpack(Frost::Frosted { compression: 0.6, refraction: 0.3, radius: 0.0 }.pack(1.0)[0]);
+        assert!((c - 0.6).abs() < 1.0 / 510.0 && (r - 0.3).abs() < 1.0 / 510.0);
+        assert_eq!(Frost::Opaque.pack(2.0), [0.0, 0.0]);
+        assert_eq!(Frost::Frosted { compression: 0.0, refraction: 0.0, radius: 0.0 }.pack(2.0), [0.0, 0.0]);
+
+        let wgsl = include_str!("../vk/shader2d.wgsl");
+        let lit = |name: &str| -> f32 {
+            let rest = wgsl.split(&format!("const {name}: f32 = ")).nth(1).unwrap_or_else(|| panic!("{name} missing"));
+            rest.split(';').next().unwrap().trim().parse().unwrap()
+        };
+        assert_eq!(lit("FROST_PACK_MAX"), Frost::PACK_MAX);
+        assert_eq!(lit("FROST_PACK_BASE"), Frost::PACK_BASE);
+        // The droplet's and the raw-vertex fallback's stride is the panel's
+        // default kernel in physical px: DEFAULT_RADIUS × scale 2 / 2.
+        assert_eq!(lit("LEGACY_STRIDE"), Frost::DEFAULT_RADIUS * 2.0 / 2.0);
+    }
+
     /// A popover is the base colour at menu opacity, frosted — the bytes the
     /// three menu sites used to write by negating an alpha.
     #[test]
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index 05b2e54..e922b21 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -83,9 +83,11 @@ pub struct PlatePush {
     pub light: [f32; 4],
     /// [shading strength, specular strength, shininess, curvature/AO strength].
     pub material: [f32; 4],
-    /// Mode 1: `[feature offset, feature count, 0, 0]` into the frame's
-    /// `plate_features` — the carves CSG'd out of this plate (the renderer adds
-    /// the frame slot's base offset at record time). Mode 14 uses the same
+    /// Mode 1: `[feature offset, feature count, frost z, frost w]` — xy into
+    /// the frame's `plate_features`, the carves CSG'd out of this plate (the
+    /// renderer adds the frame slot's base offset at record time); zw the
+    /// plate's frost recipe, `scene::material::Frost::pack` (compression and
+    /// refraction packed in z, the blur sigma in physical px in w). Mode 14 uses the same
     /// `[offset, count]` for the union's boxes. Mode 2: the host-plate box
     /// (center + half-extents) a free recess fades out against; far-away sides
     /// (±1e5) disable the fade.
@@ -135,7 +137,7 @@ const PLATE_FEATURE_BYTES: usize = 48;
 // [size/clip vec4][carve profile meta][8 carve slopes][roll profile meta]
 // [8 roll slopes][relief heights][backdrop meta] = 21 vec4. Grows only at the
 // END — every offset above is addressed by index from both sides.
-const WINDOW_INFO_BYTES: vk::DeviceSize = 336;
+const WINDOW_INFO_BYTES: vk::DeviceSize = 320;
 
 pub(crate) struct AllocatedBuffer {
     pub(crate) buffer: vk::Buffer,
@@ -259,8 +261,6 @@ pub struct VkRenderer {
     /// last uploaded in WindowInfo — compared each frame, since editors set
     /// them straight into the style registry with no generation counter.
     relief_uploaded: (f32, f32),
-    compression_uploaded: f32,
-    refraction_uploaded: f32,
     /// Same for the edge (roll) profile LUT.
     roll_profile_gen: u64,
     plate_features: AllocatedBuffer,
@@ -877,8 +877,6 @@ impl VkRenderer {
             window_info,
             profile_gen: 0,
             relief_uploaded: (0.0, 0.0),
-            compression_uploaded: 0.0,
-            refraction_uploaded: 0.0,
             roll_profile_gen: 0,
             plate_features,
             frames,
@@ -915,19 +913,6 @@ impl VkRenderer {
         (self.corner_radius_px * crate::layout::corner_span_factor()).min(cap)
     }
 
-    /// How hard a frosted plate compresses its backdrop's luminance toward
-    /// its own key — the plate's legibility control, tracked for re-upload
-    /// like the relief heights because it is live-editable config.
-    fn backdrop_compression(&self) -> f32 {
-        crate::color::plate_backdrop_compression()
-    }
-
-    /// How far a plate's roll refracts its backdrop — tracked for re-upload
-    /// beside the compression, being live-editable config the same way.
-    fn refraction(&self) -> f32 {
-        crate::color::plate_refraction()
-    }
-
     /// The pinned relief heights in physical px, 0 = follow the width.
     fn relief_px(&self) -> (f32, f32) {
         let s = crate::scale::scale_factor().max(0.001);
@@ -940,8 +925,8 @@ impl VkRenderer {
     fn write_window_info(&mut self) {
         // [size/clip vec4][carve profile meta vec4][8 vec4 carve slopes]
         // [roll profile meta vec4][8 vec4 roll slopes][relief heights vec4]
-        // [backdrop meta vec4] — must stay in lockstep with shader2d's
-        // WindowInfo.
+        // — must stay in lockstep with shader2d's WindowInfo. (The frost
+        // recipe is per plate, in its push block, since RFC material step 3.)
         let mut data = [0.0f32; WINDOW_INFO_BYTES as usize / 4];
         data[0] = self.extent.width as f32;
         data[1] = self.extent.height as f32;
@@ -961,12 +946,6 @@ impl VkRenderer {
         data[76] = relief.0;
         data[77] = relief.1;
         self.relief_uploaded = relief;
-        let compression = self.backdrop_compression();
-        data[80] = compression;
-        self.compression_uploaded = compression;
-        let refraction = self.refraction();
-        data[81] = refraction;
-        self.refraction_uploaded = refraction;
         self.profile_gen = crate::layout::bevel_profile_generation();
         self.roll_profile_gen = crate::layout::roll_profile_generation();
         if let Some(allocation) = self.window_info.allocation.as_mut() {
@@ -1590,8 +1569,6 @@ impl VkRenderer {
             if self.profile_gen != crate::layout::bevel_profile_generation()
                 || self.roll_profile_gen != crate::layout::roll_profile_generation()
                 || self.relief_uploaded != self.relief_px()
-                || self.compression_uploaded != self.backdrop_compression()
-                || self.refraction_uploaded != self.refraction()
             {
                 self.write_window_info();
             }
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index e528c97..55c3d77 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -34,12 +34,6 @@ struct WindowInfo {
     // (p_light.w) they become the slope scale, so a pinned 0.5 mm drop is
     // the same geometry whatever wall it is cut with.
     relief_meta: vec4f,
-    // x = how hard a frosted plate pulls its backdrop's luminance toward its
-    // own key (0 = untouched, 1 = flat). See `resolve_blur`.
-    // y = rim refraction: how far the plate's roll displaces what it samples,
-    // and how much CLEARER the rim is than the frosted body. See MODE_PLATE.
-    // Appended last so the established offsets above keep their indices.
-    backdrop_meta: vec4f,
 }
 
 @group(0) @binding(2) var<uniform> window_info: WindowInfo;
@@ -115,7 +109,11 @@ struct RRectClip {
     // [shading strength, specular strength, shininess, curvature/AO strength].
     p_mat: vec4f,
     // Mode 1 (raised plate): xy = [offset, count] into plate_features — the
-    // carves CSG'd out of this plate's material.
+    // carves CSG'd out of this plate's material; z = the plate's FROST
+    // recipe, compression and refraction as 12-bit fixed point in one float
+    // (hi·FROST_PACK_BASE + lo, each over FROST_PACK_MAX — see
+    // material::Frost::pack); w = the blur kernel's sigma in physical px,
+    // 0 = a clear plate (one clean sample). Unread on an opaque plate.
     // Mode 2 (free recess overlay): the host-plate box (center + half-extents)
     // the carve fades out against — a wall flush with the host's edge dies
     // across the host's perimeter roll; far-away sides sit at ±1e5 (no fade).
@@ -132,6 +130,17 @@ var<push_constant> rrect_clip: RRectClip;
 // the `> 5.5` fillet branch or the fillet arm would have swallowed it, taken 4
 // off, and drawn every groove as a ridge. Equality makes a new mode inert
 // wherever it is added rather than silently captured by a neighbour.
+// Frost recipe packing — mirrored by `scene::material::Frost`, checked by
+// its tests against this text.
+const FROST_PACK_MAX: f32 = 4095.0;
+const FROST_PACK_BASE: f32 = 4096.0;
+// The kernel stride a frosted surface with NO recipe uses — the droplet
+// (its push block is full) and a raw negative-alpha vertex from outside the
+// display list: the panel's default kernel (Frost::DEFAULT_RADIUS at scale
+// 2), which is exactly the fixed 5.5 px stride every frosted plate had
+// before recipes were per plate.
+const LEGACY_STRIDE: f32 = 5.5;
+
 const MODE_NONE: i32 = 0;         // not a plate batch
 const MODE_PLATE: i32 = 1;        // raised lit plate: fill + rolled perimeter + CSG carves
 const MODE_RECESS: i32 = 2;       // free carve, interior one step DOWN
@@ -503,7 +512,9 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         }
         var base = vcol;
         if (vcol.a < 0.0) {
-            base = resolve_blur(frag, vcol, vec2f(0.0), 0.0);
+            // No recipe: every p_host slot is the drop's geometry. The
+            // kernel default, no compression (what a drop always drew).
+            base = resolve_blur(frag, vcol, vec2f(0.0), 0.0, 0.0, LEGACY_STRIDE);
         }
         let t2 = max(rrect_clip.p_light.w, 0.001);
         let u = clamp(din / t2, 0.0, 1.0);
@@ -569,10 +580,15 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         // drifting off it at another radius. The clarity ramp is f*f — the
         // clear window belongs to the outer third of the roll, and the face
         // must reach zero exactly or the whole plate unfrosts.
-        let refr = clamp(window_info.backdrop_meta.y, 0.0, 1.0);
+        // The plate's own recipe, from its push block (see RRectClip.p_host).
+        let fz = rrect_clip.p_host.z;
+        let fhi = floor(fz / FROST_PACK_BASE);
+        let k_plate = clamp(fhi / FROST_PACK_MAX, 0.0, 1.0);
+        let refr = clamp((fz - fhi * FROST_PACK_BASE) / FROST_PACK_MAX, 0.0, 1.0);
+        let stride = rrect_clip.p_host.w * 0.5;
         var base = vcol;
         if (vcol.a < 0.0) {
-            base = resolve_blur(frag, vcol, sv_rim * (refr * t), refr * f * f);
+            base = resolve_blur(frag, vcol, sv_rim * (refr * t), refr * f * f, k_plate, stride);
         }
         var extra = PLATE_CREST * f * f * f;
         let f_off = u32(rrect_clip.p_host.x);
@@ -956,11 +972,12 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
         return vec4f(c.rgb, c.a * clip_cov);
     }
 
-    // Blur-behind plate: negative alpha mixes the (blurred) backdrop with the
-    // plate color at |alpha| opacity.
+    // A raw negative-alpha vertex with no plate block: geometry pushed from
+    // outside the display list (a legacy host's own quads). No recipe to
+    // read, so the kernel default and no compression. Everything the
+    // display list frosts is a plate batch and never lands here.
     if (in.color.a < 0.0) {
-        // A plain blur-behind quad has no roll to refract through.
-        let c = resolve_blur(in.clip_position.xy, in.color, vec2f(0.0), 0.0);
+        let c = resolve_blur(in.clip_position.xy, in.color, vec2f(0.0), 0.0, 0.0, LEGACY_STRIDE);
         return vec4f(c.rgb, c.a * clip_cov);
     }
 
@@ -971,29 +988,37 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
 // FULLY blurred backdrop is the base (no clean-backdrop passthrough; mixing
 // the clean sample back in at plate opacity left translucent plates barely
 // blurred), tinted by the plate color at |alpha| opacity.
-fn resolve_blur(pos: vec2f, color: vec4f, refract: vec2f, clarity: f32) -> vec4f {
+//
+// `k_in` is the plate's luminance compression and `stride` its kernel's tap
+// spacing in physical px (sigma = 2 taps); both come from the plate's own
+// push block (MODE_PLATE), or are the no-recipe defaults (droplet, raw
+// vertices). A stride of 0 is a CLEAR plate: one clean sample, tinted.
+fn resolve_blur(pos: vec2f, color: vec4f, refract: vec2f, clarity: f32, k_in: f32, stride: f32) -> vec4f {
     let tex_size = vec2f(textureDimensions(t_backdrop));
 
-    var blurred = vec4f(0.0);
-    var total_weight = 0.0;
-
-    // 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) * 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;
-            total_weight += weight;
+    var backdrop_color = vec4f(0.0);
+    if (stride <= 0.0) {
+        backdrop_color = textureSample(t_backdrop, s_backdrop, (pos + refract) / tex_size);
+    } else {
+        var blurred = vec4f(0.0);
+        var total_weight = 0.0;
+        // 7x7 Gaussian kernel at `stride` px (sigma two taps, reach ±3
+        // taps); the linear sampler between taps papers over the stride.
+        // The panel default is 5.5 px; a 2.5 px stride 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) * stride;
+                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;
+                total_weight += weight;
+            }
         }
+        backdrop_color = blurred / total_weight;
     }
 
-    var backdrop_color = blurred / total_weight;
-
     // The rim's clear window onto the backdrop.
     //
     // Refraction has to sample something with STRUCTURE or it is invisible:
@@ -1034,7 +1059,7 @@ fn resolve_blur(pos: vec2f, color: vec4f, refract: vec2f, clarity: f32) -> vec4f
     // Tone-mapping it would pull the refracted view back toward the plate's
     // own key — the exact contrast the rim exists to show — and the effect
     // measured nearly invisible with the two fighting.
-    let k = clamp(window_info.backdrop_meta.x, 0.0, 1.0) * (1.0 - clamp(clarity, 0.0, 1.0));
+    let k = clamp(k_in, 0.0, 1.0) * (1.0 - clamp(clarity, 0.0, 1.0));
     let W = vec3f(0.2126, 0.7152, 0.0722);
     let bl = dot(backdrop_color.rgb, W);
     let key = dot(color.rgb, W);