makepad/libs/fab/tests/perf.rs
Admin 9821263b2c fab: a 3D creation shell and the viewer built on it — colour picker, material textures, dials that move the scene while they drag, the glTF loader, the tour, and the probes
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- fab: a 3D creation shell and the viewer built on it
- raytrace: the traced pane starts coarse and doubles to native, with the raster underneath
- fab: a colour picker, a material's textures, and dials that move the scene while they drag
- texcomp: the block codec and the container every texture will travel in
- fab: FAB_PROBE_MAT — per-material triangle counts, texture presence and uv spread in the roof probe
2026-08-26 08:49:47 +02:00

192 lines
6.8 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The performance gates from the viewer architecture, run against
//! the synthetic stress model rather than a sample (samples are untracked, and
//! neither of them is anywhere near 5 M triangles).
//!
//! These are slow and meaningless in a debug build, so they are `#[ignore]`d:
//!
//! ```text
//! cargo test --release -p makepad-fab-shell -- --ignored --nocapture
//! ```
use fab::model::makepad_math::Vec3f;
use fab::model::*;
fn rng(seed: &mut u32) -> f32 {
*seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
(*seed >> 8) as f32 / 16_777_216.0
}
#[test]
#[ignore = "stress test: run with --release -- --ignored"]
fn five_million_triangles_stay_interactive() {
let t0 = std::time::Instant::now();
let model = demo::synthetic_model(5_000_000, 4_000);
let gen_ms = t0.elapsed().as_secs_f32() * 1000.0;
let t1 = std::time::Instant::now();
let mut stages: Vec<(&'static str, f32)> = Vec::new();
let scene = Scene::from_model_with(model, &mut |stage, f| {
if f >= 1.0 || stages.last().map_or(true, |(s, _)| *s != stage) {
stages.push((stage, t1.elapsed().as_secs_f32() * 1000.0));
}
});
let build_ms = t1.elapsed().as_secs_f32() * 1000.0;
println!(
"synthetic: {} tris, {} verts, {} elements, {} batches, {} bvh nodes\n\
generate {gen_ms:.0} ms, build {build_ms:.0} ms, geometry {:.1} MB\n\
stages: {:?}",
scene.stats.triangles,
scene.stats.vertices,
scene.stats.elements,
scene.stats.batches,
scene.stats.bvh_nodes,
scene.stats.geometry_bytes as f64 / 1e6,
stages,
);
assert!(scene.stats.triangles > 4_500_000);
// Batch count is the whole point of the (material × cell) planner: the
// Metal backend draws a geometry's index buffer whole, so a batch is the
// smallest thing frustum culling can skip. Too few = no culling, too many
// = draw-call bound.
assert!(
scene.stats.batches >= 8 && scene.stats.batches <= 128,
"{} batches",
scene.stats.batches
);
// ≤ 40 B/vertex + 4 B/index is the §5 budget for the *packed* GPU stream;
// the CPU-side batches are the 48-byte std140 layout plus contours, and
// must still stay inside 2× that.
let budget = scene.stats.vertices as u64 * 96 + scene.stats.triangles as u64 * 12;
assert!(
scene.stats.geometry_bytes < budget,
"{} bytes over budget {}",
scene.stats.geometry_bytes,
budget
);
// ---- pick: < 1 ms at 5 M triangles ---------------------------------
let state = SceneState::default();
let mask = state.visibility_mask(&scene);
let c = aabb_center(&scene.bounds);
let r = aabb_radius(&scene.bounds);
let mut seed = 7u32;
let mut hits = 0;
let n = 500;
let t2 = std::time::Instant::now();
for _ in 0..n {
let origin = Vec3f {
x: c.x + (rng(&mut seed) - 0.5) * r,
y: c.y + (rng(&mut seed) - 0.5) * r,
z: scene.bounds.max.z + r,
};
let dir = Vec3f {
x: (rng(&mut seed) - 0.5) * 0.4,
y: (rng(&mut seed) - 0.5) * 0.4,
z: -1.0,
};
if scene
.pick_masked(&Ray::new(origin, dir), &state, &mask)
.is_some()
{
hits += 1;
}
}
let per_pick_us = t2.elapsed().as_secs_f64() * 1e6 / n as f64;
println!("pick: {per_pick_us:.1} µs average over {n} rays, {hits} hits");
assert!(hits > n / 4, "only {hits}/{n} rays hit anything");
assert!(per_pick_us < 1000.0, "pick took {per_pick_us:.0} µs");
// ---- culling -------------------------------------------------------
let mut visible = Vec::new();
let mut cam_dist = r * 2.0;
let t3 = std::time::Instant::now();
for _ in 0..100 {
cam_dist *= 1.0;
let eye = Vec3f {
x: c.x,
y: c.y - cam_dist,
z: c.z + cam_dist * 0.4,
};
let view = fab::model::makepad_math::Mat4f::look_at(
eye,
c,
Vec3f { x: 0.0, y: 0.0, z: 1.0 },
);
let proj = fab::model::makepad_math::Mat4f::perspective(
40.0,
1.6,
0.1,
r * 10.0,
);
let vp = fab::model::makepad_math::Mat4f::mul(&proj, &view);
let f = Frustum::from_view_proj(&vp);
scene.visible_elements(&f, &state, &mut visible);
}
let cull_us = t3.elapsed().as_secs_f64() * 1e6 / 100.0;
println!(
"cull: {cull_us:.1} µs per frame, {} of {} elements visible",
visible.len(),
scene.stats.elements
);
assert!(cull_us < 5000.0, "culling took {cull_us:.0} µs");
// ---- the lookup texture ------------------------------------------
let mut lookup = Vec::new();
let t4 = std::time::Instant::now();
state.element_lookup(&scene, None, &mut lookup);
let lookup_us = t4.elapsed().as_secs_f64() * 1e6;
println!("lookup: {lookup_us:.0} µs for {} floats", lookup.len());
assert_eq!(lookup.len(), scene.elements.len() * 8);
}
#[test]
#[ignore = "stress test: run with --release -- --ignored"]
fn bvh_beats_brute_force_and_agrees_with_it() {
let scene = Scene::from_model(demo::synthetic_model(400_000, 400), &mut |_| {});
let state = SceneState::default();
let mask = state.visibility_mask(&scene);
let c = aabb_center(&scene.bounds);
let r = aabb_radius(&scene.bounds);
let mut seed = 99u32;
let mut checked = 0;
for _ in 0..200 {
let origin = Vec3f {
x: c.x + (rng(&mut seed) - 0.5) * r,
y: c.y + (rng(&mut seed) - 0.5) * r,
z: scene.bounds.max.z + r * 0.5,
};
let dir = Vec3f {
x: (rng(&mut seed) - 0.5) * 0.6,
y: (rng(&mut seed) - 0.5) * 0.6,
z: -1.0,
};
let ray = Ray::new(origin, dir);
let fast = scene.pick_masked(&ray, &state, &mask);
// Brute force over every triangle of every batch.
let mut slow: Option<(ElementId, f32)> = None;
for b in &scene.batches {
for t in 0..b.triangle_count() as u32 {
let (p, q, s) = b.triangle(t);
if let Some((h, _, _)) = ray.intersect_triangle(p, q, s) {
if slow.map_or(true, |(_, bt)| h < bt) {
slow = Some((b.element_of_triangle(t).unwrap(), h));
}
}
}
}
match (fast, slow) {
(Some(f), Some((e, t))) => {
assert!((f.t - t).abs() < 1e-3, "t {} vs {t}", f.t);
assert_eq!(f.element, e);
checked += 1;
}
(None, None) => {}
(a, b) => panic!("bvh {a:?} vs brute {b:?}"),
}
}
println!("{checked} rays agreed with brute force");
assert!(checked > 50);
}