497 lines
15 KiB
Rust
497 lines
15 KiB
Rust
use std::{
|
|
hint::black_box,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use bevy_math::{Quat, Vec3};
|
|
use gamecult_geometry_csg::{
|
|
Aabb, Assembler, BrushOp, DomainQuery, MaterialId, Primitive, lower_selected_cut_chunks,
|
|
ragnarok_column_fixture, select_domain_cut,
|
|
};
|
|
|
|
const WARMUP_ITERS: usize = 8;
|
|
const MEASURE_ITERS: usize = 64;
|
|
|
|
fn main() {
|
|
let cases = [
|
|
PerfCase {
|
|
name: "single_center_cut",
|
|
brushes: 2,
|
|
build: single_center_cut,
|
|
},
|
|
PerfCase {
|
|
name: "room_grid_8x8_doors",
|
|
brushes: 192,
|
|
build: room_grid_8x8_doors,
|
|
},
|
|
PerfCase {
|
|
name: "rotated_cut_stack_64",
|
|
brushes: 65,
|
|
build: rotated_cut_stack_64,
|
|
},
|
|
PerfCase {
|
|
name: "common_box_chain_64",
|
|
brushes: 64,
|
|
build: common_box_chain_64,
|
|
},
|
|
PerfCase {
|
|
name: "distant_cutters_512",
|
|
brushes: 513,
|
|
build: distant_cutters_512,
|
|
},
|
|
PerfCase {
|
|
name: "distant_oriented_cutters_128",
|
|
brushes: 129,
|
|
build: distant_oriented_cutters_128,
|
|
},
|
|
];
|
|
|
|
for case in cases {
|
|
run_case(case, PerfMode::Stable);
|
|
run_case(case, PerfMode::Dirty);
|
|
run_case(case, PerfMode::IncrementalDirty);
|
|
run_case(case, PerfMode::Routed);
|
|
}
|
|
|
|
run_lod_case(
|
|
"ragnarok_column_lod_tight",
|
|
ragnarok_lod_query(10_000.0, 100),
|
|
);
|
|
run_lod_case("ragnarok_column_lod_medium", ragnarok_lod_query(0.01, 560));
|
|
run_lod_case("ragnarok_column_lod_high", ragnarok_lod_query(0.01, 10_000));
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct PerfCase {
|
|
name: &'static str,
|
|
brushes: usize,
|
|
build: fn() -> Assembler,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum PerfMode {
|
|
Stable,
|
|
Dirty,
|
|
IncrementalDirty,
|
|
Routed,
|
|
}
|
|
|
|
impl PerfMode {
|
|
fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Stable => "stable",
|
|
Self::Dirty => "dirty",
|
|
Self::IncrementalDirty => "incremental_dirty",
|
|
Self::Routed => "routed",
|
|
}
|
|
}
|
|
|
|
fn build(
|
|
self,
|
|
assembler: &mut Assembler,
|
|
iteration: usize,
|
|
) -> gamecult_geometry_csg::BuildOutput {
|
|
match self {
|
|
Self::Stable => assembler.build(),
|
|
Self::Dirty => {
|
|
nudge_tail_brush(assembler, iteration);
|
|
assembler.rebuild()
|
|
}
|
|
Self::IncrementalDirty => {
|
|
nudge_tail_brush(assembler, iteration);
|
|
assembler.build_incremental()
|
|
}
|
|
Self::Routed => assembler.rebuild_routed_surfaces(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_case(case: PerfCase, mode: PerfMode) {
|
|
let mut assembler = (case.build)();
|
|
if matches!(mode, PerfMode::Routed) && !assembler.supports_routed_surfaces() {
|
|
return;
|
|
}
|
|
let warmup = if matches!(mode, PerfMode::IncrementalDirty) {
|
|
assembler.build_incremental()
|
|
} else {
|
|
assembler.build()
|
|
};
|
|
black_box(warmup.mesh.triangle_count());
|
|
|
|
for _ in 0..WARMUP_ITERS {
|
|
let output = mode.build(&mut assembler, 0);
|
|
black_box(output.mesh.triangle_count());
|
|
}
|
|
|
|
let mut timings = Vec::with_capacity(MEASURE_ITERS);
|
|
let mut triangles = 0;
|
|
let mut fragments = 0;
|
|
let mut warnings = 0;
|
|
let mut candidate_pairs = 0;
|
|
let mut rejected_pairs = 0;
|
|
let mut reused_mesh = false;
|
|
let mut used_checkpoint = false;
|
|
let mut checkpoint_rebuild_from = None::<usize>;
|
|
let mut direct_dirty_rebuild = false;
|
|
|
|
for iteration in 0..MEASURE_ITERS {
|
|
let start = Instant::now();
|
|
let output = mode.build(&mut assembler, iteration);
|
|
let elapsed = start.elapsed();
|
|
triangles = output.mesh.triangle_count();
|
|
fragments = output.report.emitted_convex_fragments;
|
|
warnings = output.report.warnings.len();
|
|
candidate_pairs = output.report.candidate_pairs;
|
|
rejected_pairs = output.report.rejected_pairs;
|
|
reused_mesh = output.report.reused_mesh;
|
|
used_checkpoint = output.report.used_checkpoint;
|
|
checkpoint_rebuild_from = output.report.checkpoint_rebuild_from;
|
|
direct_dirty_rebuild = output.report.direct_dirty_rebuild;
|
|
black_box((
|
|
triangles,
|
|
fragments,
|
|
warnings,
|
|
candidate_pairs,
|
|
rejected_pairs,
|
|
reused_mesh,
|
|
used_checkpoint,
|
|
checkpoint_rebuild_from,
|
|
direct_dirty_rebuild,
|
|
));
|
|
timings.push(elapsed);
|
|
}
|
|
|
|
timings.sort_unstable();
|
|
let total = timings
|
|
.iter()
|
|
.fold(Duration::ZERO, |sum, value| sum + *value);
|
|
let mean_ns = total.as_nanos() / timings.len() as u128;
|
|
let min_ns = timings.first().map_or(0, Duration::as_nanos);
|
|
let max_ns = timings.last().map_or(0, Duration::as_nanos);
|
|
let p50_ns = percentile_ns(&timings, 50);
|
|
let p95_ns = percentile_ns(&timings, 95);
|
|
|
|
println!(
|
|
"{{\"kernel\":\"gamecult_geometry_csg\",\"mode\":\"{}\",\"scenario\":\"{}\",\"brushes\":{},\"iterations\":{},\"warmup_iterations\":{},\"mean_ns\":{},\"min_ns\":{},\"p50_ns\":{},\"p95_ns\":{},\"max_ns\":{},\"triangles\":{},\"fragments\":{},\"warnings\":{},\"candidate_pairs\":{},\"rejected_pairs\":{},\"reused_mesh\":{},\"used_checkpoint\":{},\"checkpoint_rebuild_from\":{},\"direct_dirty_rebuild\":{}}}",
|
|
mode.as_str(),
|
|
case.name,
|
|
case.brushes,
|
|
MEASURE_ITERS,
|
|
WARMUP_ITERS,
|
|
mean_ns,
|
|
min_ns,
|
|
p50_ns,
|
|
p95_ns,
|
|
max_ns,
|
|
triangles,
|
|
fragments,
|
|
warnings,
|
|
candidate_pairs,
|
|
rejected_pairs,
|
|
reused_mesh,
|
|
used_checkpoint,
|
|
checkpoint_rebuild_from.map_or_else(|| "null".to_string(), |index| index.to_string()),
|
|
direct_dirty_rebuild
|
|
);
|
|
}
|
|
|
|
fn nudge_tail_brush(assembler: &mut Assembler, iteration: usize) {
|
|
let Some(brush) = assembler.brushes().last().cloned() else {
|
|
return;
|
|
};
|
|
let phase = if iteration.is_multiple_of(2) {
|
|
0.0
|
|
} else {
|
|
0.01
|
|
};
|
|
match brush.primitive {
|
|
Primitive::Box { bounds } => {
|
|
let center = bounds.center();
|
|
let size = bounds.size() + Vec3::splat(phase);
|
|
assembler.set_brush_primitive(
|
|
brush.id,
|
|
Primitive::Box {
|
|
bounds: Aabb::from_center_size(center, size),
|
|
},
|
|
);
|
|
}
|
|
Primitive::OrientedBox {
|
|
center,
|
|
size,
|
|
rotation,
|
|
} => {
|
|
assembler.set_brush_primitive(
|
|
brush.id,
|
|
Primitive::OrientedBox {
|
|
center,
|
|
size: size + Vec3::splat(phase),
|
|
rotation,
|
|
},
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn percentile_ns(values: &[Duration], percentile: usize) -> u128 {
|
|
if values.is_empty() {
|
|
return 0;
|
|
}
|
|
let index = ((values.len() - 1) * percentile) / 100;
|
|
values[index].as_nanos()
|
|
}
|
|
|
|
fn run_lod_case(name: &'static str, query: DomainQuery) {
|
|
let fixture = ragnarok_column_fixture();
|
|
let warmup_cut = select_domain_cut(&fixture, &query);
|
|
let warmup_chunks = lower_selected_cut_chunks(&warmup_cut);
|
|
black_box(
|
|
warmup_chunks
|
|
.iter()
|
|
.map(|chunk| chunk.mesh.triangle_count())
|
|
.sum::<usize>(),
|
|
);
|
|
|
|
for _ in 0..WARMUP_ITERS {
|
|
let cut = select_domain_cut(&fixture, &query);
|
|
let chunks = lower_selected_cut_chunks(&cut);
|
|
black_box(
|
|
chunks
|
|
.iter()
|
|
.map(|chunk| chunk.mesh.triangle_count())
|
|
.sum::<usize>(),
|
|
);
|
|
}
|
|
|
|
let mut timings = Vec::with_capacity(MEASURE_ITERS);
|
|
let mut selected_nodes = 0;
|
|
let mut emitted_claims = 0;
|
|
let mut triangles = 0;
|
|
let mut collider_triangles = 0;
|
|
let mut brush_count = 0;
|
|
let mut candidate_pairs = 0;
|
|
let mut rejected_pairs = 0;
|
|
|
|
for _ in 0..MEASURE_ITERS {
|
|
let start = Instant::now();
|
|
let cut = select_domain_cut(&fixture, &query);
|
|
let chunks = lower_selected_cut_chunks(&cut);
|
|
let elapsed = start.elapsed();
|
|
selected_nodes = cut.selected_nodes.len();
|
|
emitted_claims = cut.emitted_claims.len();
|
|
triangles = chunks.iter().map(|chunk| chunk.mesh.triangle_count()).sum();
|
|
collider_triangles = chunks
|
|
.iter()
|
|
.map(|chunk| {
|
|
chunk
|
|
.collider_mesh
|
|
.as_ref()
|
|
.map_or(0, |mesh| mesh.triangle_count())
|
|
})
|
|
.sum();
|
|
brush_count = chunks.iter().map(|chunk| chunk.report.input_brushes).sum();
|
|
candidate_pairs = chunks
|
|
.iter()
|
|
.map(|chunk| chunk.report.candidate_pairs)
|
|
.sum();
|
|
rejected_pairs = chunks.iter().map(|chunk| chunk.report.rejected_pairs).sum();
|
|
black_box((
|
|
selected_nodes,
|
|
emitted_claims,
|
|
triangles,
|
|
collider_triangles,
|
|
brush_count,
|
|
candidate_pairs,
|
|
rejected_pairs,
|
|
));
|
|
timings.push(elapsed);
|
|
}
|
|
|
|
timings.sort_unstable();
|
|
let total = timings
|
|
.iter()
|
|
.fold(Duration::ZERO, |sum, value| sum + *value);
|
|
let mean_ns = total.as_nanos() / timings.len() as u128;
|
|
let min_ns = timings.first().map_or(0, Duration::as_nanos);
|
|
let max_ns = timings.last().map_or(0, Duration::as_nanos);
|
|
let p50_ns = percentile_ns(&timings, 50);
|
|
let p95_ns = percentile_ns(&timings, 95);
|
|
|
|
println!(
|
|
"{{\"kernel\":\"gamecult_geometry_csg\",\"mode\":\"lod_query\",\"scenario\":\"{}\",\"selected_nodes\":{},\"emitted_claims\":{},\"brushes\":{},\"iterations\":{},\"warmup_iterations\":{},\"mean_ns\":{},\"min_ns\":{},\"p50_ns\":{},\"p95_ns\":{},\"max_ns\":{},\"triangles\":{},\"collider_triangles\":{},\"candidate_pairs\":{},\"rejected_pairs\":{}}}",
|
|
name,
|
|
selected_nodes,
|
|
emitted_claims,
|
|
brush_count,
|
|
MEASURE_ITERS,
|
|
WARMUP_ITERS,
|
|
mean_ns,
|
|
min_ns,
|
|
p50_ns,
|
|
p95_ns,
|
|
max_ns,
|
|
triangles,
|
|
collider_triangles,
|
|
candidate_pairs,
|
|
rejected_pairs
|
|
);
|
|
}
|
|
|
|
fn ragnarok_lod_query(target_error: f32, triangle_budget: usize) -> DomainQuery {
|
|
DomainQuery {
|
|
camera_position: Vec3::new(36.0, -42.0, 30.0),
|
|
frustum: Aabb::from_center_size(Vec3::new(0.0, 0.0, 45.0), Vec3::splat(150.0)),
|
|
viewport_height_px: 1080.0,
|
|
vertical_fov_radians: std::f32::consts::FRAC_PI_3,
|
|
target_error,
|
|
triangle_budget,
|
|
collider_budget: triangle_budget,
|
|
semantic_filter: Vec::new(),
|
|
requested_chunk_keys: Vec::new(),
|
|
dirty_domain_keys: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn single_center_cut() -> Assembler {
|
|
let mut asm = Assembler::new();
|
|
asm.solid_box(
|
|
"source",
|
|
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
|
MaterialId(1),
|
|
);
|
|
asm.cut_box("void", Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)));
|
|
asm
|
|
}
|
|
|
|
fn room_grid_8x8_doors() -> Assembler {
|
|
let mut asm = Assembler::new();
|
|
let wall = MaterialId(1);
|
|
let floor = MaterialId(2);
|
|
let cell = 6.0;
|
|
|
|
for y in 0..8 {
|
|
for x in 0..8 {
|
|
let center = Vec3::new((x as f32 - 3.5) * cell, (y as f32 - 3.5) * cell, 0.0);
|
|
asm.solid_box(
|
|
format!("floor_{x}_{y}"),
|
|
Aabb::from_center_size(
|
|
center + Vec3::new(0.0, 0.0, -0.1),
|
|
Vec3::new(5.6, 5.6, 0.2),
|
|
),
|
|
floor,
|
|
);
|
|
asm.solid_box(
|
|
format!("north_wall_{x}_{y}"),
|
|
Aabb::from_center_size(
|
|
center + Vec3::new(0.0, 2.8, 1.5),
|
|
Vec3::new(5.6, 0.25, 3.0),
|
|
),
|
|
wall,
|
|
);
|
|
asm.cut_box(
|
|
format!("door_{x}_{y}"),
|
|
Aabb::from_center_size(center + Vec3::new(0.0, 2.8, 1.0), Vec3::new(1.2, 0.5, 2.0)),
|
|
);
|
|
}
|
|
}
|
|
|
|
asm
|
|
}
|
|
|
|
fn rotated_cut_stack_64() -> Assembler {
|
|
let mut asm = Assembler::new();
|
|
asm.solid_box(
|
|
"slab",
|
|
Aabb::from_center_size(Vec3::ZERO, Vec3::new(32.0, 32.0, 4.0)),
|
|
MaterialId(1),
|
|
);
|
|
|
|
for index in 0..64 {
|
|
let angle = index as f32 * 0.173;
|
|
let radius = 11.0 + (index % 7) as f32 * 0.35;
|
|
let center = Vec3::new(angle.cos() * radius, angle.sin() * radius, 0.0);
|
|
asm.cut_oriented_box(
|
|
format!("rotated_void_{index}"),
|
|
center,
|
|
Vec3::new(1.0 + (index % 3) as f32 * 0.3, 8.0, 5.0),
|
|
Quat::from_rotation_z(angle),
|
|
);
|
|
}
|
|
|
|
asm
|
|
}
|
|
|
|
fn common_box_chain_64() -> Assembler {
|
|
let mut asm = Assembler::new();
|
|
asm.solid_box(
|
|
"source",
|
|
Aabb::from_center_size(Vec3::ZERO, Vec3::new(32.0, 16.0, 8.0)),
|
|
MaterialId(1),
|
|
);
|
|
|
|
for index in 1..64 {
|
|
let t = index as f32 / 63.0;
|
|
let center = Vec3::new(
|
|
(t - 0.5) * 6.0,
|
|
(t * std::f32::consts::TAU).sin() * 1.5,
|
|
0.0,
|
|
);
|
|
asm.add_brush(
|
|
format!("common_{index}"),
|
|
BrushOp::Intersect,
|
|
Primitive::OrientedBox {
|
|
center,
|
|
size: Vec3::new(30.0 - t * 8.0, 14.0, 7.0),
|
|
rotation: Quat::from_rotation_z(t * 0.2),
|
|
},
|
|
MaterialId(1),
|
|
);
|
|
}
|
|
|
|
asm
|
|
}
|
|
|
|
fn distant_cutters_512() -> Assembler {
|
|
let mut asm = Assembler::new();
|
|
asm.solid_box(
|
|
"source",
|
|
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(8.0)),
|
|
MaterialId(1),
|
|
);
|
|
|
|
for index in 0..512 {
|
|
let row = index / 32;
|
|
let col = index % 32;
|
|
asm.cut_box(
|
|
format!("far_void_{index}"),
|
|
Aabb::from_center_size(
|
|
Vec3::new(1000.0 + col as f32 * 4.0, 1000.0 + row as f32 * 4.0, 0.0),
|
|
Vec3::splat(1.0),
|
|
),
|
|
);
|
|
}
|
|
|
|
asm
|
|
}
|
|
|
|
fn distant_oriented_cutters_128() -> Assembler {
|
|
let mut asm = Assembler::new();
|
|
asm.solid_box(
|
|
"source",
|
|
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(8.0)),
|
|
MaterialId(1),
|
|
);
|
|
|
|
for index in 0..128 {
|
|
let row = index / 16;
|
|
let col = index % 16;
|
|
asm.cut_oriented_box(
|
|
format!("far_oriented_void_{index}"),
|
|
Vec3::new(1000.0 + col as f32 * 4.0, 1000.0 + row as f32 * 4.0, 0.0),
|
|
Vec3::splat(1.0),
|
|
Quat::from_rotation_z(index as f32 * 0.037),
|
|
);
|
|
}
|
|
|
|
asm
|
|
}
|