Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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);