facett-graphview/src/cpu.rs
//! The **CPU rasterizer** (`vello_cpu`) — the reference render path. Builds the shared
//! backend-neutral display list ([`crate::scene`]) from a [`GraphModel`] +
//! [`Decorations`] under a [`Camera`] and rasterizes it to a straight-RGBA8 pixel buffer
//! (ready for a PNG or an egui texture). vello_cpu is multithreaded-SIMD software
//! rendering: the proof that "vello" is not GPU-only.
//!
//! Geometry mirrors nornir's `draw_graph`: nodes are rounded rects with a kind fill + a
//! status ring; edges are PCB-style cubics (mid-break) with a little arrowhead;
//! decorations add ring colours / emphasis edges. That geometry is built ONCE in
//! [`crate::scene::build`] and shared with the GPU lane ([`crate::gpu`]) so the two
//! backends paint the same picture (TODO(#17): "do not fork geometry").
use vello_cpu::color::{AlphaColor, Srgb};
use vello_cpu::kurbo::{BezPath, Cap, Join, PathEl, Point, RoundedRect, Shape, Stroke};
use vello_cpu::{Pixmap, RenderContext};
use crate::model::{Camera, Color, Decorations, GraphModel};
use crate::scene::{Prim, Seg};
/// A rendered frame: straight (un-premultiplied) RGBA8, row-major, `w × h`.
pub struct Rendered {
pub width: u32,
pub height: u32,
/// `width * height * 4` bytes, `[r, g, b, a]` per pixel, NOT premultiplied.
pub rgba: Vec<u8>,
}
fn to_alpha(c: Color) -> AlphaColor<Srgb> {
AlphaColor::from_rgba8(c.r, c.g, c.b, c.a)
}
/// Build + rasterize `model` (decorated, under `camera`) into a `w × h` frame on the CPU.
/// The single entry point the `VelloCpu` backend drives.
pub fn render(
model: &GraphModel,
decorations: &Decorations,
camera: &Camera,
width: u32,
height: u32,
background: Color,
) -> Rendered {
let mut ctx = RenderContext::new(width as u16, height as u16);
// Background.
ctx.set_paint(to_alpha(background));
let full = vello_cpu::kurbo::Rect::new(0.0, 0.0, width as f64, height as f64);
ctx.fill_rect(&full);
// The SHARED display list — identical geometry to the GPU lane ([`crate::gpu`]).
for prim in crate::scene::build(model, decorations, camera) {
paint(&mut ctx, &prim);
}
let mut pixmap = Pixmap::new(width as u16, height as u16);
ctx.flush();
ctx.render_to_pixmap(&mut pixmap);
let rgba: Vec<u8> = pixmap
.take_unpremultiplied()
.into_iter()
.flat_map(|p| [p.r, p.g, p.b, p.a])
.collect();
// ── graphview legacy CPU render-lane emit ─────────────────────────────────
// This vello_cpu lane is distinct from the core `CpuRenderer` lane; it was
// uninstrumented. Record that it ran + the node/edge geometry it drew + a
// lit-pixel proof so a blank graph frame surfaces as RED.
#[cfg(feature = "testmatrix")]
{
let total = (width as usize) * (height as usize);
let lit = rgba.as_chunks::<4>().0.iter().filter(|p| p[3] != 0).count();
let geom = model.nodes.len() + model.edges.len();
facett_core::testmatrix::emit(
"facett-graphview::cpu::render",
"cpu_render",
(geom == 0 || lit > 0) && rgba.len() == total * 4,
&format!("nodes={} edges={} lit_px={lit} total_px={total}", model.nodes.len(), model.edges.len()),
);
}
Rendered { width, height, rgba }
}
/// Lower one shared [`Prim`] onto the vello_cpu `RenderContext`.
fn paint(ctx: &mut RenderContext, prim: &Prim) {
match prim {
Prim::FillRoundRect { x0, y0, x1, y1, radius, color } => {
let rect = RoundedRect::new(*x0, *y0, *x1, *y1, *radius);
ctx.set_paint(to_alpha(*color));
ctx.fill_path(&rect.to_path(0.1));
}
Prim::StrokeRoundRect { x0, y0, x1, y1, radius, width, color } => {
let rect = RoundedRect::new(*x0, *y0, *x1, *y1, *radius);
ctx.set_stroke(Stroke::new(*width).with_join(Join::Round));
ctx.set_paint(to_alpha(*color));
ctx.stroke_path(&rect.to_path(0.1));
}
Prim::StrokePath { segs, width, color } => {
ctx.set_stroke(Stroke::new(*width).with_caps(Cap::Round).with_join(Join::Round));
ctx.set_paint(to_alpha(*color));
ctx.stroke_path(&to_bezpath(segs));
}
}
}
/// Build a vello_cpu `BezPath` from the shared segment list.
fn to_bezpath(segs: &[Seg]) -> BezPath {
let mut path = BezPath::new();
for s in segs {
match *s {
Seg::MoveTo(x, y) => path.push(PathEl::MoveTo(Point::new(x, y))),
Seg::LineTo(x, y) => path.push(PathEl::LineTo(Point::new(x, y))),
Seg::CurveTo(c1x, c1y, c2x, c2y, x, y) => path.push(PathEl::CurveTo(
Point::new(c1x, c1y),
Point::new(c2x, c2y),
Point::new(x, y),
)),
}
}
path
}