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