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

nordisk/facett

facett-core/examples/cloud_frame_cost.rs

//! **What does a frame cost on each lane?** — the whole point of the cloud pass.
//!
//! The instance lane rebuilds a screen-space array every frame: at a million nodes that
//! is ~96 MB of CPU work and PCIe traffic to draw the same points from a slightly
//! different angle. The cloud lane uploads the model ONCE and writes a 48-byte camera.
//!
//! This measures the CPU side of a spin: N frames of "advance the yaw and get ready to
//! draw", with the GPU work excluded from both so the comparison is about the thing that
//! changed.
//!
//! Run: cargo run --release -p facett-core --features wgpu --example cloud_frame_cost
#![cfg(feature = "wgpu")]

use std::time::Instant;

use facett_core::render::gpu::{CloudCamera, CloudEdge, CloudPoint, EdgeInstance, GraphScene, NodeInstance};

const FRAMES: usize = 60;

fn model(n: usize) -> (Vec<CloudPoint>, Vec<CloudEdge>) {
    let golden = 2.399_963_2_f32;
    let pts = (0..n)
        .map(|i| {
            let m = n as f32;
            let y = 1.0 - 2.0 * ((i as f32) + 0.5) / m;
            let r = (1.0 - y * y).max(0.0).sqrt();
            let th = golden * i as f32;
            CloudPoint::new([r * th.cos(), y, r * th.sin()], egui::Color32::from_rgb(80, 160, 230))
        })
        .collect();
    // ~1.3 edges per node, the density facett-graph3d's generated dataset produces.
    let edges: Vec<CloudEdge> = (0..n * 13 / 10).map(|i| [(i % n) as u32, ((i * 7 + 3) % n) as u32]).collect();
    (pts, edges)
}

/// The per-frame CPU build the instance lane does (the shape of `Graph3D::gpu_scene`).
fn build_instances(pts: &[CloudPoint], edges: &[CloudEdge], cam: &CloudCamera) -> GraphScene {
    let (yaw, scale) = (cam.cam[0], cam.cam[1]);
    let (cx, cy) = (cam.viewport[2], cam.viewport[3]);
    let (s, c) = yaw.sin_cos();
    let proj: Vec<([f32; 2], f32)> = pts
        .iter()
        .map(|p| {
            let [px, py, pz] = p.pos;
            ([cx + (px * c + pz * s) * scale, cy - py * scale], -px * s + pz * c)
        })
        .collect();
    let rad = |depth: f32, mul: f32| {
        let k = 3.0 * (0.55 + (depth + 1.0) * 0.35).clamp(0.4, 1.3) * mul;
        [0.1 * k, 0.4 * k, 0.8 * k, 1.0]
    };
    GraphScene {
        background: [0.01, 0.01, 0.02, 1.0],
        nodes: pts
            .iter()
            .zip(&proj)
            .map(|(_, &(sp, z))| NodeInstance { center: sp, radius: 3.0 + (z + 1.0) * 2.5, _pad: 0.0, color: rad(z, 1.0) })
            .collect(),
        edges: edges
            .iter()
            .map(|&[a, b]| {
                let (pa, pb) = (proj[a as usize], proj[b as usize]);
                EdgeInstance { a: pa.0, b: pb.0, width: 1.5, _pad: 0.0, color: rad((pa.1 + pb.1) * 0.5, 0.6) }
            })
            .collect(),
    }
}

fn main() {
    println!("# per-FRAME CPU cost of a spinning cloud — instance lane vs cloud lane");
    println!("{:>9}  {:>9}  {:>16}  {:>14}  {:>16}", "nodes", "edges", "instance ms/f", "cloud ms/f", "instance bytes/f");
    for &n in &[30_000usize, 100_000, 300_000, 1_000_000] {
        let (pts, edges) = model(n);
        let yaws: Vec<f32> = (0..FRAMES).map(|f| f as f32 * 0.05).collect();

        // Instance lane: rebuild everything, every frame.
        let t = Instant::now();
        for &yaw in &yaws {
            let cam = CloudCamera::frame(1280.0, 800.0, yaw, 3.0, 1.0);
            let sc = build_instances(&pts, &edges, &cam);
            std::hint::black_box(&sc);
        }
        let inst = t.elapsed().as_secs_f64() * 1000.0 / FRAMES as f64;

        // Cloud lane: the model is already on the GPU. Per frame the CPU makes ONE
        // camera. (The upload is outside the loop because it happens once per LAYOUT,
        // not once per frame — that is the entire idea.)
        let t = Instant::now();
        for &yaw in &yaws {
            let cam = CloudCamera::frame(1280.0, 800.0, yaw, 3.0, 1.0);
            std::hint::black_box(&cam);
        }
        let cloud = t.elapsed().as_secs_f64() * 1000.0 / FRAMES as f64;

        let bytes = n * 32 + edges.len() * 40 + n * 12;
        // NO ratio column on purpose. The cloud lane's per-frame work is constructing a
        // 48-byte struct, which is below this timer's resolution — dividing by it yields
        // a six-figure "speedup" that is arithmetic, not a measurement. The honest
        // statement is that the per-frame work is GONE, not faster, and the bytes column
        // is what says so.
        println!(
            "{:>9}  {:>9}  {:>13.3} ms  {:>11.4} ms  {:>13.1} MB",
            n,
            edges.len(),
            inst,
            cloud,
            bytes as f64 / 1e6
        );
    }
    println!();
    println!("`instance bytes/f` is what the instance lane rebuilds and re-sends EVERY frame.");
    println!("The cloud lane sends 48 bytes — the camera — whatever the model size. The model");
    println!("itself is uploaded once per LAYOUT change, not once per frame. So the cloud column");
    println!("is not `faster`: the per-frame work is gone, which is why no ratio is printed.");
}