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

commitacbec30968e5a00a098c9942d5ddf61218d91028
parentccdaaa240a
authorLucas Galante <[email protected]>
date2026-08-30 21:50
feat: debug builds warn when a near-roll carve loses grouping

The grouped->overlay flip is silent, and the one place it is visually
significant is a full-ring untinted carve whose shaded region reaches an
enclosing plate's perimeter roll: grouped, that junction is arithmetic in
the plate's single height field; fallen back, it is the host-box fade.
Debug builds now report that case loudly (once per geometry, no env var)
whenever a still-open plate encloses the carve and a dynamic rule --
occlusion, feature-run contiguity, budget -- rejected it.

Deliberately a warning, not an assert: the CCE_PLATE_DEBUG audit
established every rejection is conservative-correct (the render is right;
the frame-to-frame LOOK is what flips), so a panic would crash debug
builds on correct behavior. The ubiquitous accepted case -- ordinary
geometry already closed every grouping window -- stays quiet by
construction, since no open enclosing plate remains to check against.
Classification is a pure helper (near_roll_fallback_reason), unit-tested.

Also corrects the RFC design note and a stale demo comment: what groups
is full-ring carves -- the flush bands never group (80d50de), so their
roll junction is always the fade. The note's first version had that
inverted.

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

 docs/rfc-core-rebuild.md     |  38 ++++++++---
 src/backend/window_runner.rs | 157 +++++++++++++++++++++++++++++++++++++++++++
 src/main.rs                  |   6 +-
 3 files changed, 188 insertions(+), 13 deletions(-)

diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 5f51cc9..1b57497 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -2622,14 +2622,22 @@ Constraint respected: **each crate still builds standalone** — the new core is
   The corollary that makes the design coherent: **the two paths differ visibly only
   near the host's rolled perimeter.** An interior carve (a TreeList or TextBox well
   in the middle of a window plate) never touches the roll, so the fallback is
