GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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);