facett-demo/src/graphview_tab.rs
//! **The `graphview` tab** — a real graph tab driven by **facett-graphview**, the
//! consolidated graph render ENGINE, rendering through the shared
//! `facett_core::render` **L0 Canvas** (the `render_to_rgba_l0` path).
//!
//! Where the existing `graph` tab is a `facett-graph` `Scene` (the circular
//! layout skin) and `depgraph` is the layered-DAG skin, THIS tab exercises the
//! *render kernel* directly: it lowers a [`GraphModel`] into the L0 SDF instance
//! vocabulary (rounded-square node markers + straight-capsule edges) and
//! rasterizes it with the core [`CpuRenderer`](facett_core::render) — the exact
//! same kernel the map skins draw through (see `.nornir/render-engine.md` §"ONE
//! render kernel, TWO domain skins"). The frame is uploaded as an egui texture.
//!
//! ## Why a wrapper here (not in `facett-graphview`)
//! `facett-graphview` is a render engine: it produces an RGBA frame, it is **not**
//! an egui `Facet` widget (its model mirrors nornir's `draw_graph` SHARED API so
//! nornir/korp can swap their painter for it). The demo owns the thin egui-`Facet`
//! adapter so the leaf engine crate stays UI-framework-free — the same split the
//! OSM tabs use (`osm_tabs` owns the data load; the viewer crates stay lean).
//!
//! ## At scale: the RESIDENT GPU cloud
//! The L0 kernel rasterises on the CPU on every camera move — fine for a mesh, a
//! wall at 100 000 nodes. Above `cloud::RESIDENT_ABOVE` (5 000) nodes, with the shared
//! instanced cloud renderer installed, the tab paints the SAME model (fills, world
//! positions, the same chip radius and edge width the L0 lowering uses) on the
//! resident lane instead: points + edge index pairs uploaded ONCE per model
//! (`cloud::ResidentSlot`), each frame the 48-byte camera alone. With no renderer the
//! L0 CPU lane draws as before and `state_json` says `no GPU`.
//!
//! ## Navigable
//! Drag pans, scroll zooms (the shared pan/zoom contract — `caps().navigable`).
//! The live camera + the lowered instance counts + a lit-pixel proof ride in
//! `state_json`, so the robot matrix drives it and asserts a real, non-blank
//! render (EMIT-DOCTRINE: the frame's `lit_px` is the can't-see-from-outside
//! "did it actually draw" oracle, asserted as data).
use facett::Facet;
use facett::core::FacetCaps;
use facett_graphview::{
Backend, Camera, Color, Decorations, GpuProbe, GraphEdge, GraphModel, GraphNode, Pos, decide,
render_to_rgba_l0,
};
/// The stable tab title (the deck/discovery-contract key + the legend key).
pub const TITLE: &str = "graphview";
/// A `graphview`-engine-backed graph tab — a canonical [`Facet`] that renders a
/// synthetic layered service-mesh graph through the consolidated L0 kernel.
pub struct GraphviewTab {
title: &'static str,
model: GraphModel,
/// The runtime-chosen backend (CPU here; the GPU L0 lane shares the same
/// lowered instances and is opt-in behind the engine's `gpu` feature).
backend: Backend,
/// The live 2D pan/zoom camera (the navigable contract). Re-fit on first draw
/// to the actual widget rect; user drag/scroll then drives it.
camera: Camera,
/// Whether the camera has been fit to a real widget rect yet.
fitted: bool,
/// The cached uploaded frame texture (re-rendered when the camera/rect moves).
tex: Option<egui::TextureHandle>,
/// The last (w, h) the frame was rendered at — re-render on rect change.
last_size: (u32, u32),
/// The last frame's lit-pixel count — the non-blank render oracle.
lit_px: usize,
/// Which lane drew the last frame: `"l0-cpu"`, `"resident"`, or `""` (not drawn).
lane: &'static str,
/// The resident lane's upload bookkeeping (one model per generation).
#[cfg(any(feature = "gpu", feature = "graph-wgpu"))]
resident: facett::core::render::gpu::graphcloud::cloud::ResidentSlot,
/// This model's resident generation (process-unique; the model never changes in
/// place — a new graph is a new tab value).
#[cfg(any(feature = "gpu", feature = "graph-wgpu"))]
generation: u64,
}
impl GraphviewTab {
/// Build the tab over a synthetic **layered service-mesh** graph: `LAYERS`
/// columns of `PER_LAYER` nodes, fully forward-connected layer→layer, so the
/// engine draws a real fan-in/fan-out mesh (not a 3-node toy). The node fill
/// is a per-layer hue so the columns read as bands.
pub fn new(title: &'static str) -> Self {
Self::from_model(title, synthetic_mesh())
}
/// The tab over a caller's `model` (the size + generator control's graph).
pub fn from_model(title: &'static str, model: GraphModel) -> Self {
let backend = decide(GpuProbe::cpu_only());
// A provisional fit (re-fit on the first real draw to the widget rect).
let camera = Camera::fit(&model, 900.0, 560.0, 48.0);
Self {
title,
model,
backend,
camera,
fitted: false,
tex: None,
last_size: (0, 0),
lit_px: 0,
lane: "",
#[cfg(any(feature = "gpu", feature = "graph-wgpu"))]
resident: Default::default(),
#[cfg(any(feature = "gpu", feature = "graph-wgpu"))]
generation: facett::core::render::gpu::graphcloud::cloud::fresh_generation(),
}
}
/// Does this model take the resident lane when a renderer is installed?
#[must_use]
pub fn at_scale(&self) -> bool {
self.model.nodes.len() > RESIDENT_ABOVE
}
/// Which lane drew the last frame (`"l0-cpu"` / `"resident"` / `""`).
#[must_use]
pub fn lane(&self) -> &'static str {
self.lane
}
/// Models the resident lane has uploaded (`0` on the L0 lane).
#[must_use]
pub fn resident_uploads(&self) -> u64 {
#[cfg(any(feature = "gpu", feature = "graph-wgpu"))]
{
self.resident.uploads()
}
#[cfg(not(any(feature = "gpu", feature = "graph-wgpu")))]
{
0
}
}
/// Is the shared cloud renderer installed in this process?
fn renderer_installed() -> bool {
crate::universe_tab::renderer_installed()
}
/// The camera `(zoom, pan_x, pan_y)`.
#[must_use]
pub fn camera(&self) -> (f32, f32, f32) {
(self.camera.zoom, self.camera.pan_x, self.camera.pan_y)
}
/// Pan the camera by `(dx, dy)` px — what a drag does.
pub fn pan_by(&mut self, dx: f32, dy: f32) {
self.camera.pan_x += dx;
self.camera.pan_y += dy;
}
/// The lane readout at scale (no per-node data): the summary a `GenTab` publishes.
#[must_use]
pub fn summary_json(&self) -> serde_json::Value {
serde_json::json!({
"engine": "facett-graphview",
"nodes": self.node_count(),
"edges": self.edge_count(),
"navigable": true,
"zoom": self.camera.zoom,
"pan": [self.camera.pan_x, self.camera.pan_y],
"lane": self.lane,
"gpu": if Self::renderer_installed() { "gpu" } else { "no GPU" },
"resident_uploads": self.resident_uploads(),
"lit_px": self.lit_px,
})
}
/// **The resident frame**: the camera every frame, the model only when this slot's
/// generation is not the resident one. `false` when the lane cannot draw (no
/// renderer, a zero-pixel rect, or a build without the GPU cloud).
#[cfg(any(feature = "gpu", feature = "graph-wgpu"))]
fn paint_resident(&self, ui: &egui::Ui, rect: egui::Rect) -> bool {
use facett::core::render::gpu::graphcloud::{CloudCamera, CloudPoint};
use facett_graphview::model::BOX_H;
let ppp = ui.ctx().pixels_per_point();
let zoom = self.camera.zoom;
// The L0 projection is `p · zoom + pan` from the frame's top-left.
let cam = CloudCamera::plane(
[rect.width() * ppp, rect.height() * ppp],
[self.camera.pan_x * ppp, self.camera.pan_y * ppp],
zoom * ppp,
(BOX_H * 0.5 * zoom).max(2.0) * ppp,
(1.4 * zoom).clamp(0.7, 3.0) * 0.5 * ppp,
2.5,
);
let model = &self.model;
let build = || {
let idx: std::collections::HashMap<&str, u32> =
model.nodes.iter().enumerate().map(|(i, n)| (n.id.as_str(), i as u32)).collect();
let pts = model
.nodes
.iter()
.map(|n| {
let c = n.fill;
CloudPoint::new([n.pos.x, -n.pos.y, 0.0], egui::Color32::from_rgba_unmultiplied(c.r, c.g, c.b, c.a))
})
.collect();
let eds = model
.edges
.iter()
.filter_map(|e| Some([*idx.get(e.from.as_str())?, *idx.get(e.to.as_str())?]))
.collect();
(pts, eds)
};
self.resident.draw(ui, rect, self.generation, build, cam)
}
#[cfg(not(any(feature = "gpu", feature = "graph-wgpu")))]
fn paint_resident(&self, _ui: &egui::Ui, _rect: egui::Rect) -> bool {
false
}
/// The number of nodes / edges in the driven graph (for tests + state).
pub fn node_count(&self) -> usize {
self.model.nodes.len()
}
pub fn edge_count(&self) -> usize {
self.model.edges.len()
}
/// Render the current model+camera to an RGBA frame through the **consolidated
/// L0 kernel** (`render_to_rgba_l0`) and upload it as an egui texture. Records
/// the lowered instance counts + lit-pixel proof. The single render path shared
/// by `ui` and the headless test.
fn render_into_texture(&mut self, ctx: &egui::Context, w: u32, h: u32) {
let bg = Color::rgb(16, 18, 26);
// The L0 lane: lower the model into core SDF instances + rasterize on the
// shared `facett_core::render::CpuRenderer` (the same kernel the maps use).
let frame = render_to_rgba_l0(
self.backend,
&self.model,
&Decorations::default(),
&self.camera,
w,
h,
bg,
);
self.lit_px = frame.lit_px();
let color = egui::ColorImage::from_rgba_unmultiplied([w as usize, h as usize], &frame.rgba);
let tex = ctx.load_texture(format!("{}-frame", self.title), color, egui::TextureOptions::LINEAR);
self.tex = Some(tex);
self.last_size = (w, h);
}
}
impl Facet for GraphviewTab {
fn title(&self) -> &str {
self.title
}
fn ui(&mut self, ui: &mut egui::Ui) {
// The viewport: take the available area (min a sane size so the headless
// harness, which can hand a zero rect on the first frame, still renders).
let avail = ui.available_size();
let w = (avail.x.max(64.0)) as u32;
let h = avail.y.clamp(64.0, 900.0) as u32;
// Fit to the real rect the first time we know it.
if !self.fitted {
self.camera = Camera::fit(&self.model, w as f32, h as f32, 48.0);
self.fitted = true;
}
let (rect, resp) =
ui.allocate_exact_size(egui::vec2(w as f32, h as f32), egui::Sense::click_and_drag());
// ── Navigable: drag pans, scroll zooms (the shared pan/zoom contract). ──
let mut moved = false;
if resp.dragged() {
let d = resp.drag_delta();
self.camera.pan_x += d.x;
self.camera.pan_y += d.y;
moved = true;
}
let scroll = ui.input(|i| i.smooth_scroll_delta.y);
if resp.hovered() && scroll.abs() > 0.0 {
let factor = (scroll * 0.0025).exp();
self.camera.zoom = (self.camera.zoom * factor).clamp(0.05, 12.0);
moved = true;
}
// At scale with a renderer: the resident cloud (camera-only frames upload
// nothing). Otherwise the L0 kernel, re-rendered when the rect changed or the
// camera moved (else the texture is reused).
let resident = self.at_scale() && {
let th = facett::core::theme(ui);
ui.painter().rect_filled(rect, 0.0, th.bg);
self.paint_resident(ui, rect)
};
if resident {
self.lane = "resident";
self.tex = None;
self.last_size = (0, 0);
} else {
self.lane = "l0-cpu";
if moved || self.tex.is_none() || self.last_size != (w, h) {
let ctx = ui.ctx().clone();
self.render_into_texture(&ctx, w, h);
}
if let Some(tex) = &self.tex {
ui.painter().image(
tex.id(),
rect,
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
egui::Color32::WHITE,
);
}
}
// A small label so the AccessKit tree carries the tab name for the robot.
let lane_txt = if resident {
"resident GPU cloud"
} else if self.at_scale() {
"L0 kernel (CPU — no GPU)"
} else {
"L0 kernel"
};
ui.painter().text(
rect.left_top() + egui::vec2(8.0, 8.0),
egui::Align2::LEFT_TOP,
format!("graphview · {lane_txt} · {} nodes · {} edges", self.node_count(), self.edge_count()),
egui::FontId::proportional(13.0),
egui::Color32::from_gray(200),
);
// ── render-lane emit: this Facet::ui path RAN ─────────────────────────
#[cfg(feature = "testmatrix")]
{
let (quads, lines) = facett_graphview::lower_to_l0(&self.model, &Decorations::default(), &self.camera);
let (n_quads, n_lines) = (quads.len(), lines.len());
facett::core::testmatrix::emit(
"facett-demo::GraphviewTab::ui",
"ui_render",
// OK = the model lowered into the L0 SDF vocabulary and rasterized:
// a non-empty graph yields ≥1 node-marker quad and lit pixels, and
// the lowered counts never exceed the model (rendered ≤ input).
n_quads <= self.node_count()
&& n_lines <= self.edge_count()
&& (self.node_count() == 0 || (n_quads > 0 && self.lit_px > 0)),
&format!(
"nodes={} edges={} l0_quads={} l0_lines={} lit_px={}",
self.node_count(),
self.edge_count(),
n_quads,
n_lines,
self.lit_px,
),
);
}
}
fn state_json(&self) -> serde_json::Value {
// Lower the model into the L0 SDF vocabulary HERE (cheap, deterministic) so
// the instance-count proof is available even before the first `ui()` draw
// (the headless deck self-check reads state without drawing). The cached
// `self.lowered` from the last real draw matches this exactly.
let (quads, lines) = facett_graphview::lower_to_l0(&self.model, &Decorations::default(), &self.camera);
serde_json::json!({
"engine": "facett-graphview",
"render_path": "facett_core::render L0 Canvas (render_to_rgba_l0)",
"backend": format!("{:?}", self.backend),
"nodes": self.node_count(),
"edges": self.edge_count(),
"navigable": true,
"zoom": self.camera.zoom,
"pan": [self.camera.pan_x, self.camera.pan_y],
// The lowered L0 instance counts — proof the model routed through the
// shared SDF vocabulary (quads = node markers, lines = edges).
"l0_quads": quads.len(),
"l0_lines": lines.len(),
// The non-blank render oracle (EMIT-DOCTRINE: the can't-see-from-outside
// "did it actually draw" signal, surfaced as data). 0 until first draw.
"lit_px": self.lit_px,
// Which lane drew the last frame + whether the GPU cloud is there at all.
"lane": self.lane,
"gpu": if Self::renderer_installed() { "gpu" } else { "no GPU" },
"resident_uploads": self.resident_uploads(),
})
}
fn caps(&self) -> FacetCaps {
FacetCaps::NONE.navigable().themeable().resizable()
}
}
/// Above this many nodes the tab paints on the resident GPU cloud (when installed) —
/// the shared cut every resident 2-D pane makes.
#[cfg(any(feature = "gpu", feature = "graph-wgpu"))]
pub use facett::core::render::gpu::graphcloud::cloud::RESIDENT_ABOVE;
/// See the GPU build's `RESIDENT_ABOVE` (this build has no resident lane).
#[cfg(not(any(feature = "gpu", feature = "graph-wgpu")))]
pub const RESIDENT_ABOVE: usize = 5_000;
/// The number of layers in the synthetic mesh.
pub const LAYERS: usize = 5;
/// Nodes per layer.
pub const PER_LAYER: usize = 6;
/// Build the synthetic **layered service-mesh** graph the tab draws: `LAYERS`
/// columns × `PER_LAYER` nodes, fully forward-connected (every node in layer `i`
/// feeds every node in layer `i+1`) — a real fan-in/fan-out mesh that shows the
/// engine drawing dozens of nodes + scores of edges, not a 3-node toy. Per-layer
/// hue so the columns read as bands.
pub fn synthetic_mesh() -> GraphModel {
let layer_hue = |l: usize| -> Color {
// Walk the hue wheel across the layers (cheap HSV-ish ramp).
let palette = [
Color::rgb(70, 110, 190),
Color::rgb(80, 170, 120),
Color::rgb(200, 160, 70),
Color::rgb(190, 90, 120),
Color::rgb(140, 110, 200),
];
palette[l % palette.len()]
};
let mut nodes: Vec<GraphNode> = Vec::with_capacity(LAYERS * PER_LAYER);
for l in 0..LAYERS {
for r in 0..PER_LAYER {
let id = format!("n{l}_{r}");
// World-space layout: columns left→right, rows top→bottom (the caller
// owns layout, per the engine's model contract).
let x = l as f32 * 260.0;
let y = r as f32 * 90.0;
nodes.push(GraphNode {
id,
label: format!("svc {l}.{r}"),
fill: layer_hue(l),
stroke: Color::WHITE,
pos: Pos::new(x, y),
});
}
}
let mut edges: Vec<GraphEdge> = Vec::new();
for l in 0..LAYERS - 1 {
for r in 0..PER_LAYER {
for r2 in 0..PER_LAYER {
// Thin the mesh a touch so it reads as a mesh, not a solid block:
// connect to a rotating window of the next layer's nodes.
if (r + r2) % 2 == 0 {
edges.push(GraphEdge {
from: format!("n{l}_{r}"),
to: format!("n{}_{}", l + 1, r2),
color: Color::rgba(170, 180, 205, 200),
dashed: (r2 % 3) == 0,
label: None,
});
}
}
}
}
GraphModel { nodes, edges }
}
#[cfg(test)]
mod tests {
use super::*;
use facett::core::harness;
/// INJECT-ASSERT: the synthetic mesh is a real multi-layer fan-in/fan-out
/// graph (dozens of nodes, scores of edges), the tab is a navigable Facet, and
/// a headless render routes through the L0 kernel and draws a non-blank frame —
/// the lowered instance counts + lit pixels prove the consolidated path, not
/// "didn't panic".
#[test]
fn graphview_tab_renders_through_l0_kernel_nonblank() {
let model = synthetic_mesh();
assert_eq!(model.nodes.len(), LAYERS * PER_LAYER, "the mesh has LAYERS×PER_LAYER nodes");
assert!(model.edges.len() > model.nodes.len(), "real fan-in/fan-out edges, not a chain");
let mut tab = GraphviewTab::new(TITLE);
assert!(tab.caps().navigable, "the graphview tab is navigable");
assert!(tab.caps().themeable, "the graphview tab is themeable");
// Drive a real headless render frame.
let r = harness::headless_render(&mut tab);
assert!(r.drew(), "the graphview tab tessellated the egui texture image");
let s = tab.state_json();
assert_eq!(s["engine"], "facett-graphview");
assert!(s["render_path"].as_str().unwrap().contains("L0 Canvas"));
assert_eq!(s["nodes"].as_u64().unwrap(), (LAYERS * PER_LAYER) as u64);
assert!(s["edges"].as_u64().unwrap() > (LAYERS * PER_LAYER) as u64);
assert_eq!(s["navigable"], true);
// The L0 vocabulary carried the model: one quad per node (markers; rings
// only with decorations, none here) + one line per edge.
assert_eq!(s["l0_quads"].as_u64().unwrap(), (LAYERS * PER_LAYER) as u64, "one L0 marker quad per node");
assert_eq!(s["l0_lines"].as_u64().unwrap(), model.edges.len() as u64, "one L0 line per edge");
// The render oracle: the consolidated kernel actually lit pixels.
assert!(s["lit_px"].as_u64().unwrap() > 0, "the L0 kernel drew a non-blank frame");
}
/// INJECT-ASSERT: the navigable contract responds — zoom changes after a
/// scroll-driven camera move, asserted as a real state value (not "didn't
/// panic"). Mirrors the OSM tabs' navigable assertion.
#[test]
fn graphview_tab_camera_is_drivable() {
let mut tab = GraphviewTab::new(TITLE);
let z0 = tab.camera.zoom;
// Simulate a scroll-zoom step the way `ui` does.
tab.camera.zoom = (tab.camera.zoom * 1.2).clamp(0.05, 12.0);
assert!((tab.camera.zoom - z0).abs() > 1e-3, "the camera zoom moved");
// Re-render at the new zoom still lights pixels (the move didn't blank it).
let ctx = egui::Context::default();
tab.render_into_texture(&ctx, 800, 500);
assert!(tab.lit_px > 0, "re-render after a camera move stays non-blank");
}
}