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

nordisk/facett

.nornir/render-guide.md

The render engine — guide

Class: guide · audience: a techie drawing through, or driving, the facett render kernel. The what is in render-design.md; the why in render-reasoning.md. This file is the workflow.

f0 — zero to a rendered frame (the whole workflow)

Four moves take a batch of primitives to pixels, on whichever backend the host has:

  1. Lower your domain into SDF instances. A node/marker/POI becomes a CircleInstance/RingInstance/MarkerInstance; a road/edge becomes a LineInstance. Everything is in screen pixels.
  2. Probe once, decide once. Build a GpuProbe, call render::decide(probe) → a Backend. The same probe/pick is used for the whole surface.
  3. Draw once against the seam. Hold a Renderer (CpuRenderer or, feature wgpu, GpuSdfHeadless); begin(w,h,camera) → a &mut dyn Canvas; push_quads / push_lines; present() → a Frame (straight RGBA8).
  4. Assert on the frame or scan it. frame.lit_px() for "did it draw"; imgscan::scan(&frame.rgba, w, h, bg) for a structural read.

Then it is automatically testable headless — no window, no GPU required (the CPU lane is always compiled).

f1 — drawing through the seam (CPU lane)

use facett_core::render::{
    CpuRenderer, Renderer, Camera,
    prim::{CircleInstance, RingInstance, LineInstance},
};

let mut r = CpuRenderer::new([12, 14, 20, 255]);      // background RGBA8
let cam = Camera::default();
let canvas = r.begin(256, 256, cam);                   // -> &mut dyn Canvas

canvas.push_quads(&[
    CircleInstance { center: [64.0, 64.0], radius: 20.0, color: [1.0, 0.3, 0.3, 1.0], aa: 1.5 }.lower(),
    RingInstance   { center: [180.0, 90.0], radius: 24.0, inner: 12.0, color: [0.2, 0.9, 1.0, 1.0], aa: 1.5 }.lower(),
]);
canvas.push_lines(&[
    LineInstance::round([20.0, 20.0], [230.0, 200.0], 3.0, 1.5, [0.9, 0.9, 0.2, 0.8]),
]);

let frame = r.present();                               // Frame { width, height, rgba }
assert!(frame.lit_px() > 0, "the CPU lane drew real pixels");
assert_eq!(r.backend(), facett_core::render::Backend::VelloCpu);

Rule of thumb: never touch sdf.wgsl params by hand — go through the typed Circle/Ring/Marker constructors and call .lower(). Lines draw under quads (edges under chips); coverage AA is the aa field (0 = a hard edge).

Coverage math is shared: if you need it on the CPU side directly (a custom rasterizer, a decoration), render::cpu::sdf::quad_coverage / line_coverage are the exact functions sdf.wgsl mirrors.

f2 — the GPU lane (feature wgpu)

Two ways to reach the GPU:

// (a) self-contained headless — its own adapter/device, reads back a Frame.
//     Returns None with no adapter, so a CPU-only box degrades cleanly.
use facett_core::render::gpu::offscreen_render;
if let Some(frame) = offscreen_render(256, 256, [0.05, 0.06, 0.08, 1.0], &quads, &lines) {
    assert!(frame.lit_px() > 0);
}

// (b) the Renderer seam — same draw code as the CPU lane, GPU under it.
use facett_core::render::{gpu::GpuSdfHeadless, Renderer};
let mut r = GpuSdfHeadless::new([0.05, 0.06, 0.08, 1.0]);
let c = r.begin(256, 256, Camera::default());
c.push_quads(&quads);
let frame = r.present();   // GPU if an adapter exists; else the same SDF math on CPU

Inside a live egui app, install the renderer once and record into the host pass:

// startup (eframe creation closure), once:
render::gpu::install_renderer(render_state, |device, fmt| GpuSdfRenderer::new(device, fmt));
// inside an egui_wgpu callback: renderer.record(&mut pass, &bind_group, quad_buf, line_buf);

For a depth-tested 3D skin, render::gpu::OffscreenColorDepth owns the HDR (Rgba16Float) colour + depth target and a blit that samples it into egui's pass scissored to the widget rect — the GPU twin of the CPU clip_poly_to_rect. The post/SOTA modules (GaussianBlur, ColorGrade, MsdfText, GpuParticles) hang off that same HDR target; each ships a CPU mirror of its math (agx_tonemap, gaussian_taps, median3, step_cpu) you can unit-test with no device.

f3 — the headless render seam + parity test

The whole point: assert a component drew without a window. Two seams:

use facett_core::harness;

// (a) a Facet: render offscreen, get a RenderReport.
assert!(harness::headless_render(&mut my_facet).drew());     // 800×600, default theme
let rep = harness::render_sized(&mut my_facet, (1280.0, 720.0));
let rep = harness::render_themed(&mut my_facet, theme);      // themed paint path

RenderReport { title, state, vertices } and RenderReport::drew() (vertices > 0) are the render-report API — see elm-guide.md f3 for the full Elm harness (snapshot / snapshot_json / drive), which this file does not re-derive.

// (b) CPU↔GPU parity — the two lanes must produce matching pixels.
let cpu = { let mut r = CpuRenderer::new(bg8); let c = r.begin(w, h, cam);
            c.push_quads(&quads); c.push_lines(&lines); r.present() };
if let Some(gpu) = render::gpu::offscreen_render(w, h, bgf, &quads, &lines) {
    // frames match because cpu::sdf and sdf.wgsl are the same coverage math.
    assert_eq!(cpu.width, gpu.width);
    // (a real parity test compares within a small per-channel tolerance)
} // no adapter -> skip: L1/GPU is never required.

f4 — scanning the frame (the pixel oracle)

Never assert "non-blank" alone — a smear passes it. Run the analyzer and assert a tight, sensitivity-proven bound:

use facett_core::imgscan::{scan, Rgba};
let bg = Rgba::rgb(12, 14, 20);
let r = scan(&frame.rgba, frame.width, frame.height, bg);
assert!(r.spoke_score < 0.30,   "no radiating smear");   // the green-spoke bug oracle
assert!(r.coverage > 0.02 && r.coverage < 0.45, "sane fill band");
assert!(!r.bbox.is_empty(), "real painted mass");
// centroid near centre, hf_ratio low, etc. — one assertion per structural claim.

ScanReport fields: spoke_score, high_freq_ratio, coverage, centroid, bbox — each backed by a real analyzer (spoke_score, high_freq_ratio, coverage, painted_centroid_and_bbox).

f5 — adding L1 vello beauty (feature l1-vello)

Compose gradient fills / AA curves / glows over an L0 Frame:

use facett_core::render::l1::{L1Overlay, GradientRect, compose_l0_l1};
let overlay = L1Overlay {
    gradient_rects: vec![GradientRect { min: [40.0, 40.0], max: [216.0, 100.0], corner: 12.0,
        color0: [0.10, 0.45, 0.95, 1.0], color1: [0.95, 0.30, 0.55, 1.0] }],
    ..Default::default()
};
let (frame, l1_drawn) = compose_l0_l1(l0_frame, &overlay);
// l1_drawn == false with no GPU: you get the bare L0 frame back. L1 never fails.

Surface l1_drawn in your state_json so a test can assert whether the beauty lane lit up on this host.

Backend & feel knobs