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.");
}