facett-graphview/benches/raster.rs
//! **The "is it worth it" benchmark** — `vello_cpu` rasterization vs egui's
//! `epaint` tessellation, on a synthetic large graph (1k / 10k edges). Reports
//! per-frame raster/tessellation time. This is the evidence for the engine.
//!
//! NOTE — read the result honestly: this is NOT apples-to-apples. The vello_cpu
//! side does the FULL software rasterization to a 1.6M-pixel buffer; the epaint
//! side only TESSELLATES the same chips+edges to meshes (it does not rasterize —
//! egui's backend does that afterward). So on the CPU, full raster is naturally
//! heavier than mesh-building. The real win is the GPU path (vello on wgpu),
//! where fill is ~free and the compute pipeline scales past epaint's CPU
//! tessellator. The vello_cpu number here is the no-GPU FLOOR. See
//! `.nornir/design.md` for the full reading.
//!
//! Run: `cargo bench -p facett-graphview`
use criterion::{Criterion, criterion_group, criterion_main};
use std::hint::black_box;
use facett_graphview::{
Backend, Camera, Color, Decorations, GraphEdge, GraphModel, GraphNode, Pos, render_to_rgba,
};
/// Build a synthetic layered graph with roughly `edges` edges: a grid of nodes
/// in `cols` columns, each node wired to a couple in the next column.
fn synthetic(edges: usize) -> GraphModel {
let cols = 24usize;
let per_col = ((edges / 2).max(cols) / cols).max(2);
let mut nodes = Vec::new();
for c in 0..cols {
for r in 0..per_col {
nodes.push(GraphNode {
id: format!("{c}_{r}"),
label: format!("n{c}.{r}"),
fill: Color::rgb(40 + (c * 7) as u8, 80, 140),
stroke: Color::WHITE,
pos: Pos::new(c as f32 * 260.0, r as f32 * 60.0),
});
}
}
let mut es = Vec::new();
for c in 0..cols - 1 {
for r in 0..per_col {
for k in 0..2 {
let tr = (r + k) % per_col;
es.push(GraphEdge {
from: format!("{c}_{r}"),
to: format!("{}_{}", c + 1, tr),
color: Color::rgb(180, 180, 200),
dashed: k == 1,
label: None,
});
}
}
}
GraphModel { nodes, edges: es }
}
/// The epaint baseline: tessellate the same model into egui meshes (what egui's
/// painter does every frame before the backend draws). We build the shapes a
/// painter would emit (rounded rects + cubic edges) and run epaint's tessellator.
fn epaint_tessellate(model: &GraphModel, cam: &Camera) -> usize {
use egui::epaint::{
CubicBezierShape, RectShape, Shape, Stroke, StrokeKind, TessellationOptions,
tessellator::Tessellator,
};
use egui::{Color32, CornerRadius, Pos2, Rect, Vec2};
let mut shapes: Vec<egui::epaint::ClippedShape> = Vec::new();
let clip = Rect::from_min_size(Pos2::ZERO, egui::vec2(4000.0, 4000.0));
let idx: std::collections::HashMap<&str, Pos2> = model
.nodes
.iter()
.map(|n| {
let (x, y) = cam.project(n.pos);
(n.id.as_str(), Pos2::new(x, y))
})
.collect();
let hw = facett_graphview::BOX_W * 0.5 * cam.zoom;
let hh = facett_graphview::BOX_H * 0.5 * cam.zoom;
for n in &model.nodes {
let (x, y) = cam.project(n.pos);
let rect = Rect::from_center_size(Pos2::new(x, y), Vec2::new(hw * 2.0, hh * 2.0));
let fill: Color32 = n.fill.into();
let stroke: Color32 = n.stroke.into();
shapes.push(egui::epaint::ClippedShape {
clip_rect: clip,
shape: Shape::Rect(RectShape::new(
rect,
CornerRadius::same(5),
fill,
Stroke::new(1.4_f32, stroke),
StrokeKind::Outside,
)),
});
}
for e in &model.edges {
let (Some(&a0), Some(&b0)) = (idx.get(e.from.as_str()), idx.get(e.to.as_str())) else {
continue;
};
let a = a0 + Vec2::new(hw, 0.0);
let b = b0 - Vec2::new(hw, 0.0);
let midx = (a.x + b.x) * 0.5;
let col: Color32 = e.color.into();
shapes.push(egui::epaint::ClippedShape {
clip_rect: clip,
shape: Shape::CubicBezier(CubicBezierShape::from_points_stroke(
[a, Pos2::new(midx, a.y), Pos2::new(midx, b.y), b],
false,
Color32::TRANSPARENT,
Stroke::new(1.4_f32, col),
)),
});
}
let mut tess = Tessellator::new(
1.0,
TessellationOptions::default(),
[16, 16],
Vec::new(),
);
let mut total_vertices = 0usize;
for cs in &shapes {
let mut mesh = egui::epaint::Mesh::default();
tess.tessellate_shape(cs.shape.clone(), &mut mesh);
total_vertices += mesh.vertices.len();
}
total_vertices
}
fn bench(c: &mut Criterion) {
let (w, h) = (1600u32, 1000u32);
for &edge_count in &[1_000usize, 10_000usize] {
let model = synthetic(edge_count);
let cam = Camera::fit(&model, w as f32, h as f32, 40.0);
let deco = Decorations::default();
let real_edges = model.edges.len();
let nodes = model.nodes.len();
let mut g = c.benchmark_group(format!("graph_{real_edges}e_{nodes}n"));
g.sample_size(10);
g.bench_function("vello_cpu_raster", |b| {
b.iter(|| {
let frame = render_to_rgba(
Backend::VelloCpu,
black_box(&model),
&deco,
&cam,
w,
h,
Color::rgb(18, 20, 28),
);
black_box(frame.rgba.len())
})
});
g.bench_function("epaint_tessellate", |b| {
b.iter(|| black_box(epaint_tessellate(black_box(&model), &cam)))
});
g.finish();
}
}
criterion_group!(benches, bench);
criterion_main!(benches);