-  effectively exact there. That is why the measured grouping rates — the demo groups
-  2 of 10 carves, cce-files 0 of 7 — are correct spending, not waste: the carves that
-  DO group (menubar/status bands sinking through the window's rounded-corner region,
-  edge to edge) are precisely the ones the approximation would visibly regress. So
-  neither path is retirable: fallback-only regresses perimeter-spanning carves, and
-  grouping-always is impossible because ordinary geometry painted after a plate
-  correctly closes its feature run (the carve's shading is baked into the plate's
-  earlier draw).
+  effectively exact there. Two scope corrections recorded 2026-08-30 (the first
+  version of this note got them wrong): what groups is **full-ring untinted carves**
+  (button grooves, slider wells) — the flush menubar/status bands never group, by
+  design since cce-ui@80d50de: an edge-suppressed carve's wall rect extends past the
+  boundary, relying on the overlay cover quad to clip it, a clip the grouped
+  whole-plate draw does not have (grouped, the extended walls smeared across the
+  plate). So the junction where a flush band meets the plate's roll is ALWAYS the
+  host-box fade; grouping's value is the single-evaluation lighting of full-ring
+  carves, exact wherever one sits near the roll. And per the audit that shipped
+  `CCE_PLATE_DEBUG` (cce-ui@949e35e, three apps): **no misgrouping — every fallback
+  is a documented rule firing correctly**; grouping is rare (demo 2 of 10, cce-files
+  0 of 7) because apps constantly interleave flat fills with reliefs, and each one
+  correctly closes the grouping window (the carve's shading is baked into the
+  plate's earlier draw). Neither path is retirable: grouping-always is impossible
+  for exactly that reason, and fallback-only would forfeit the exact junctions
+  full-ring carves get when they do group.
 
   The standing hazard is the *silent flip*: three of the six grouping conditions are
   dynamic (draw order, sibling plates, whether another plate claimed the host's
@@ -2639,9 +2647,17 @@ Constraint respected: **each crate still builds standalone** — the new core is
   carve-host tracker a stack (`plate_stack`, cce-ui@9cfadad). `CCE_PLATE_DEBUG=1`
   reports each carve's verdict, the fallback reason, and which prim closed a
   grouping window; it is the first tool for any "same widget, different look"
-  report. A future hardening candidate: assert (debug builds) that a
-  perimeter-touching carve actually grouped, turning the silent flip into a loud
-  one.
+  report. Hardening (landed 2026-08-30): debug builds warn loudly — once per
+  geometry, no env var — when a groupable full-ring carve is enclosed by a
+  still-open plate, its shaded region reaches that plate's roll band, and a dynamic
+  rule (occlusion, feature-run contiguity, budget) rejected it: the one class where
+  the flip is visually significant (`near_roll_fallback_reason` in
+  `backend/window_runner.rs`, unit-tested). Deliberately a warning, NOT an assert:
+  the audit established every rejection is conservative-correct — the render is
+  right, it is the frame-to-frame look that flips — so a panic would crash debug
+  builds on correct behavior. The ubiquitous accepted case (ordinary geometry
+  already closed every grouping window) stays quiet by construction: no open
+  enclosing plate remains for the check to run against.
 
 Order rationale: each phase is independently valuable and reversible, and no phase requires the
 next to compile. Phase 0 can land immediately regardless of the rest.
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b81104b..37db2c0 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1687,6 +1687,71 @@ fn plate_debug() -> bool {
     *ON.get_or_init(|| std::env::var("CCE_PLATE_DEBUG").is_ok_and(|v| v != "0"))
 }
 
+/// Debug builds make one kind of fallback LOUD without `CCE_PLATE_DEBUG`: a
+/// carve that could group (full ring, untinted) failing to while a still-open
+/// plate encloses it and the carve's shaded region reaches that plate's
+/// perimeter roll. There the grouped and overlay paths shade the junction
+/// differently, and the rejection is one of the dynamic rules — so the SAME
+/// widget can flip looks frame to frame with nothing on stderr. Not an
+/// assert/panic: every rejection is conservative-CORRECT (the audit that
+/// shipped CCE_PLATE_DEBUG found no misgrouping; a later plate overlapping the
+/// carve genuinely must be shaded over, not under) — it is the frame-to-frame
+/// LOOK that flips, so the right loudness is an unmissable warning, not a
+/// crash. The ubiquitous quiet case stays quiet by construction: ordinary
+/// geometry closing every grouping window empties `plate_stack`, so no
+/// enclosing OPEN plate exists and this never runs — that is draw-order
+/// design, not a flip.
+///
+/// Returns the dynamic rule to report, or `None` when the fallback is not the
+/// loud case. Pure so the classification is unit-testable; `later_plates` are
+/// the open plates emitted after the enclosing host.
+#[cfg(debug_assertions)]
+fn near_roll_fallback_reason(
+    carve: &crate::scene::layout::Rect,
+    depth: f32,
+    host: &crate::scene::layout::Rect,
+    roll: f32,
+    later_plates: &[crate::scene::layout::Rect],
+    budget_full: bool,
+) -> Option<&'static str> {
+    // The carve's shaded region — the overlay path's cover-quad inflation.
+    let infl = depth * 0.5 + 2.0;
+    let (sx0, sy0) = (carve.x - infl, carve.y - infl);
+    let (sx1, sy1) = (carve.x + carve.width + infl, carve.y + carve.height + infl);
+    // "Near the roll" = the shaded region leaves the host rect deflated by the
+    // host's own roll width on any side.
+    let near = sx0 < host.x + roll
+        || sy0 < host.y + roll
+        || sx1 > host.x + host.width - roll
+        || sy1 > host.y + host.height - roll;
+    if !near {
+        return None;
+    }
+    // The dynamic rules, in the order the grouping guard tests them.
+    if budget_full {
+        return Some("the feature budget is full");
+    }
+    if later_plates
+        .iter()
+        .any(|o| sx0 < o.x + o.width && sx1 > o.x && sy0 < o.y + o.height && sy1 > o.y)
+    {
+        return Some("a later plate overlaps the carve's shaded region");
+    }
+    Some("the host's feature run is closed (another plate appended features since)")
+}
+
+/// Print a near-roll fallback warning once per distinct message — a carve in a
+/// steady layout would otherwise repeat it every frame.
+#[cfg(debug_assertions)]
+fn plate_carve_warn_once(msg: String) {
+    use std::sync::{Mutex, OnceLock};
+    static SEEN: OnceLock<Mutex<std::collections::HashSet<String>>> = OnceLock::new();
+    let seen = SEEN.get_or_init(|| Mutex::new(std::collections::HashSet::new()));
+    if seen.lock().unwrap().insert(msg.clone()) {
+        eprintln!("{msg}");
+    }
+}
+
 pub fn tessellate_display_list(
     dl: &crate::scene::paint::DisplayList,
     sw: f32,
@@ -1918,6 +1983,37 @@ pub fn tessellate_display_list(
                 } else {
                     None
                 };
+                // Debug-build loudness for the silent grouped→overlay flip —
+                // see `near_roll_fallback_reason` on what qualifies and why
+                // this warns instead of panicking.
+                #[cfg(debug_assertions)]
+                if host_plate.is_none() && mode < 3.5 && full_ring && tint.is_none() {
+                    let enclosing = plate_stack.iter().enumerate().rev().find(|(_, (_, p))| {
+                        rect.x >= p.x - 0.5
+                            && rect.y >= p.y - 0.5
+                            && rect.x + rect.width <= p.x + p.width + 0.5
+                            && rect.y + rect.height <= p.y + p.height + 0.5
+                    });
+                    if let Some((si, &(bi, prect))) = enclosing {
+                        // Host roll width rides the push's light.w (physical px).
+                        let roll = batches[bi].plate.as_ref().map_or(0.0, |p| p.light[3]) / scale;
+                        let later: Vec<crate::scene::layout::Rect> =
+                            plate_stack[si + 1..].iter().map(|&(_, r)| r).collect();
+                        let budget_full = features.len() >= crate::vk::MAX_PLATE_FEATURES;
+                        if let Some(why) =
+                            near_roll_fallback_reason(rect, *depth, &prect, roll, &later, budget_full)
+                        {
+                            let kind = if mode > 2.5 { "boss" } else { "recess" };
+                            plate_carve_warn_once(format!(
+                                "plate-carve: near-roll {kind} ({:.0},{:.0} {:.0}x{:.0}) lost grouping — {why}; \
+                                 its junction with the host plate's roll shades through the overlay fallback, \
+                                 visually different from grouped frames (CCE_PLATE_DEBUG=1 traces verdicts) \
+                                 [debug-build warning, printed once]",
+                                rect.x, rect.y, rect.width, rect.height
+                            ));
+                        }
+                    }
+                }
                 if dbg_plates {
                     match host_plate {
                         Some(_) => dbg_grouped += 1,
@@ -5588,3 +5684,64 @@ fn run_session<'l, A: Application>(
     drop(engine_state);
     (app, end)
 }
+
+#[cfg(test)]
+mod near_roll_fallback_tests {
+    use super::near_roll_fallback_reason;
+    use crate::scene::layout::Rect;
+
+    fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
+        Rect { x, y, width: w, height: h }
+    }
+
+    const HOST: Rect = Rect { x: 0.0, y: 0.0, width: 800.0, height: 600.0 };
+    const ROLL: f32 = 8.0;
+
+    #[test]
+    fn interior_carve_is_quiet() {
+        // Well inside the deflated host: the overlay fallback is exact there.
+        let carve = r(100.0, 100.0, 200.0, 100.0);
+        assert_eq!(near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[], false), None);
+    }
+
+    #[test]
+    fn shaded_region_reaching_the_roll_is_loud() {
+        // Carve rect stops 3px short of the roll band, but its shaded region
+        // (depth*0.5 + 2 = 5px) crosses in — the inflation must count.
+        let carve = r(ROLL + 3.0, 100.0, 200.0, 100.0);
+        assert_eq!(
+            near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[], false),
+            Some("the host's feature run is closed (another plate appended features since)")
+        );
+    }
+
+    #[test]
+    fn occlusion_is_named_before_run_contiguity() {
+        let carve = r(2.0, 100.0, 200.0, 100.0);
+        let occluder = r(150.0, 150.0, 100.0, 100.0);
+        assert_eq!(
+            near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[occluder], false),
+            Some("a later plate overlaps the carve's shaded region")
+        );
+    }
+
+    #[test]
+    fn non_overlapping_later_plate_is_not_occlusion() {
+        let carve = r(2.0, 100.0, 200.0, 100.0);
+        let elsewhere = r(500.0, 400.0, 100.0, 100.0);
+        assert_eq!(
+            near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[elsewhere], false),
+            Some("the host's feature run is closed (another plate appended features since)")
+        );
+    }
+
+    #[test]
+    fn budget_wins_over_every_other_reason() {
+        let carve = r(2.0, 100.0, 200.0, 100.0);
+        let occluder = r(150.0, 150.0, 100.0, 100.0);
+        assert_eq!(
+            near_roll_fallback_reason(&carve, 6.0, &HOST, ROLL, &[occluder], true),
+            Some("the feature budget is full")
+        );
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 00b09c0..3392827 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -391,8 +391,10 @@ impl Application for DemoApp {
 
         // Status band: the header's mirror — carved into the bottom of the
         // plate, flush to the window's bottom and sides, its only wall the top
-        // one facing the content. Emitted here, before any widget geometry, so
-        // it CSG-groups into the plate like the header band does. Sized from
+        // one facing the content. (Neither band CSG-groups: edge-suppressed
+        // carves never do — their extended walls would smear across the
+        // plate's whole-surface draw. Both shade through the overlay fallback,
+        // whose host-box fade owns the junction with the roll.) Sized from
         // the status font plus a symmetric pad (the layout's status leaf only
         // reserves the space; the band and its text center independently).
         let status_h = text_leaf_height(STATUS_FONT_SIZE) + 2.0 * STATUS_BAND_PAD;