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

nordisk/facett

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
}