facett-demo/src/velo_l1_tab.rs
//! **The `Velo L1` tab** — the showcase that makes the **L1 vello beauty lane**
//! visible. It draws the **SAME scene rendered two ways, side by side**:
//!
//! - **left = L0 SDF only** — the facett-core instanced SDF base (node markers +
//! capsule edges) on the `CpuRenderer` (always available, zero vello L1).
//! - **right = L0 + L1 vello overlay** — the identical L0 frame with the L1 vello
//! beauty lane (gradient sweep, antialiased Bézier curve, gaussian glow)
//! composited on top — the curves / gradients / effects the SDF base
//! deliberately skips (`render-engine.md` §"L0 / L1").
//!
//! The right lane only lights up when the demo is built with the `l1-vello`
//! feature **and** a GPU adapter exists (vello is the fat-GPU member of the velo
//! family; desktop only; never required). Otherwise the right pane shows the L0
//! frame alone and `state_json` reports `l1_available = false` — an honest,
//! observable degrade, not a fake.
//!
//! ## 2-category auto-select, surfaced as data
//! The tab exposes the runtime backend decision (`facett_core::render::decide`)
//! in `state_json` (`backend`, `gpu_probe`), so the **CPU / CPU+GPU** pick the
//! whole render engine hinges on is observable + robot-assertable (LAW: CLI /
//! headless parity, components expose what they render as data).
use facett::Facet;
use facett::core::FacetCaps;
use facett_core::render::{
Backend, Camera, CircleInstance, CpuRenderer, Frame, GpuProbe, LineInstance, MarkerInstance,
QuadInstance, Renderer, RingInstance, decide, prim::shape,
};
/// The stable tab title (deck / discovery-contract key + legend key).
pub const TITLE: &str = "Velo L1";
/// Number of node markers in the synthetic scene (so a test can assert the L0
/// instance count is real, not a toy).
pub const NODES: usize = 7;
/// The `Velo L1` tab — a canonical [`Facet`] rendering the L0 / (L0+L1) side by
/// side through the shared `facett_core::render` kernel + the L1 vello overlay.
pub struct VeloL1Tab {
title: &'static str,
/// The L0 SDF scene (lowered node-marker quads + edge lines), built once.
quads: Vec<QuadInstance>,
lines: Vec<LineInstance>,
/// The runtime backend the 2-category policy picked (CPU here in the demo /
/// headless test; the GPU L0 lane shares the same lowered instances).
backend: Backend,
/// The probe the policy decided from (surfaced as data).
probe: GpuProbe,
/// Cached textures for the two lanes (re-rendered on resize).
tex_l0: Option<egui::TextureHandle>,
tex_l1: Option<egui::TextureHandle>,
last_size: (u32, u32),
/// Last lit-pixel counts — the non-blank oracle for each lane.
lit_l0: usize,
lit_l1: usize,
/// Whether the L1 vello lane actually ran (feature on AND a GPU was found).
l1_available: bool,
/// Pixel count where the L1 lane differs from L0 (the proof L1 added beauty).
l1_diff_px: usize,
}
impl VeloL1Tab {
/// Build the tab over a small synthetic SDF scene (a ring of node markers
/// wired by capsule edges) — enough L0 content that the L1 overlay registers
/// clearly over it.
pub fn new(title: &'static str) -> Self {
let (quads, lines) = synthetic_scene(WIDTH, HEIGHT);
let probe = GpuProbe::cpu_only();
let backend = decide(probe);
Self {
title,
quads,
lines,
backend,
probe,
tex_l0: None,
tex_l1: None,
last_size: (0, 0),
lit_l0: 0,
lit_l1: 0,
l1_available: false,
l1_diff_px: 0,
}
}
/// The L0-only frame: rasterize the scene on the shared `CpuRenderer`.
pub fn render_l0(&self, w: u32, h: u32) -> Frame {
let mut r = CpuRenderer::new(BG);
let canvas = r.begin(w, h, Camera::default());
canvas.push_lines(&self.lines);
canvas.push_quads(&self.quads);
r.present()
}
/// The L0 + L1 frame: the L0 frame with the L1 vello overlay composited over
/// it. Returns `(frame, l1_ran)` — `l1_ran` is `false` when the `l1-vello`
/// feature is off or no GPU exists (then the L0 frame is returned unchanged).
pub fn render_l0_l1(&self, w: u32, h: u32) -> (Frame, bool) {
let base = self.render_l0(w, h);
#[cfg(feature = "l1-vello")]
{
let overlay = beauty_overlay(w, h);
facett_core::render::l1::compose_l0_l1(base, &overlay)
}
#[cfg(not(feature = "l1-vello"))]
{
(base, false)
}
}
/// Render both lanes into cached textures + record the oracles. The single
/// render path shared by `ui` and the headless test.
fn render_into_textures(&mut self, ctx: &egui::Context, w: u32, h: u32) {
let f0 = self.render_l0(w, h);
self.lit_l0 = f0.lit_px();
let (f1, ran) = self.render_l0_l1(w, h);
self.lit_l1 = f1.lit_px();
self.l1_available = ran;
self.l1_diff_px = frame_diff_px(&f0, &f1);
let img0 = egui::ColorImage::from_rgba_unmultiplied([w as usize, h as usize], &f0.rgba);
let img1 = egui::ColorImage::from_rgba_unmultiplied([w as usize, h as usize], &f1.rgba);
self.tex_l0 = Some(ctx.load_texture(format!("{}-l0", self.title), img0, egui::TextureOptions::LINEAR));
self.tex_l1 = Some(ctx.load_texture(format!("{}-l1", self.title), img1, egui::TextureOptions::LINEAR));
self.last_size = (w, h);
}
pub fn node_count(&self) -> usize {
NODES
}
}
impl Facet for VeloL1Tab {
fn title(&self) -> &str {
self.title
}
fn ui(&mut self, ui: &mut egui::Ui) {
if self.tex_l0.is_none() || self.last_size != (WIDTH, HEIGHT) {
let ctx = ui.ctx().clone();
self.render_into_textures(&ctx, WIDTH, HEIGHT);
}
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label("L0 — SDF base (no vello)");
if let Some(tex) = &self.tex_l0 {
ui.image((tex.id(), egui::vec2(WIDTH as f32, HEIGHT as f32)));
}
});
ui.separator();
ui.vertical(|ui| {
let lbl = if self.l1_available {
"L0 + L1 — vello overlay (gradient · curve · glow)"
} else {
"L0 + L1 — vello off (feature off / no GPU) → L0 shown"
};
ui.label(lbl);
if let Some(tex) = &self.tex_l1 {
ui.image((tex.id(), egui::vec2(WIDTH as f32, HEIGHT as f32)));
}
});
});
ui.label(format!(
"backend: {} · L0 lit {} px · L0+L1 lit {} px · L1 differs in {} px",
self.backend.label(),
self.lit_l0,
self.lit_l1,
self.l1_diff_px,
));
// ── render-lane emit: this Facet::ui path RAN; both lanes lit pixels and
// (when L1 is available) the L1 lane really added beauty over L0. ────────
#[cfg(feature = "testmatrix")]
{
facett::core::testmatrix::emit(
"facett-demo::VeloL1Tab::ui",
"ui_render",
self.lit_l0 > 0 && self.lit_l1 > 0 && (!self.l1_available || self.l1_diff_px > 0),
&format!(
"backend={:?} l1_available={} lit_l0={} lit_l1={} l1_diff_px={}",
self.backend, self.l1_available, self.lit_l0, self.lit_l1, self.l1_diff_px,
),
);
}
}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({
"component": "VeloL1Tab",
"render_path": "facett_core::render L0 SDF (CpuRenderer) + L1 vello overlay (render::l1::compose_l0_l1)",
// The 2-category auto-select decision, surfaced as DATA (the CPU /
// CPU+GPU pick the whole engine hinges on).
"backend": self.backend.label(),
"gpu_probe": {
"usable_gpu": self.probe.usable_gpu,
"force_cpu": self.probe.force_cpu,
},
"category": if self.backend == Backend::Wgpu { "CPU+GPU" } else { "CPU" },
"nodes": self.node_count(),
"l0_quads": self.quads.len(),
"l0_lines": self.lines.len(),
// Whether the demo was BUILT with the l1-vello feature compiled in.
"l1_feature": cfg!(feature = "l1-vello"),
// Whether the L1 lane actually RAN this frame (feature on + a GPU).
"l1_available": self.l1_available,
// The non-blank oracles (EMIT-DOCTRINE: the can't-see-from-outside
// "did each lane draw" signal, as data). 0 until first render.
"lit_l0": self.lit_l0,
"lit_l1": self.lit_l1,
// The proof the L1 overlay added beauty the L0 base lacks.
"l1_diff_px": self.l1_diff_px,
"navigable": false,
})
}
fn caps(&self) -> FacetCaps {
FacetCaps::NONE.themeable().resizable()
}
}
/// Off-screen render dimensions for the two lanes (each pane).
pub const WIDTH: u32 = 420;
pub const HEIGHT: u32 = 320;
/// The dark scene background (straight RGBA8).
const BG: [u8; 4] = [16, 18, 26, 255];
/// Build the synthetic L0 SDF scene: `NODES` node markers laid out on a ring and
/// wired by capsule edges + a couple of decorative rings — real instanced content
/// the L1 overlay registers over. Returns `(quads, lines)` in screen pixels.
pub fn synthetic_scene(w: u32, h: u32) -> (Vec<QuadInstance>, Vec<LineInstance>) {
let (cx, cy) = (w as f32 * 0.5, h as f32 * 0.5);
let radius = (w.min(h) as f32) * 0.32;
let palette = [
[0.27, 0.43, 0.75, 1.0],
[0.31, 0.67, 0.47, 1.0],
[0.78, 0.63, 0.27, 1.0],
[0.75, 0.35, 0.47, 1.0],
[0.55, 0.43, 0.78, 1.0],
[0.30, 0.70, 0.72, 1.0],
[0.85, 0.50, 0.30, 1.0],
];
let mut pos = Vec::with_capacity(NODES);
for i in 0..NODES {
let a = (i as f32 / NODES as f32) * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2;
pos.push([cx + radius * a.cos(), cy + radius * a.sin()]);
}
let mut quads = Vec::new();
let mut lines = Vec::new();
// Edges: a ring + a few chords, drawn under the node markers.
let edge_col = [0.66, 0.70, 0.80, 0.78];
for i in 0..NODES {
let j = (i + 1) % NODES;
lines.push(LineInstance::round(pos[i], pos[j], 2.5, 1.5, edge_col));
}
for i in 0..NODES {
let j = (i + 3) % NODES;
lines.push(LineInstance::round(pos[i], pos[j], 1.8, 1.2, [0.5, 0.55, 0.7, 0.5]));
}
// Node markers (rounded squares) + a soft ring halo per node.
for i in 0..NODES {
let col = palette[i % palette.len()];
quads.push(
RingInstance { center: pos[i], radius: 22.0, inner: 17.0, color: [col[0], col[1], col[2], 0.45], aa: 1.5 }
.lower(),
);
quads.push(
MarkerInstance { center: pos[i], radius: 14.0, corner: 4.0, color: col, aa: 1.5, shape: shape::SQUARE }
.lower(),
);
}
// A centre hub circle.
quads.push(CircleInstance { center: [cx, cy], radius: 10.0, color: [0.9, 0.9, 0.95, 1.0], aa: 1.5 }.lower());
(quads, lines)
}
/// Build the L1 beauty overlay for the scene (screen px): a gradient banner across
/// the top, a sweeping antialiased Bézier curve, and a soft glow behind the hub —
/// exactly the gradient / curve / effect classes the L0 SDF base deliberately
/// skips. Built only when the `l1-vello` feature is compiled in.
#[cfg(feature = "l1-vello")]
pub fn beauty_overlay(w: u32, h: u32) -> facett_core::render::l1::L1Overlay {
use facett_core::render::l1::{Curve, GradientRect, Glow, L1Overlay};
let (cx, cy) = (w as f32 * 0.5, h as f32 * 0.5);
L1Overlay {
gradient_rects: vec![GradientRect {
min: [w as f32 * 0.08, h as f32 * 0.06],
max: [w as f32 * 0.92, h as f32 * 0.16],
corner: 8.0,
color0: [0.12, 0.45, 0.95, 0.85],
color1: [0.95, 0.30, 0.55, 0.85],
}],
curves: vec![Curve {
p0: [w as f32 * 0.10, h as f32 * 0.85],
c0: [w as f32 * 0.30, h as f32 * 0.35],
c1: [w as f32 * 0.70, h as f32 * 1.05],
p1: [w as f32 * 0.90, h as f32 * 0.55],
width: 5.0,
color: [0.98, 0.86, 0.22, 0.95],
}],
glows: vec![Glow {
min: [cx - 26.0, cy - 26.0],
max: [cx + 26.0, cy + 26.0],
corner: 14.0,
color: [0.35, 0.95, 0.75, 0.9],
std_dev: 9.0,
}],
}
}
/// Count pixels whose RGBA differs between two same-size frames — the proof the L1
/// overlay changed the picture (added beauty the L0 base lacked). `0` when the two
/// frames are identical (e.g. L1 unavailable → the same L0 frame both sides).
pub fn frame_diff_px(a: &Frame, b: &Frame) -> usize {
if a.width != b.width || a.height != b.height {
return 0;
}
a.rgba
.as_chunks::<4>().0.iter()
.zip(b.rgba.as_chunks::<4>().0)
.filter(|(pa, pb)| pa != pb)
.count()
}
#[cfg(test)]
mod tests {
use super::*;
/// INJECT-ASSERT: the synthetic L0 scene is real instanced content (a marker +
/// halo ring per node + a hub, wired by edges), and the L0-only lane lights a
/// substantial, contained frame on the shared CpuRenderer — not "didn't panic".
#[test]
fn l0_lane_renders_real_instanced_content() {
let tab = VeloL1Tab::new(TITLE);
// 2 quads/node (ring halo + marker) + 1 hub.
assert_eq!(tab.quads.len(), NODES * 2 + 1, "marker + halo per node + hub");
assert!(tab.lines.len() >= NODES, "at least the ring of edges");
let f0 = tab.render_l0(WIDTH, HEIGHT);
assert_eq!(f0.rgba.len(), (WIDTH * HEIGHT * 4) as usize);
assert!(f0.lit_px() > 5_000, "L0 lit real pixels, got {}", f0.lit_px());
// Containment: the corner pixel is the untouched background.
assert_eq!(&f0.rgba[0..4], &BG, "L0 corner is background (drew inside the rect)");
}
/// INJECT-ASSERT: the 2-category backend decision is real + observable. With a
/// cpu_only probe the policy picks CPU; state_json carries the category + probe.
#[test]
fn backend_auto_select_is_observable_in_state() {
let tab = VeloL1Tab::new(TITLE);
assert_eq!(tab.backend, Backend::VelloCpu, "cpu_only probe → CPU lane");
let s = tab.state_json();
assert_eq!(s["category"], "CPU");
assert_eq!(s["gpu_probe"]["usable_gpu"], false);
assert_eq!(s["nodes"].as_u64(), Some(NODES as u64));
assert_eq!(s["l0_quads"].as_u64(), Some((NODES * 2 + 1) as u64));
}
}