RenderBackend — the swappable 3D render-backend seam (LANDED)
Implements the
.nornir/render-engine.mddesign's "Renderer/Canvas backend-seam trait = the 2-category swap expressed as one trait" (galileo pattern, our own impl). Branchagent/map3d-render-trait. The trait abstracts the 3D draw of theMap3Dscene so a different Rust engine drops in without changing the map3d / map3d-flight view code. The trait is the point.
The contract (facett_map3d::backend)
RenderScene<'a> backend-neutral frame: &Mesh3 + &WaterMesh + OrbitCamera + SunSky
+ Viewport(rect,center,half_h) + bg + SceneFlags + clock_s
SceneFlags sky / ssao / soft_shadows / shadow_strength / water_on / planar_reflection
RenderStats visible_triangles · lit_fraction · ink_outside_rect(==0 oracle)
· raw_overshoot_px · depth_monotone · max_extent_px · backend · used_gpu
BackendCaps name · gpu · shadows · reflective_water · post_fx
trait RenderBackend {
fn caps(&self) -> BackendCaps;
fn analyze(&self, &RenderScene) -> RenderStats; // PURE, device-free — the oracle
fn paint(&self, &RenderScene, &mut egui::Ui) -> RenderStats; // default = CPU painter
fn reset_cache(&self) {} // drop device cache on mesh swap
}
analyze is the conformance surface: every backend, handed the same scene, must report the same geometry oracles. It is device-free, so the GPU backend's analyze runs headless alongside the CPU/null ones.
The impls (all behind the one trait)
| backend | role | draws |
CpuPainterBackend |
impl #1a — always available, headless, the correctness reference | project+cull+depth-sort+light → egui::Mesh |
NullBackend |
headless / conformance — identical oracle, draws nothing | — |
WgpuVelloBackend (feat wgpu) |
impl #1b, our stack | all 3D on wgpu (shadow→reflection→PBR mesh→water→blit); vello = 2D HUD only |
How the swap works
Map3D holds backend: Box<dyn RenderBackend> (default CpuPainterBackend). with_gpu(true) swaps in WgpuVelloBackend. Every frame Map3D::view builds a RenderScene via build_scene() and calls self.backend.paint(&scene, ui) — the view code never mentions wgpu or vello. A future engine implements RenderBackend and drops in with a one-line self.backend = Box::new(NewEngine::new()).
korp connector seam (facett_map3d::korp)
FeatureLayers { buildings, streets, landuse, water } + load_layers(&[Way]) -> FeatureLayers is the single wiring point. Today it classifies OSM ways by tag (the OSM pipeline is the fallback source); TODO(korp) swaps its body for korp::connect(region) once korp/.nornir exists — the extruder, the GroundLayer z-ordering, and the render backend do NOT change.
Street / feature layering (depth-correct, no z-fight)
extrude::GroundLayer { Cover, Road } assigns each flat feature a disjoint, ordered sub-band of GROUND_LIFT: terrain(0) < Cover(landuse) < Road(streets), all sub-pixel. A road crossing a park is deterministically above the park fill — no z-fight — and buildings rise far above both.
Bench (benches/heavy_render.rs, cargo bench -p facett-map3d)
Per-part-step + rollup over a large synthetic OSM tile (MAP3D_BENCH_TOWN=n, default 64). 48×48 tile = 2402 ways / 36 870 tris: extrude 1.40 ms (1.72 Melem/s), classify 112 µs, shade 9.54 ms (3.87 Mtri/s), scissor 758 µs, backend_analyze 10.26 ms.
Tests
tests/backend_conformance.rs— Cpu≡Null≡Wgpu(analyze) on the same scene; scene invariants (ink_outside_rect==0, monotone, bounded extent); camera-sweep parity; caps; device paint#[ignore](compiles under--features wgpu).tests/street_layering.rs— band disjoint+ordered, deterministic lift, road-above- cover no-zfight, buildings clear the ground band, depth-sorted projection.korp.rsunit tests — layer classification + flatten draw-order +into_map3d.