facett-graphview/src/lib.rs
//! **facett-graphview** — 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.
//!
//! # The shared API (a future drop-in for nornir's `draw_graph`)
//! The model — [`GraphModel`] (nodes id/label/pos/fill/stroke + edges
//! from/to/color/dashed/label) and the caller-supplied [`Decorations`] overlay
//! (per-node ring/badge + emphasis edges) — mirrors nornir's
//! `src/viz/graph_render.rs` SHARED API exactly, so that egui routine can swap
//! its painter for this engine without changing its callers. Decorations stay
//! caller-supplied and domain-agnostic: facett never learns what a "release
//! gate" is.
//!
//! # Backends (vello is NOT GPU-only)
//! [`Backend`] {`Wgpu`, `VelloCpu`}; [`decide`] turns a [`GpuProbe`] into a
//! pick (probe → choose, mirroring nornir's embedder runtime-select). The CPU
//! path ([`cpu::render`]) is the proven reference — `vello_cpu` already ships
//! transitively inside epaint 0.34, so it always builds here. The GPU path is a
//! real, version-aligned seam (`vello` 0.9 ↔ wgpu 29 ↔ egui-wgpu 0.34), wired
//! behind the `gpu` cargo feature; see [`gpu`].
//!
//! # Render entry point
//! [`render_to_rgba`] dispatches on the chosen [`Backend`] and returns straight
//! RGBA8 pixels (PNG / egui-texture ready). The `GpuVello` arm submits the shared
//! scene to a live [`vello::Renderer`] on a wgpu device and reads the texture back to
//! RGBA (see [`gpu::GpuRenderer`]); with no usable adapter — or the `gpu` feature off —
//! it falls back cleanly to the [`cpu::render`] reference. Both lanes build their
//! geometry from the SAME [`scene::build`], so they paint the same picture.
//!
//! # What is STUBBED (documented, not built — see `.nornir/design.md`)
//! LOD / viewport-culling, clustering / aggregation, edge-bundling, streaming
//! layout, and the data-source **adapters** (nornir dep graph / FalkorDB /
//! pipeline). Clear TODOs live at each seam.
/// The pure-algorithm **graph analysis** toolbox (centrality / community /
/// pathfinding / cycles / components / k-core / similarity) — the compute core of the
/// graph-DB IDE. Index-based `(n, edges)`; no egui, no GPU.
pub mod analysis;
pub mod backend;
/// The shared CPU 3D camera + placement math (`project_azimuth`, `fib_sphere`) —
/// pure functions, UNGATED, so CPU-side hosts (dwarves' warehouse-nav / funnel 3D)
/// import the one implementation instead of porting it (LAW 5).
pub mod camera3d;
pub mod clip;
pub mod community;
pub mod cpu;
pub mod decorated_view;
pub mod depgraph_layout;
/// The **FalkorDB browser** view-model — a Cypher query → nodes/edges → laid-out
/// [`GraphModel`] state machine, egui-free, with a pluggable [`falkor::CypherRunner`]
/// transport seam (redis/RESP lives at the consumer). See [`falkor::FalkorBrowser`].
pub mod falkor;
pub mod fdeb;
#[cfg(feature = "gpu")]
pub mod fdeb_gpu;
pub mod generate;
pub mod gpu_layout;
pub mod l0;
/// **Legibility at scale + the shared instanced GPU cloud lane** — the spatial
/// [`legible::PickIndex`] (picking that costs a neighbourhood, not the dataset) and the
/// opt-in GPU cloud painter. Both consume `facett-core`'s shared primitives; neither
/// changes a default path. See [`legible`].
pub mod legible;
/// The RESIDENT GPU cloud lane of [`DecoratedGraphView`] at scale (feature `gpu-cloud`).
#[cfg(feature = "gpu-cloud")]
pub mod resident;
pub mod metro;
pub mod model;
/// The **shared, backend-neutral scene builder** — the single geometry source both the
/// CPU ([`cpu`]) and GPU ([`gpu`]) rasterizer lanes paint from, so they draw pixel-for-
/// pixel the same chips/edges/rings (TODO(#17): "do not fork geometry"). See [`scene`].
pub mod scene;
// The WORLD GRAPH: repos→crates→functions LOD + the scale-aware layout (Barnes–Hut
// O(n log n) below the force budget, O(n) hierarchical placement above it) that feeds
// the instanced GPU cloud past the O(n²) 100k wall. See [`world`].
pub mod world;
#[cfg(feature = "gpu")]
pub mod gpu;
/// The world-graph STILL — a converged galaxy layout rendered once, headless, on the GPU
/// into a single emissive image (the 100k+ "mega tier"). Reuses the shared instanced
/// cloud renderer. Behind feature `gpu`.
#[cfg(feature = "gpu")]
pub mod world_still;
pub use backend::{Backend, GpuProbe, decide, probe_from_env};
pub use clip::{GraphClipReport, WidgetRect, pick, render_graph_clipped};
pub use cpu::{Rendered, render as render_cpu};
pub use decorated_view::{DecoratedGraphView, downstream_of};
pub use depgraph_layout::{DepEdge, DepGraphLayout, EdgeClass, LaidEdge, LaidNode, LayoutOpts};
// The FalkorDB browser (graph-DB IDE data half): a Cypher query lowers to a laid-out
// GraphModel a host paints through DecoratedGraphView. The transport is the pluggable
// `CypherRunner` seam so this crate stays pure-Rust + headless-testable.
pub use falkor::{
BrowserStatus, CannedRunner, CypherEdge, CypherNode, CypherResult, CypherRunner, FalkorBrowser,
FalkorEndpoint,
};
// The elite GPU-compute layout engine (#1): CSR streamed from Arrow → force-directed
// positions that iterate in VRAM (feature `gpu`), with the deterministic CPU fallback
// always built. See [`gpu_layout`] + `.nornir/elite-graph-engine.md`.
pub use gpu_layout::{relax_cpu, step_cpu, GraphCsr, LayoutParams, LayoutState};
// Force-directed edge bundling (#2): the hairball cure — compatible edges attract
// into shared "information highways". See [`fdeb`].
pub use fdeb::{bundle, occupancy, BundleParams, BundledEdge};
// Topological semantic zoom (#3): Louvain communities collapse into meta-nodes when
// zoomed out and fracture open (injected-clock easing) when zoomed in. See [`community`].
pub use community::{
fracture_t, fractured_positions, louvain, meta_graph, visible_count, Communities, MetaGraph, MetaNode,
};
// CONS-CORE Phase C: the opt-in L0 kernel adoption (routes nodes/edges through the
// shared `facett_core::render` Canvas). The default render path
// ([`render_to_rgba`]) is UNCHANGED — this is the alternative L0 lane.
pub use l0::{render_to_rgba_l0, lower as lower_to_l0};
pub use legible::PickIndex;
// The WORLD GRAPH surface: LOD (repos→crates→functions) + the scale-aware layout that
// breaks the 100k O(n²) wall (Barnes–Hut to ~1M, O(n) hierarchical placement to 10M).
pub use world::{
hier_placement, layout_world, CrateSpec, LayoutBudget, LayoutLane, Lod, NodeLevel, RepoSpec,
WorldGraph,
};
pub use metro::{MetroBranch, MetroLine, MetroMap, MetroStation, MetroView, StationKind};
pub use model::{
BOX_H, BOX_W, Camera, Color, Decorations, GraphEdge, GraphModel, GraphNode, NodeDecoration, Pos,
};
/// Render `model` (decorated, under `camera`) to a `w × h` straight-RGBA8 frame,
/// dispatching on the runtime-chosen `backend`. The one call a host makes after
/// [`decide`].
///
/// With the `gpu` feature ON, the `GpuVello` arm submits the shared scene to a live
/// `vello::Renderer` on a wgpu device ([`gpu::render_gpu`]) and reads the rendered
/// texture back to straight RGBA8 — the same pixel contract the CPU path returns, so a
/// host swaps backends without touching its blit. If no usable adapter exists (headless
/// CI, this box), `render_gpu` returns `None` and the arm falls back cleanly to
/// [`cpu::render`]. Without the `gpu` feature the arm is compile-time the CPU path.
/// Both lanes build their geometry from the SAME [`scene::build`], so they paint the
/// same picture (only the rasterizer's own anti-aliasing differs).
pub fn render_to_rgba(
backend: Backend,
model: &GraphModel,
decorations: &Decorations,
camera: &Camera,
width: u32,
height: u32,
background: Color,
) -> Rendered {
// ── render-lane dispatch emit: record WHICH backend arm this call took ─────
// The recurring smear hid in an uninstrumented branch; emit the chosen arm so
// the matrix proves both the Wgpu and VelloCpu dispatch arms get exercised.
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-graphview::render_to_rgba",
"render_dispatch",
true,
&format!("backend={backend:?} nodes={} edges={}", model.nodes.len(), model.edges.len()),
);
match backend {
Backend::Wgpu => {
#[cfg(feature = "gpu")]
{
// Submit the shared scene to the live vello GPU pipeline (vello 0.9 on
// the `wgpu29` alias — vello has no wgpu-30 release yet) and read it
// back to RGBA. `None` = no usable adapter → fall through to the CPU
// reference so the pixel contract holds on any box.
if let Some(frame) =
gpu::render_gpu(model, decorations, camera, width, height, background)
{
return frame;
}
}
cpu::render(model, decorations, camera, width, height, background)
}
Backend::VelloCpu => cpu::render(model, decorations, camera, width, height, background),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_graph() -> GraphModel {
let nodes = vec![
GraphNode {
id: "a".into(),
label: "alpha".into(),
fill: Color::rgb(60, 90, 160),
stroke: Color::WHITE,
pos: Pos::new(0.0, 0.0),
},
GraphNode {
id: "b".into(),
label: "beta".into(),
fill: Color::rgb(60, 140, 90),
stroke: Color::WHITE,
pos: Pos::new(300.0, -80.0),
},
GraphNode {
id: "c".into(),
label: "gamma".into(),
fill: Color::rgb(160, 90, 60),
stroke: Color::WHITE,
pos: Pos::new(300.0, 80.0),
},
];
let edges = vec![
GraphEdge { from: "a".into(), to: "b".into(), color: Color::rgb(200, 200, 220), dashed: false, label: None },
GraphEdge { from: "a".into(), to: "c".into(), color: Color::rgb(200, 200, 220), dashed: true, label: None },
];
GraphModel { nodes, edges }
}
#[test]
fn cpu_render_produces_nonblank_frame() {
let model = sample_graph();
let cam = Camera::fit(&model, 640.0, 480.0, 40.0);
let backend = decide(GpuProbe::cpu_only());
assert_eq!(backend, Backend::VelloCpu);
let frame = render_to_rgba(
backend,
&model,
&Decorations::default(),
&cam,
640,
480,
Color::rgb(18, 20, 28),
);
assert_eq!(frame.rgba.len(), 640 * 480 * 4);
// Non-blank: more than one distinct colour drew (chips + edges over bg).
let mut seen = std::collections::HashSet::new();
for px in frame.rgba.as_chunks::<4>().0 {
seen.insert([px[0], px[1], px[2]]);
if seen.len() > 3 {
break;
}
}
assert!(seen.len() > 3, "vello_cpu drew a real graph, not a flat pane");
}
/// The `Wgpu` dispatch arm must ALWAYS return a well-formed, non-blank frame —
/// whether it renders live on a GPU (`--features gpu` + a real adapter) or falls back
/// to the CPU reference (no adapter, or the `gpu` feature off). This is the honest
/// wiring + clean-fallback contract: the pixel shape a host relies on never changes.
#[test]
fn gpu_dispatch_frame_is_valid_or_cpu_fallback() {
// THE GPU TURN: the GpuVello arm brings up the cached vello device on first use.
let _gpu = facett_core::render::gputurn::hold();
let model = sample_graph();
let (w, h) = (256u32, 192u32);
let cam = Camera::fit(&model, w as f32, h as f32, 20.0);
let frame = render_to_rgba(
Backend::Wgpu,
&model,
&Decorations::default(),
&cam,
w,
h,
Color::rgb(18, 20, 28),
);
assert_eq!(frame.rgba.len(), (w * h * 4) as usize, "full frame from the Wgpu arm");
let mut seen = std::collections::HashSet::new();
for px in frame.rgba.as_chunks::<4>().0 {
seen.insert([px[0], px[1], px[2]]);
if seen.len() > 3 {
break;
}
}
assert!(seen.len() > 3, "the Wgpu arm drew a real graph (GPU or CPU fallback)");
}
#[test]
fn camera_fit_centers_world() {
let model = sample_graph();
let cam = Camera::fit(&model, 640.0, 480.0, 40.0);
// The world-bounds centre should land near the viewport centre.
let (min_x, min_y, max_x, max_y) = model.world_bounds().unwrap();
let (wcx, wcy) = ((min_x + max_x) * 0.5, (min_y + max_y) * 0.5);
let (sx, sy) = cam.project(Pos::new(wcx, wcy));
assert!((sx - 320.0).abs() < 1.0, "x centered, got {sx}");
assert!((sy - 240.0).abs() < 1.0, "y centered, got {sy}");
}
}