Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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));
    }
}