use std::collections::HashSet;
use facett_graphview::{
Backend, Camera, Color, Decorations, GraphEdge, GraphModel, GraphNode, NodeDecoration, Pos,
backend::{decide, probe_from_env},
cpu::render as cpu_render,
l0::{lower, render_to_rgba_l0},
render_to_rgba,
model::{BOX_H, BOX_W},
};
use facett_core::render::prim::shape;
#[allow(unused)]
fn emit(check: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit("facett-graphview", check, ok, detail);
#[cfg(not(feature = "testmatrix"))]
{
let _ = (check, ok, detail);
}
}
fn model(n: usize, topo: Topo) -> GraphModel {
let cols = (n as f64).sqrt().ceil().max(1.0) as usize;
let nodes: Vec<GraphNode> = (0..n)
.map(|i| {
let (r, c) = (i / cols, i % cols);
GraphNode {
id: format!("n{i}"),
label: format!("node {i}"),
fill: Color::rgb((40 + (i * 7) % 200) as u8, (60 + (i * 13) % 180) as u8, 160),
stroke: Color::WHITE,
pos: Pos::new(c as f32 * (BOX_W + 40.0), r as f32 * (BOX_H + 40.0)),
}
})
.collect();
let edges = topo.edges(n);
GraphModel { nodes, edges }
}
#[derive(Clone, Copy, Debug)]
enum Topo {
Chain,
FanOut,
FanIn,
Star,
Grid,
}
impl Topo {
const ALL: [Topo; 5] = [Topo::Chain, Topo::FanOut, Topo::FanIn, Topo::Star, Topo::Grid];
fn name(self) -> &'static str {
match self {
Topo::Chain => "chain",
Topo::FanOut => "fanout",
Topo::FanIn => "fanin",
Topo::Star => "star",
Topo::Grid => "grid",
}
}
fn edge(from: usize, to: usize) -> GraphEdge {
GraphEdge {
from: format!("n{from}"),
to: format!("n{to}"),
color: Color::rgb(200, 200, 220),
dashed: from % 2 == 0,
label: None,
}
}
fn edges(self, n: usize) -> Vec<GraphEdge> {
if n < 2 {
return vec![];
}
match self {
Topo::Chain => (0..n - 1).map(|i| Self::edge(i, i + 1)).collect(),
Topo::FanOut => (1..n).map(|i| Self::edge(0, i)).collect(),
Topo::FanIn => (0..n - 1).map(|i| Self::edge(i, n - 1)).collect(),
Topo::Star => (1..n).flat_map(|i| [Self::edge(0, i), Self::edge(i, 0)]).collect(),
Topo::Grid => {
let cols = (n as f64).sqrt().ceil().max(1.0) as usize;
let mut e = Vec::new();
for i in 0..n {
if (i % cols) + 1 < cols && i + 1 < n {
e.push(Self::edge(i, i + 1));
}
if i + cols < n {
e.push(Self::edge(i, i + cols));
}
}
e
}
}
}
}
#[test]
fn world_bounds_contains_every_chip_and_is_finite() {
for topo in Topo::ALL {
for &n in &[1usize, 2, 17, 256] {
let m = model(n, topo);
let (mnx, mny, mxx, mxy) = m.world_bounds().expect("non-empty model has bounds");
assert!(mnx.is_finite() && mxy.is_finite(), "finite bounds");
assert!(mnx <= mxx && mny <= mxy, "min <= max");
let (hw, hh) = (BOX_W * 0.5, BOX_H * 0.5);
for node in &m.nodes {
assert!(node.pos.x - hw >= mnx - 1e-3 && node.pos.x + hw <= mxx + 1e-3);
assert!(node.pos.y - hh >= mny - 1e-3 && node.pos.y + hh <= mxy + 1e-3);
}
}
}
assert!(GraphModel::default().world_bounds().is_none());
emit("world_bounds_contains_chips", true, "AABB contains every chip across 5 topologies × 4 sizes");
}
#[test]
fn camera_fit_centers_world_and_clamps_zoom() {
for topo in Topo::ALL {
for &n in &[1usize, 9, 100, 5000] {
for &(w, h) in &[(320.0f32, 320.0f32), (640.0, 480.0), (1920.0, 1080.0)] {
let m = model(n, topo);
let cam = Camera::fit(&m, w, h, 40.0);
assert!(cam.zoom >= 0.05 && cam.zoom <= 4.0, "zoom clamp: {}", cam.zoom);
if let Some((mnx, mny, mxx, mxy)) = m.world_bounds() {
let (wcx, wcy) = ((mnx + mxx) * 0.5, (mny + mxy) * 0.5);
let (sx, sy) = cam.project(Pos::new(wcx, wcy));
assert!((sx - w * 0.5).abs() < 1.0, "x centered n={n} got {sx}");
assert!((sy - h * 0.5).abs() < 1.0, "y centered n={n} got {sy}");
}
}
}
}
emit("camera_fit_centers_and_clamps", true, "fit centers world + zoom∈[0.05,4] across sizes/topologies");
}
#[test]
fn project_is_the_exact_affine() {
let cam = Camera { pan_x: 12.5, pan_y: -7.0, zoom: 2.5 };
for &(x, y) in &[(0.0f32, 0.0f32), (100.0, -50.0), (-300.0, 220.0)] {
let (sx, sy) = cam.project(Pos::new(x, y));
assert!((sx - (x * 2.5 + 12.5)).abs() < 1e-4, "x affine");
assert!((sy - (y * 2.5 - 7.0)).abs() < 1e-4, "y affine");
}
let def = Camera::fit(&GraphModel::default(), 100.0, 100.0, 10.0);
assert_eq!((def.pan_x, def.pan_y, def.zoom), (0.0, 0.0, 1.0));
emit("project_affine_exact", true, "project = p*zoom+pan to 1e-4");
}
#[test]
fn decide_is_monotone_in_the_probe() {
use facett_graphview::GpuProbe;
assert_eq!(decide(GpuProbe::cpu_only()), Backend::CpuVello);
assert_eq!(decide(GpuProbe { usable_gpu: true, force_cpu: true }), Backend::CpuVello);
assert_eq!(decide(GpuProbe { usable_gpu: false, force_cpu: false }), Backend::CpuVello);
let got = decide(GpuProbe { usable_gpu: true, force_cpu: false });
assert_eq!(got.is_gpu(), facett_graphview::backend::gpu_arm_compiled());
let p = probe_from_env(false);
assert_eq!(decide(p), Backend::CpuVello, "no gpu ⇒ cpu");
emit("decide_monotone", true, "cpu-only/forced/no-gpu→CPU; gpu+unforced→feature-gated");
}
#[test]
fn lower_counts_equal_nodes_and_edges_with_real_attrs() {
for topo in Topo::ALL {
for &n in &[2usize, 10, 64, 400] {
let m = model(n, topo);
let cam = Camera::fit(&m, 1280.0, 800.0, 40.0);
let mut deco = Decorations::default();
let mut rings = 0;
for (i, node) in m.nodes.iter().enumerate() {
if i % 5 == 0 {
deco.nodes.insert(
node.id.clone(),
NodeDecoration { scale: None, ring: Some(Color::rgb(240, 80, 80)), badge: None, badge_color: None },
);
rings += 1;
}
}
if n >= 2 {
deco.edges.push(Topo::edge(0, 1));
}
let (quads, lines) = lower(&m, &deco, &cam);
assert_eq!(quads.len(), n + rings, "quads = nodes + rings (n={n} topo={})", topo.name());
let want_lines = m.edges.len() + deco.edges.len();
assert_eq!(lines.len(), want_lines, "lines = base+emphasis edges");
assert_eq!(quads[0].shape, shape::SQUARE);
let want = [
m.nodes[0].fill.r as f32 / 255.0,
m.nodes[0].fill.g as f32 / 255.0,
m.nodes[0].fill.b as f32 / 255.0,
1.0,
];
assert!((quads[0].color[0] - want[0]).abs() < 1e-6, "fill carried");
if let Some(l) = lines.first() {
assert!((l.color[0] - 200.0 / 255.0).abs() < 1e-6, "edge colour carried");
}
}
}
emit("lower_counts_and_attrs", true, "quads=nodes+rings, lines=edges+emphasis, colours carried");
}
fn distinct_colours(rgba: &[u8], cap: usize) -> usize {
let mut seen = HashSet::new();
for px in rgba.chunks_exact(4) {
seen.insert([px[0], px[1], px[2]]);
if seen.len() > cap {
break;
}
}
seen.len()
}
#[test]
fn cpu_reference_lane_renders_nonblank() {
let bg = Color::rgb(18, 20, 28);
let backend = decide(facett_graphview::GpuProbe::cpu_only());
for topo in Topo::ALL {
for &n in &[2usize, 12, 80] {
let m = model(n, topo);
let cam = Camera::fit(&m, 640.0, 480.0, 40.0);
let frame = cpu_render(&m, &Decorations::default(), &cam, 640, 480, bg);
assert_eq!(frame.rgba.len(), 640 * 480 * 4);
let lit = frame.rgba.chunks_exact(4).filter(|p| p[3] != 0).count();
let colours = distinct_colours(&frame.rgba, 8);
let ok = lit > 0 && colours > 3;
assert!(ok, "cpu lane non-blank n={n} topo={} lit={lit} colours={colours}", topo.name());
let via = render_to_rgba(backend, &m, &Decorations::default(), &cam, 640, 480, bg);
assert_eq!(via.rgba, frame.rgba, "render_to_rgba == cpu::render on the CPU arm");
emit(
"cpu_render_nonblank",
ok,
&format!("topo={} n={n} lit_px={lit} distinct_colours={colours}", topo.name()),
);
}
}
}
#[test]
fn l0_lane_renders_nonblank_through_core_canvas() {
let bg = Color::rgb(18, 20, 28);
let backend = decide(facett_graphview::GpuProbe::cpu_only());
for topo in Topo::ALL {
for &n in &[2usize, 12, 80] {
let m = model(n, topo);
let cam = Camera::fit(&m, 640.0, 480.0, 40.0);
let frame = render_to_rgba_l0(backend, &m, &Decorations::default(), &cam, 640, 480, bg);
assert_eq!(frame.rgba.len(), 640 * 480 * 4);
let lit = frame.lit_px();
let colours = distinct_colours(&frame.rgba, 8);
let ok = lit > 0 && colours > 3;
assert!(ok, "l0 lane non-blank n={n} topo={} lit={lit} colours={colours}", topo.name());
emit(
"l0_render_nonblank",
ok,
&format!("topo={} n={n} lit_px={lit} distinct_colours={colours}", topo.name()),
);
}
}
}
#[test]
fn matrix_thousand_cases_assert_each_stage() {
let sizes: &[usize] = &[
10, 25, 50, 100, 250, 500, 750, 1_000, 2_500, 5_000, 10_000, 25_000, 50_000, 75_000,
100_000,
];
let zooms: &[Option<f32>] = &[None, Some(0.05), Some(0.5), Some(1.0), Some(4.0)]; let canvases: &[(u32, u32)] = &[(320, 320), (640, 480), (1920, 1080)];
let bg = Color::rgb(18, 20, 28);
let mut cases = 0usize;
let mut lower_ok_all = true;
let mut raster_ok_all = true;
let mut bounds_ok_all = true;
let mut centered_ok_all = true;
for topo in Topo::ALL {
for &n in sizes {
let m = model(n, topo);
let (mnx, mny, mxx, mxy) = m.world_bounds().unwrap();
bounds_ok_all &= mnx <= mxx && mny <= mxy && mnx.is_finite();
for &zoom in zooms {
for &(w, h) in canvases {
cases += 1;
let cam = match zoom {
None => Camera::fit(&m, w as f32, h as f32, 40.0),
Some(z) => {
let (cx, cy) = ((mnx + mxx) * 0.5, (mny + mxy) * 0.5);
Camera {
zoom: z,
pan_x: w as f32 * 0.5 - cx * z,
pan_y: h as f32 * 0.5 - cy * z,
}
}
};
let (cx, cy) = ((mnx + mxx) * 0.5, (mny + mxy) * 0.5);
let (sx, sy) = cam.project(Pos::new(cx, cy));
centered_ok_all &= (sx - w as f32 * 0.5).abs() < 1.5 && (sy - h as f32 * 0.5).abs() < 1.5;
let (quads, lines) = lower(&m, &Decorations::default(), &cam);
lower_ok_all &= quads.len() == n && lines.len() == m.edges.len();
if n <= 1_000 && w <= 640 {
let frame = render_to_rgba_l0(Backend::CpuVello, &m, &Decorations::default(), &cam, w, h, bg);
raster_ok_all &= frame.rgba.len() == (w * h * 4) as usize;
}
}
}
}
}
assert!(cases >= 1000, "matrix must run ≥1000 cases, ran {cases}");
assert!(bounds_ok_all, "every world_bounds finite + ordered");
assert!(centered_ok_all, "every camera centres the world");
assert!(lower_ok_all, "every lower: quads==nodes && lines==edges");
assert!(raster_ok_all, "every sampled raster has the right buffer shape");
emit("matrix_cases", true, &format!("ran {cases} cases (5 topo × 9 sizes × 5 zoom × 3 canvas)"));
emit("matrix_bounds_stage", bounds_ok_all, "world_bounds finite+ordered every case");
emit("matrix_camera_stage", centered_ok_all, "camera centres world every case");
emit("matrix_lower_stage", lower_ok_all, "lower counts == nodes/edges every case (to 100k)");
emit("matrix_raster_stage", raster_ok_all, "raster buffer contract holds every sampled case");
}
#[test]
fn color_dim_and_egui_roundtrip() {
let c = Color::rgba(200, 100, 50, 200);
let dim = c.dim(0.5);
assert_eq!((dim.r, dim.g, dim.b), (200, 100, 50), "rgb preserved");
assert_eq!(dim.a, 100, "alpha halved");
let opaque = Color::rgb(200, 100, 50);
let back_opaque: Color = egui::Color32::from(opaque).into();
assert_eq!(back_opaque, opaque, "opaque Color ↔ Color32 round-trips exactly");
let back: Color = egui::Color32::from(c).into();
assert!(
(back.r as i32 - c.r as i32).abs() <= 1
&& (back.g as i32 - c.g as i32).abs() <= 1
&& (back.b as i32 - c.b as i32).abs() <= 1
&& back.a == c.a,
"translucent Color ↔ Color32 round-trips within ±1 LSB (premultiply), got {back:?}"
);
emit("color_dim_and_roundtrip", true, "dim halves alpha; Color32 round-trips");
}
#[test]
fn frame_becomes_a_nonblank_egui_texture() {
let m = model(40, Topo::Star);
let cam = Camera::fit(&m, 512.0, 512.0, 40.0);
let frame = render_to_rgba_l0(Backend::CpuVello, &m, &Decorations::default(), &cam, 512, 512, Color::rgb(18, 20, 28));
let img = egui::ColorImage::from_rgba_unmultiplied([512, 512], &frame.rgba);
assert_eq!(img.size, [512, 512], "texture sized to the canvas");
assert_eq!(img.pixels.len(), 512 * 512, "one pixel per texel");
let bg = egui::Color32::from_rgb(18, 20, 28);
let non_bg = img.pixels.iter().filter(|&&p| p != bg).count();
let ok = non_bg > 0;
assert!(ok, "the egui texture is non-blank ({non_bg} non-bg texels)");
emit("frame_to_egui_texture_nonblank", ok, &format!("non_bg_texels={non_bg} of {}", 512 * 512));
}