The render engine — guide
Class:
guide· audience: a techie drawing through, or driving, the facett render kernel. The what is inrender-design.md; the why inrender-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:
- Lower your domain into SDF instances. A node/marker/POI becomes a
CircleInstance/RingInstance/MarkerInstance; a road/edge becomes aLineInstance. Everything is in screen pixels. - Probe once, decide once. Build a
GpuProbe, callrender::decide(probe)→ aBackend. The same probe/pick is used for the whole surface. - Draw once against the seam. Hold a
Renderer(CpuRendereror, featurewgpu,GpuSdfHeadless);begin(w,h,camera)→ a&mut dyn Canvas;push_quads/push_lines;present()→ aFrame(straight RGBA8). - 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
- Force CPU for a deterministic golden: set
FACETT_RENDER_CPU=1(read byGpuProbe::from_env), or passGpuProbe { force_cpu: true, .. }todecide. Cameradrives both 2D (project2d) and 3D (project_view/view_proj); pick the per-OS feel withInputFeel::macos()/windows().- Draw a graph over a map by pushing
LayerStacklayers at ascending z and painting inordered()order — the composition seam, no retrofit.