Expose dirty CSG perf mode
This commit is contained in:
parent
c793da0359
commit
76764870e9
4 changed files with 121 additions and 22 deletions
|
|
@ -90,19 +90,26 @@ the performance target.
|
|||
Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp`:
|
||||
|
||||
```jsonl
|
||||
{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":743,"min_ns":600,"p50_ns":700,"p95_ns":800,"max_ns":1300,"triangles":72,"fragments":6,"warnings":0}
|
||||
{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":106331,"min_ns":78000,"p50_ns":89500,"p95_ns":161600,"max_ns":328900,"triangles":3072,"fragments":256,"warnings":0}
|
||||
{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":141673,"min_ns":117600,"p50_ns":128900,"p95_ns":185900,"max_ns":296900,"triangles":3404,"fragments":280,"warnings":0}
|
||||
{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":940,"min_ns":800,"p50_ns":900,"p95_ns":1000,"max_ns":1000,"triangles":45,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":717,"min_ns":500,"p50_ns":500,"p95_ns":600,"max_ns":12000,"triangles":12,"fragments":1,"warnings":0}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":9500,"min_ns":6600,"p50_ns":7800,"p95_ns":13500,"max_ns":24900,"triangles":72,"vertices":144,"mesh_descriptions":3}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":1167600,"min_ns":493200,"p50_ns":944700,"p95_ns":2337700,"max_ns":3141500,"triangles":12800,"vertices":20992,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":272400,"min_ns":122800,"p50_ns":200600,"p95_ns":519800,"max_ns":1843100,"triangles":3498,"vertices":5102,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":63600,"min_ns":44900,"p50_ns":59100,"p95_ns":88000,"max_ns":259700,"triangles":1536,"vertices":2412,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":573000,"min_ns":316200,"p50_ns":423000,"p95_ns":1235000,"max_ns":2374500,"triangles":6180,"vertices":12360,"mesh_descriptions":4}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":948,"min_ns":800,"p50_ns":900,"p95_ns":1100,"max_ns":1200,"triangles":72,"fragments":6,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":6123,"min_ns":4300,"p50_ns":5500,"p95_ns":6000,"max_ns":44700,"triangles":72,"fragments":6,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":85546,"min_ns":66100,"p50_ns":84900,"p95_ns":103300,"max_ns":114800,"triangles":3072,"fragments":256,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":322628,"min_ns":231400,"p50_ns":301600,"p95_ns":428500,"max_ns":841700,"triangles":3072,"fragments":256,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":162057,"min_ns":107500,"p50_ns":139600,"p95_ns":271700,"max_ns":518200,"triangles":3404,"fragments":280,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":4835134,"min_ns":2799000,"p50_ns":4260700,"p95_ns":7080200,"max_ns":10841300,"triangles":3404,"fragments":280,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":937,"min_ns":700,"p50_ns":900,"p95_ns":1100,"max_ns":9700,"triangles":45,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":700876,"min_ns":396000,"p50_ns":662400,"p95_ns":963600,"max_ns":1582700,"triangles":45,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"stable","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":278,"min_ns":200,"p50_ns":300,"p95_ns":300,"max_ns":400,"triangles":12,"fragments":1,"warnings":0}
|
||||
{"kernel":"vg_csg","mode":"dirty","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":4762,"min_ns":4500,"p50_ns":4700,"p95_ns":5000,"max_ns":5200,"triangles":12,"fragments":1,"warnings":0}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":7900,"min_ns":5900,"p50_ns":6300,"p95_ns":12100,"max_ns":16400,"triangles":72,"vertices":144,"mesh_descriptions":3}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":775500,"min_ns":376100,"p50_ns":670500,"p95_ns":1451600,"max_ns":2108000,"triangles":12800,"vertices":20992,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":154300,"min_ns":106500,"p50_ns":132900,"p95_ns":240000,"max_ns":380100,"triangles":3498,"vertices":5102,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":64000,"min_ns":45000,"p50_ns":61400,"p95_ns":90500,"max_ns":224800,"triangles":1536,"vertices":2412,"mesh_descriptions":4}
|
||||
{"kernel":"realtime_csg_cpp","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":740400,"min_ns":340900,"p50_ns":641200,"p95_ns":1403100,"max_ns":2258600,"triangles":6180,"vertices":12360,"mesh_descriptions":4}
|
||||
```
|
||||
|
||||
Read these as smoke timings, not final benchmark gospel. Stable unchanged
|
||||
output is no longer embarrassing. Dirty rebuilds for dense rotated subtraction
|
||||
still need the category-router kernel; cached output is a guardrail, not a
|
||||
replacement for the real engine.
|
||||
output is no longer embarrassing. Dirty rebuilds are now explicit: box-heavy
|
||||
dirty paths are respectable, distant bounds rejection is excellent, and dense
|
||||
rotated subtraction is still the open wound. The category-router kernel remains
|
||||
the next architectural move; cached output is a guardrail, not a replacement
|
||||
for the real engine.
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ fn main() {
|
|||
];
|
||||
|
||||
for case in cases {
|
||||
run_case(case);
|
||||
run_case(case, PerfMode::Stable);
|
||||
run_case(case, PerfMode::Dirty);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,13 +51,35 @@ struct PerfCase {
|
|||
build: fn() -> Assembler,
|
||||
}
|
||||
|
||||
fn run_case(case: PerfCase) {
|
||||
#[derive(Clone, Copy)]
|
||||
enum PerfMode {
|
||||
Stable,
|
||||
Dirty,
|
||||
}
|
||||
|
||||
impl PerfMode {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Stable => "stable",
|
||||
Self::Dirty => "dirty",
|
||||
}
|
||||
}
|
||||
|
||||
fn build(self, assembler: &Assembler) -> vg_csg::BuildOutput {
|
||||
match self {
|
||||
Self::Stable => assembler.build(),
|
||||
Self::Dirty => assembler.rebuild(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_case(case: PerfCase, mode: PerfMode) {
|
||||
let assembler = (case.build)();
|
||||
let warmup = assembler.build();
|
||||
let warmup = mode.build(&assembler);
|
||||
black_box(warmup.mesh.triangle_count());
|
||||
|
||||
for _ in 0..WARMUP_ITERS {
|
||||
let output = assembler.build();
|
||||
let output = mode.build(&assembler);
|
||||
black_box(output.mesh.triangle_count());
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +90,7 @@ fn run_case(case: PerfCase) {
|
|||
|
||||
for _ in 0..MEASURE_ITERS {
|
||||
let start = Instant::now();
|
||||
let output = assembler.build();
|
||||
let output = mode.build(&assembler);
|
||||
let elapsed = start.elapsed();
|
||||
triangles = output.mesh.triangle_count();
|
||||
fragments = output.report.emitted_convex_fragments;
|
||||
|
|
@ -87,7 +110,8 @@ fn run_case(case: PerfCase) {
|
|||
let p95_ns = percentile_ns(&timings, 95);
|
||||
|
||||
println!(
|
||||
"{{\"kernel\":\"vg_csg\",\"scenario\":\"{}\",\"brushes\":{},\"iterations\":{},\"warmup_iterations\":{},\"mean_ns\":{},\"min_ns\":{},\"p50_ns\":{},\"p95_ns\":{},\"max_ns\":{},\"triangles\":{},\"fragments\":{},\"warnings\":{}}}",
|
||||
"{{\"kernel\":\"vg_csg\",\"mode\":\"{}\",\"scenario\":\"{}\",\"brushes\":{},\"iterations\":{},\"warmup_iterations\":{},\"mean_ns\":{},\"min_ns\":{},\"p50_ns\":{},\"p95_ns\":{},\"max_ns\":{},\"triangles\":{},\"fragments\":{},\"warnings\":{}}}",
|
||||
mode.as_str(),
|
||||
case.name,
|
||||
case.brushes,
|
||||
MEASURE_ITERS,
|
||||
|
|
|
|||
|
|
@ -176,6 +176,10 @@ impl Assembler {
|
|||
output
|
||||
}
|
||||
|
||||
pub fn rebuild(&self) -> BuildOutput {
|
||||
self.build_uncached()
|
||||
}
|
||||
|
||||
fn build_uncached(&self) -> BuildOutput {
|
||||
if self
|
||||
.compiled
|
||||
|
|
@ -474,9 +478,10 @@ fn subtract_from_solids(solids: Vec<ConvexSolid>, cutter: &ConvexSolid) -> Vec<C
|
|||
fn intersect_solids(solids: Vec<ConvexSolid>, cutter: &ConvexSolid) -> Vec<ConvexSolid> {
|
||||
let mut out = Vec::with_capacity(solids.len());
|
||||
for solid in solids {
|
||||
if solid.bounds.intersects(cutter.bounds)
|
||||
&& let Some(fragment) = solid.intersect_convex_owned(cutter)
|
||||
{
|
||||
if !solid.bounds.intersects(cutter.bounds) {
|
||||
continue;
|
||||
}
|
||||
if let Some(fragment) = solid.intersect_convex_owned(cutter) {
|
||||
out.push(fragment);
|
||||
}
|
||||
}
|
||||
|
|
@ -587,4 +592,37 @@ mod tests {
|
|||
assert_eq!(output.report.warnings.len(), 0);
|
||||
assert_eq!(output.mesh.triangle_count(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_build_and_dirty_rebuild_emit_same_output() {
|
||||
let mut asm = Assembler::new();
|
||||
asm.solid_box(
|
||||
"slab",
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::new(8.0, 8.0, 2.0)),
|
||||
MaterialId(1),
|
||||
);
|
||||
asm.cut_box(
|
||||
"center void",
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)),
|
||||
);
|
||||
asm.cut_oriented_box(
|
||||
"angled void",
|
||||
Vec3::new(1.0, 1.0, 0.0),
|
||||
Vec3::new(1.25, 4.0, 2.5),
|
||||
bevy_math::Quat::from_rotation_z(0.35),
|
||||
);
|
||||
|
||||
let stable = asm.build();
|
||||
let cached = asm.build();
|
||||
let dirty = asm.rebuild();
|
||||
|
||||
assert_eq!(stable.report.emitted_convex_fragments, dirty.report.emitted_convex_fragments);
|
||||
assert_eq!(stable.report.warnings, dirty.report.warnings);
|
||||
assert_eq!(stable.mesh.triangle_count(), dirty.mesh.triangle_count());
|
||||
assert_eq!(stable.mesh.vertex_count(), dirty.mesh.vertex_count());
|
||||
assert_eq!(stable.mesh.positions, dirty.mesh.positions);
|
||||
assert_eq!(stable.mesh.indices, dirty.mesh.indices);
|
||||
assert_eq!(cached.mesh.positions, stable.mesh.positions);
|
||||
assert_eq!(cached.mesh.indices, stable.mesh.indices);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,13 @@ impl ConvexSolid {
|
|||
}
|
||||
|
||||
pub fn intersect_convex_owned(self, cutter: &Self) -> Option<Self> {
|
||||
if cutter.contains_solid(&self) {
|
||||
return Some(self);
|
||||
}
|
||||
if self.contains_solid(cutter) {
|
||||
return Some(cutter.clone_with_material(self.material));
|
||||
}
|
||||
|
||||
let mut remainder = self;
|
||||
|
||||
for plane in &cutter.clip_planes {
|
||||
|
|
@ -190,6 +197,29 @@ impl ConvexSolid {
|
|||
Some(remainder)
|
||||
}
|
||||
|
||||
pub fn contains_point(&self, point: Vec3) -> bool {
|
||||
self.clip_planes
|
||||
.iter()
|
||||
.all(|plane| plane.signed_distance(point) <= EPSILON)
|
||||
}
|
||||
|
||||
pub fn contains_solid(&self, other: &Self) -> bool {
|
||||
other
|
||||
.polygons
|
||||
.iter()
|
||||
.flat_map(|polygon| polygon.vertices.iter())
|
||||
.all(|point| self.contains_point(*point))
|
||||
}
|
||||
|
||||
pub fn clone_with_material(&self, material: MaterialId) -> Self {
|
||||
let mut solid = self.clone();
|
||||
solid.material = material;
|
||||
for polygon in &mut solid.polygons {
|
||||
polygon.material = material;
|
||||
}
|
||||
solid
|
||||
}
|
||||
|
||||
pub fn append_to_mesh(&self, mesh: &mut TriangleMesh) {
|
||||
for polygon in &self.polygons {
|
||||
if !polygon.visible || polygon.vertices.len() < 3 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue