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

nordisk/facett

.nornir/render-design.md

The render engine — design

Class: design · Function axis: f0 (the 2-category L0 kernel) + f1 vocabulary + seam · f2 CPU lane · f3 GPU lane · f4 backend select + shared seams · f5 L1 vello overlay · f6 egui-painter look layer + pixel oracle. Strict: this file states only the shape we run. Every why, every alternative, every road-not-taken lives in render-reasoning.md.

f0 — the L0 render kernel (the whole)

facett-core::render is one domain-agnostic render kernel that draws a small vocabulary of signed-distance primitives two ways behind one seam. A map point, a graph node, a POI, and a waypoint are all a QuadInstance; a road, an edge, and a leader line are all a LineInstance — the kernel knows nothing of maps or graphs (facett-core/src/render/mod.rs:1, module doc).

The frame flows through four fixed contracts:

  1. One primitive vocabulary. A skin lowers its domain into QuadInstance/LineInstance, both bytemuck-able POD in screen pixels (render/prim.rs:1).
  2. One coverage math, two evaluators. cpu::sdf is the source of truth; sdf.wgsl/line.wgsl mirror it byte-for-byte, so a CPU frame and a GPU frame match (render/cpu/sdf.rs:3).
  3. One frame type. Both lanes emit a Frame — straight (un-premultiplied) RGBA8, row-major. Frame::lit_px() is the "did it actually draw" oracle.
  4. One backend swap. decide(GpuProbe) returns a Backend; the host draws once against the Canvas/Renderer seam and the concrete renderer decides how instances become pixels.

Two hardware categories exist, nothing between them:

category renderer reach
CPU (Backend::VelloCpu) CpuRenderer → vello_cpu pixmap, gatling scanlines always compiled; device / headless / wasm / no-GPU
CPU + GPU (Backend::Wgpu) GpuSdfRenderer → instanced wgpu feature wgpu; large scenes, planet-scale

On top of L0 sits the optional L1 vello overlay (feature l1-vello, f5): gradient fills, antialiased Bézier curves, and gaussian glow the SDF base skips, alpha-composited over the L0 Frame. L0 stands alone with L1 off.

Reuse map (what the render subsystem uses): the always-on lane depends only on vello_cpu (raster target) + znippy-zoomies (the gatling scanline pool); the wgpu feature adds wgpu/egui-wgpu/bytemuck/pollster; l1-vello adds vello. The default build carries zero GPU dependency edge. It is used by every map/graph skin (facett-map, facett-map3d, facett-graphview) and, through the harness render seam, by nornir's render matrix.

f1 — the SDF vocabulary + the draw seam

The instance model (render/prim.rs). Everything is a flat #[repr(C)] POD in screen pixels:

type fields lowers to
QuadInstance center, radius, inner, color, aa, shape (48 B) itself — the one quad the pipeline draws
CircleInstance disc QuadInstance shape=CIRCLE
RingInstance annulus (inner<radius, clamped) shape=RING
MarkerInstance square / triangle / diamond shape=SQUARE|TRIANGLE|DIAMOND
LineInstance capsule a→b, half_width, cap (48 B) itself

shape is a u32 discriminant (prim::shape) — GPU-uploadable verbatim, matching the SHAPE_* constants in sdf.wgsl. aa is the half-width of the anti-alias band; coverage ramps 1→0 across [edge−aa, edge+aa]. QuadInstance::half_extent() and LineInstance::bounds() inflate by radius+aa / half_width+aa so the AA fringe is never clipped.

The coverage math (render/cpu/sdf.rs) is the single source of truth both lanes funnel through. Every shape returns coverage α ∈ [0,1] via one distance→coverage kernel coverage_from_sd(d, aa) built on smoothstep (identical to WGSL's). Per-shape: circle_coverage, ring_coverage, square_coverage, diamond_coverage, triangle_coverage; dispatched per pixel by quad_coverage and, for lines, line_coverage (a BUTT cap additionally clips the rounded overshoot flat).

The seam traits (render/mod.rs). A skin writes its draw code once:

Both CpuRenderer and (feature wgpu) GpuSdfHeadless satisfy the seam.

f2 — the CPU lane (Backend::VelloCpu)

The always-on L0 fallback (render/cpu/mod.rs). CpuCanvas collects quad/line batches and rasterizes onto a vello_cpu Pixmap, producing a straight-RGBA8 Frame byte-for-byte matching the GPU lane. CpuRenderer wraps it as the seam's Renderer: begin opens a CpuCanvas sized to the rect, present (:451) rasterizes and returns the Frame.

GATLING multicore raster (raster_batches). The raster fans across all cores by scanline row through znippy_zoomies::gatling_forkjoin::gatling_scanlines — each row owned by exactly one worker, so blends never contend and the per-pixel composite order (all lines in push order under all quads in push order) is bit-identical to the sequential path. rayon is not used. Two scaling lifts live inside the parallel region:

Below PARALLEL_PX_THRESHOLD (64 K px) the frame stays single-threaded. Per-pixel compositing is source-over on premultiplied pixels (blend_px); the pixmap→RGBA8 un-premultiply (frame) runs through the same gatling kernel into an un-zeroed buffer. Lines draw under quads (edges under chips).

The CPU rect scissor (render/cpu/scissor.rs) is the geometry-level clip — the "ink never escapes the rect" guarantee. clip_poly_to_rect is Sutherland–Hodgman clip of a triangle to an axis-aligned rect; ink_outside_rect is the scissor oracle, counting clipped vertices still outside the rect (a correct scissor leaves 0). See the clip pipeline below.

Under feature testmatrix, CpuRenderer::present emits a cpu_render matrix row (render/cpu/mod.rs:465): frame dims correct, lit_px>0 on a non-empty surface, rgba.len() == w·h·4.

f3 — the GPU lane (Backend::Wgpu, feature wgpu)

The L0 GPU SDF kernel (render/gpu/sdf_pipeline.rs). GpuSdfRenderer builds two pipelines from a &wgpu::Device + target TextureFormat: a quad pipeline (instanced screen-aligned quads, fragment-shader SDF via SDF_WGSL) and a line pipeline (thick AA capsules via LINE_WGSL), sharing one 16-byte viewport uniform (SdfUniforms) and premultiplied source-over blend. Two drive paths:

GpuSdfHeadless wraps offscreen_render as a Renderer; its present renders headlessly, and on None falls back to the same cpu::sdf math through a CpuCanvas — the seam never panics. Under testmatrix it emits gpu_render (adapter found, render/gpu/sdf_pipeline.rs:436) or gpu_render_cpu_fallback (no adapter, :465).

Shared GPU scaffold (render/gpu/mod.rs): install_renderer is the install-once lifecycle over an egui_wgpu::RenderState's callback_resources; types holds the bytemuck Vertex/GpuWayMeta/ DrawUniforms/DrawIndirectArgs + color32_to_f32; buffer is the upload-once + chunking partition (partition_chunks / chunk_budget) that keeps each geometry buffer ≤ max_buffer_size, splitting a country's geometry on way boundaries so the per-way cull shader is unchanged. CULL_WGSL/DRAW_WGSL are byte-identical to facett-map's cull/draw shaders.

The HDR offscreen target (render/gpu/offscreen.rs). OffscreenColorDepth owns an offscreen colour (OFFSCREEN_FORMAT = Rgba16Float, HDR) + depth (DEPTH_FORMAT = Depth32Float) target and a blit pipeline that samples the colour into egui's colour-only pass, scissored to the widget rect. The GPU rect-clip math (gpu_scissor_px / scissor_intersection / ScissorPx) is the GPU twin of the CPU painter's clip_poly_to_rect.

Composable post / SOTA modules hang off the HDR target (all feature wgpu):

module type draws
blur GaussianBlur separable 9-tap Gaussian (gaussian_taps) on a ping-pong pair
colorgrade ColorGrade exposure → CAS → AgX tonemap (agx_tonemap) → vignette → grain → dither
msdf MsdfText crisp glyph quads from an MSDF atlas (median3/coverage/screen_px_range)
particles GpuParticles compute boids (step_cpu is the CPU parity ref) drawn additively into the HDR target

Each SOTA module mirrors its curve/step math on the CPU so it is unit-tested deterministically with no GPU, and carries an on-device readback proof that self-skips without an adapter.

f4 — backend selection + the shared seams

The picker (render/backend.rs). A host fills a GpuProbe (usable_gpu, force_cpu; built via gpu() / cpu_only() / from_env which reads FACETT_RENDER_CPU). decide(probe) returns Wgpu only when the GPU is usable, not forced off, and gpu_arm_compiled() (cfg!(feature = "wgpu")) is true — otherwise VelloCpu. So a default build stays GPU-dep-free while the seam is real; a probe claiming a GPU still resolves to CPU when the feature is off. Backend::label gives the status-line string.

The shared camera (render/camera.rs). Camera is the superset of the two skin cameras: a 2D pan/zoom half (project2d, mirroring graphview's p*zoom+pan affine) and a 3D orbit half (eye/basis/ view_space/project_view/ view_proj, reproducing map3d's OrbitCamera math bit-for-bit; NEAR_PLANE = 0.02). InputFeel carries the per-OS FEEL (orbit/pan/dolly sensitivity + damping) with macos/windows presets.

The graphs-over-maps seam (render/layer.rs). LayerStack is a z-ordered set of Layers (LayerKind: Map2d/Map3d/Graph/Overlay); ordered yields them back→front by ascending z with insertion order breaking ties, so a graph layer composites over a map layer deterministically.

f5 — the L1 vello beauty overlay (feature l1-vello)

The optional GPU lane (render/l1/mod.rs) that draws the beauty primitives the L0 SDF base deliberately skips. An L1Overlay is a declarative recipe in screen pixels — GradientRects (linear-gradient rounded rects), Curves (AA cubic Béziers), Glows (gaussian-blurred rounded rects). to_scene encodes it into a vello::Scene; render_overlay renders it headless on a throwaway wgpu device to a straight-RGBA8 Frame (None with no adapter); composite_over source-over composites it onto an L0 Frame. compose_l0_l1(base, overlay) returns (Frame, l1_drawn) — the same final frame carries both lanes, and l1_drawn=false with the bare L0 frame when no GPU exists. L1 degrades, never fails.

f6 — the egui-painter look layer + the pixel oracle

Beyond the L0 SDF kernel, facett-core carries a family of pure-egui painter render helpers (the "glow backend": layered-alpha shapes on an egui::Painter, no custom shader, so they light up identically native and on wasm) plus a CPU decoration kernel and a pixel-analysis oracle.

The cyber-toon decoration kernel (render/decor.rs) — domain-agnostic, pure-CPU, L0-only (emits the same prim instances both lanes draw), all animation injected-clock-driven so snapshots stay deterministic: glow_halo, ao_shadow, node_decor, dash_line (marching dash-array), particles_on_edge, shockwave_ring, pulse/ strobe envelopes, gradient_by_scalar, and bloom_composite — a separable CPU box-blur bloom over a Frame.

Graph edges (edges.rs) — draw_edge paints EdgeStyle::Straight/Spline/Soft with widths/glow from the theme; spline_points samples the cubic Bézier. Under EffectsPolicy::None (Device) Soft collapses to a crisp hairline, no glow.

3D label stabilization (labels3d.rs) — billboard_placement draws a projected label upright and jitter-free via smooth_anchor + stable_snap (sub-pixel grid), so labels never rotate with the scene or bob.

Motion + bloom (effects.rs) — Tween/Curve injected-clock tweens, glow_rect/glow_text/ shimmer, the RevealHighlight Fluent hover cue, FadeTrack per-element hover fades, and the signature deterministic sprites RavenSprite, ParticleBurst, IceDrip. Smooth scroll (scroll_engine.rs) — SmoothScroll, sub-pixel momentum under an injected clock. Deck effects (deckfx.rs) — DeckFx wires palette-switch + glow

The pixel oracle (imgscan.rs) — the SCAN-THE-PIXELS analyzer a render test runs on the received frame: scan returns a ScanReport with spoke_score (thin radiating smear signature), high_freq_ratio, coverage, and painted_centroid_and_bbox — a render test asserts a viewport-tight, sensitivity-proven invariant on the pixels, not "non-blank".

The headless render seam every consumer drives is harness::headless_render / render_sized / render_themed, returning a RenderReport whose drew() asserts it painted — see elm-guide.md f3 for driving it.


The GLOW-STROKE LADDER (render::decor, 2026-09-03)

decor gained the primitive a luminous line is made of, because three consumers were each about to invent it: korp's golden case trail, the population graph's liquid money edges, and any future tracer.

pub struct GlowPass { pub width_mul: f32, pub alpha: f32, pub white: f32 }
pub const GLOW_LADDER: [GlowPass; 5];
pub enum GlowGround { Dark, Light }
pub fn glow_stroke_passes_on(core_width, ink, ground) -> Vec<(f32, [f32; 4])>;
pub fn glow_stroke_passes(core_width, ink)            // = _on(.., Dark)
pub fn glow_core_coverage() -> f32;                   // the composited centre alpha
pub fn glow_reach_mul() -> f32;                       // the halo's width, in cores
pub fn glow_flow_phase(t, period_s) -> f32;           // pure, for the tracers

Everything is a MULTIPLIER or a fraction, never a pixel count, so the same ladder serves a 1.5 px graph edge and a 7 m map ribbon. Pure and deterministic — no painter, no clock — so the CPU lane, the GPU lane and the wasm demo lower the same five numbers.

Why five passes, and why these numbers

The three-pass ladder korp shipped — (9.0, α46) (4.5, α120) (1.6, α255), all at the pure hue — did not read as a glow, and each reason is measurable:

defect measurement
the halo did not reach 5.6× the core. The ladder reaches 14×.
the core kept the hue a saturated cyan at α 1.0 is a cyan line. Light is white at the centre and coloured at the edge; the two inner passes now carry white 0.55 / 0.90.
the alphas ramped linearly stacked translucent strokes composite multiplicatively, so a linear ramp piles the ink in the middle. The ladder is geometric: 0.09 → 0.16 → 0.30 → 0.62 → 1.00.

Guarded by decor::the_old_three_pass_ladder_is_not_a_glow, which builds the old ladder and fails every gate the new one passes.

The ground is a required input, not a style option

A luminous line is a CONTRAST effect. MEASURED with the same ladder on both papers (korp's tests/golden_trail_pixels.rs): over a dark pane the white-hot core reads as emission; over the cartographic DAY paper #F2EFE9 the identical ladder composites to (232, 250, 253) — a near-white line on near-white paper, invisible — and the faint halo desaturates to grey.

GlowGround::Light therefore inverts the tone: every pass is deepened toward black (base 0.30, the core a further 0.30) and every alpha is gained 2.2×. Same widths, same reach, same ordering — the halo becomes a coloured contact shadow and the core a saturated filament. The highlighter look, which is how a bright line reads luminous against a bright ground.

The painter projection

facett_graph3d::neon::paint_glow_trail(painter, pts, color, core_w, phase) lowers the ladder onto the egui painter, and optionally rides decor::particles_on_edge along the path so the light flows. Every pass is stroked over the WHOLE path before the next, narrower one — a per-segment loop lays a core over its neighbour's halo and the line reads as beads.

Guarded on pixels (neon::the_glow_trail_lays_a_wide_halo_and_a_hot_core): a plain Shape::line at the same width gives a 2 px cross-section, the ladder 28, and the ladder's peak brightness is 739 against the plain stroke's 497.

Where it is visible

facett-demo ▸ 🧬 population graph: the liquid money edges are drawn with the ladder (pop_beauty::render_scene) instead of one LineInstance — a wide faint bleed under a white-hot core, on the pop pane's dark ground.