facett-graphview — design note (foundation spike)
A domain-agnostic, scalable 2D graph render engine, vello-backed, that runtime-selects its backend by hardware: vello (GPU/wgpu compute) when a usable GPU exists, vello_cpu (multithreaded SIMD) as the no-GPU fallback. The eventual single home for every graph surface — nornir's arch/dep/release dashboards, korp, graph-DB browsing — aiming to beat Graphviz on interactivity
- scale.
This is task #17's FOUNDATION: it proves the architecture, the runtime backend select, the CPU raster path, the egui-embeddable frame, and carries the benchmark evidence. It deliberately STUBS the heavy engine pieces (LOD, clustering, bundling, streaming layout, adapters) behind documented seams.
What landed (proven)
- Crate scaffold
facett-graphview(workspace member). The model (GraphModel= nodes id/label/pos/fill/stroke + edges from/to/color/dashed/ label, plusDecorations= caller-supplied per-node ring/badge + emphasis edges) mirrors nornir'ssrc/viz/graph_render.rsSHARED API byte-for-byte in shape, so it's a future drop-in for that egui routine.Coloris a tiny POD withFrom<egui::Color32>/Intoboth ways, so a caller already speaking egui maps across with zero glue. Decorations stay caller-supplied — facett never learns what a "release gate" is. - vello scene from a GraphModel: nodes → rounded rects, edges → PCB-style cubics with a mid-break + arrowhead, decoration rings + dashed emphasis edges. See
src/cpu.rs. - Runtime backend select:
Backend { Wgpu, VelloCpu }+decide(GpuProbe)(probe → pick, mirroring nornir's embedder runtime-select).src/backend.rs. - CPU render is real:
cpu::renderrasterizes via vello_cpu to a straight-RGBA8 buffer. A headless test asserts a non-blank, multi-colour frame; therender_pngexample writes a 1280×720 PNG of a sample decorated graph. That RGBA buffer is exactly what a host uploads as an egui texture and shows via a paint callback — PNG here == egui embedding there, minus the window. This is the user's key point proven: vello is NOT GPU-only — the same scene rasterizes on the CPU via vello_cpu. - GPU seam compiles: with
--features gpu,vello0.9 +wgpu29 pull in andgpu::build_scene/gpu::probe_usable_gputype-check against the live vello/wgpu API. See "Versions" — there is no version war with the egui stack.
Versions (the key compatibility result)
| crate | version | note |
| egui/eframe | 0.34 | the facett workspace pin |
| wgpu | 29.0.3 | what egui-wgpu 0.34 pins |
| vello | 0.9.0 | depends on wgpu 29.0.3 — exactly the egui stack's wgpu |
| vello_cpu | 0.0.6 | already a transitive dep of epaint 0.34 (egui's paint) |
| vello_common | 0.0.6 | with vello_cpu |
Two findings made integration painless:
vello_cpu0.0.6 was already in the workspace lock — epaint 0.34 pulls it in. The CPU path was guaranteed to build before a line was written.vello0.9 pins wgpu 29.0.3, the same wgpu egui-wgpu 0.34 uses. The GPU path will coexist with eframe inside a facett host with no version war.
vello_cpu 0.0.6 is young; its API matched: RenderContext::new(u16,u16), set_paint(impl Into<PaintType>), set_stroke(Stroke), fill_path/stroke_path /fill_rect, flush(), render_to_pixmap(&mut Pixmap); Pixmap::take_unpremultiplied() for PNG. kurbo/peniko/color are re-exported.
Benchmark — vello_cpu raster vs epaint tessellation
cargo bench -p facett-graphview (benches/raster.rs), 1600×1000 frame:
| graph | vello_cpu raster | epaint tessellate |
| ~1k edges (480 nodes) | ~23 ms | ~1.5 ms |
| ~10k edges (5k nodes) | ~112 ms | ~11 ms |
Read this honestly. It is NOT apples-to-apples and it does NOT say "vello_cpu beats epaint" on the CPU:
vello_cpudoes the entire software rasterization of a 1.6M-pixel canvas.epaintonly tessellates to meshes — it has not rasterized at all; egui's backend (the GPU, or egui's own software rasterizer) does that afterward.
So on the CPU, full software raster is naturally heavier than mesh-building. The real "is it worth it" win is the GPU path: vello on wgpu rasterizes on the GPU where fill cost is ~free and the compute pipeline scales to scenes that choke epaint's CPU tessellator (which is O(primitives) on the main thread every frame and has no culling/LOD). That is the architecture this spike puts in place. The CPU number is the floor (the no-GPU fallback), and ~112 ms for a 10k-edge full-canvas raster with zero LOD/culling is already a usable floor — the stubs below cut it hard.
GRAPH-CLIP-TEST (landed) — the start→pixel clip discipline
The graph twin of facett-map3d's clip pipeline now ships in src/clip.rs (render_graph_clipped + the gpu lane render_graph_clipped_gpu + pick). It renders a GraphModel into a widget rect ⊆ canvas, instruments the chain layout → cull → nodes → edges → picking → composite, and proves nothing is painted outside the rect (composite.ink_outside_rect == 0) with a final-picture PNG blob a test reads back + a deliberately-unclipped bug variant that goes RED (sensitivity). Full note: .nornir/graph-clip-test.md. Tests: tests/graph_clip.rs (CPU + gpu).
What is STUBBED (documented, not built)
Each has a clear seam + TODO in code:
- LOD / viewport-culling — only draw what's in the camera rect, and drop to cheaper primitives (dots, not chips; lines, not cubics) when zoomed out. The single biggest CPU-path win;
cpu::rendercurrently draws every node/edge. Seam: filtermodel.nodes/edgesbyCamera-projected bounds before build. - Clustering / aggregation — collapse dense subgraphs into super-nodes off a caller-supplied or computed grouping; expand on zoom. Seam: a pre-pass that rewrites
GraphModel→ a coarserGraphModelkeyed by an LOD level. - Edge-bundling — route many parallel edges through shared splines to cut visual + raster cost. Seam: replace per-edge cubics in
cpu::draw_edge. - Streaming layout — incremental force/layered layout that updates positions across frames without a full relayout (the caller owns layout today; the engine will offer an optional layout service). Seam: a
layoutmodule feedingGraphNode::pos. - GPU texture readback / egui paint callback —
gpu::build_scenebuilds the vello scene; the live path adds avello::Rendererrendering to a wgpu texture, shown via anegui_wgpupaint callback (or read back to RGBA for headless). Versions already align (vello 0.9 ↔ wgpu 29 ↔ egui-wgpu 0.34). - Text glyphs / badges — vello_cpu
glyph_runneeds a parsed font; node labels + decoration badges are not painted yet (the spike proves geometry). Seam: aglyph_runpass incpu::renderafter the chip fill. -
Data-source adapters (#22) — turn a domain source into a
GraphModel:- nornir dep graph (the MCP
dep_graph_svg/deps_ofdata → nodes/edges), - FalkorDB / graph-DB result sets,
- the pipeline DAG. These live OUTSIDE the engine (domain-agnostic) — a
From<DomainThing> for GraphModelper source, in the consuming crate, not here.
- nornir dep graph (the MCP
Path from this spike to the full engine (#17) + adapters (#22)
- Land viewport-culling + LOD in
cpu.rs(biggest floor win; cheap). - Wire the live GPU path:
gpu::GpuRenderer(device/queue/vello renderer/ target) + readback, then theegui_wgpupaint callback for in-app embedding. Add a realgpu::probe_usable_gpu-drivendecideat host start-up. - Add text glyphs (labels + badges) — shared font handle.
- Add the optional layout service + clustering/bundling as the engine grows toward Graphviz-scale interactivity.
- Write the adapters (#22) in the consuming crates (nornir dep/arch/release, korp, FalkorDB, pipeline) as
-> GraphModelconverters; swap nornir'sdraw_graphpainter forfacett_graphview::render_to_rgbabehind its egui UI.