From 38c969c57f108c82ce14356f68e22cfe0c9e1565 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 7 May 2026 01:50:49 +0100 Subject: [PATCH] Add CSG perf fixtures and polygon category splitting --- crates/vg_csg/examples/csg_perf_fixture.rs | 234 +++++++++++++++++++ crates/vg_csg/src/convex.rs | 151 ++++++++++++ crates/vg_csg/src/lib.rs | 2 +- crates/vg_csg/tests/realtime_csg_parity.rs | 23 ++ docs/research/csg-performance-fixtures.md | 72 ++++++ docs/research/realtime-csg-bevy-assembler.md | 19 +- experiments/generated/csg-perf-latest.jsonl | 6 + notes/fresh-workspace-handoff.md | 17 +- state/evidence.jsonl | 1 + state/map.yaml | 4 +- tools/run_csg_perf.ps1 | 41 ++++ 11 files changed, 556 insertions(+), 14 deletions(-) create mode 100644 crates/vg_csg/examples/csg_perf_fixture.rs create mode 100644 docs/research/csg-performance-fixtures.md create mode 100644 experiments/generated/csg-perf-latest.jsonl create mode 100644 tools/run_csg_perf.ps1 diff --git a/crates/vg_csg/examples/csg_perf_fixture.rs b/crates/vg_csg/examples/csg_perf_fixture.rs new file mode 100644 index 0000000..64225ba --- /dev/null +++ b/crates/vg_csg/examples/csg_perf_fixture.rs @@ -0,0 +1,234 @@ +use std::{ + hint::black_box, + time::{Duration, Instant}, +}; + +use bevy_math::{Quat, Vec3}; +use vg_csg::{Aabb, Assembler, BrushOp, MaterialId, Primitive}; + +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, + }, + ]; + + for case in cases { + run_case(case); + } +} + +#[derive(Clone, Copy)] +struct PerfCase { + name: &'static str, + brushes: usize, + build: fn() -> Assembler, +} + +fn run_case(case: PerfCase) { + let assembler = (case.build)(); + let warmup = assembler.build(); + black_box(warmup.mesh.triangle_count()); + + for _ in 0..WARMUP_ITERS { + let output = assembler.build(); + 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; + + for _ in 0..MEASURE_ITERS { + let start = Instant::now(); + let output = assembler.build(); + let elapsed = start.elapsed(); + triangles = output.mesh.triangle_count(); + fragments = output.report.emitted_convex_fragments; + warnings = output.report.warnings.len(); + black_box((triangles, fragments, warnings)); + 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\":\"vg_csg\",\"scenario\":\"{}\",\"brushes\":{},\"iterations\":{},\"warmup_iterations\":{},\"mean_ns\":{},\"min_ns\":{},\"p50_ns\":{},\"p95_ns\":{},\"max_ns\":{},\"triangles\":{},\"fragments\":{},\"warnings\":{}}}", + case.name, + case.brushes, + MEASURE_ITERS, + WARMUP_ITERS, + mean_ns, + min_ns, + p50_ns, + p95_ns, + max_ns, + triangles, + fragments, + warnings + ); +} + +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 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 +} diff --git a/crates/vg_csg/src/convex.rs b/crates/vg_csg/src/convex.rs index e892a3e..00bd83f 100644 --- a/crates/vg_csg/src/convex.rs +++ b/crates/vg_csg/src/convex.rs @@ -81,6 +81,14 @@ pub struct ConvexSolid { pub bounds: Aabb, } +#[derive(Clone, Debug, Default)] +pub struct CategorizedPolygons { + pub inside: Vec, + pub outside: Vec, + pub aligned: Vec, + pub reverse_aligned: Vec, +} + impl ConvexSolid { pub fn from_aabb(bounds: Aabb, material: MaterialId) -> Self { Self::box_from_center_size(bounds.center(), bounds.size(), Quat::IDENTITY, material) @@ -209,6 +217,42 @@ impl ConvexSolid { } } + pub fn categorize_polygons_against(&self, cutter: &Self) -> CategorizedPolygons { + let mut categorized = CategorizedPolygons::default(); + + for polygon in &self.polygons { + let mut pieces = vec![CategorizationPiece { + polygon: polygon.clone(), + aligned_category: None, + }]; + + for plane in &cutter.clip_planes { + let mut inside_pieces = Vec::new(); + for piece in pieces { + classify_piece_against_plane( + piece, + *plane, + &mut inside_pieces, + &mut categorized, + ); + } + pieces = inside_pieces; + } + + for piece in pieces { + match piece.aligned_category { + Some(PolygonCategory::Aligned) => categorized.aligned.push(piece.polygon), + Some(PolygonCategory::ReverseAligned) => { + categorized.reverse_aligned.push(piece.polygon); + } + _ => categorized.inside.push(piece.polygon), + } + } + } + + categorized + } + fn split(&self, plane: Plane) -> SplitResult { let mut saw_front = false; let mut saw_back = false; @@ -276,12 +320,84 @@ enum SplitResult { Coplanar(ConvexSolid), } +struct CategorizationPiece { + polygon: ConvexPolygon, + aligned_category: Option, +} + #[derive(Clone, Copy)] enum KeepSide { Front, Back, } +fn classify_piece_against_plane( + mut piece: CategorizationPiece, + plane: Plane, + inside_pieces: &mut Vec, + categorized: &mut CategorizedPolygons, +) { + let piece_plane = piece + .polygon + .vertices + .first() + .map(|point| Plane::from_point_normal(*point, piece.polygon.normal)); + + if piece_plane.is_some_and(|piece_plane| piece_plane.is_coplanar_with(plane)) { + piece.aligned_category = Some(if piece.polygon.normal.dot(plane.normal) >= 0.0 { + PolygonCategory::Aligned + } else { + PolygonCategory::ReverseAligned + }); + inside_pieces.push(piece); + return; + } + + let mut saw_front = false; + let mut saw_back = false; + for vertex in &piece.polygon.vertices { + let distance = plane.signed_distance(*vertex); + saw_front |= distance > EPSILON; + saw_back |= distance < -EPSILON; + } + + match (saw_front, saw_back) { + (true, false) => { + let mut polygon = piece.polygon; + polygon.category = PolygonCategory::Outside; + categorized.outside.push(polygon); + } + (false, true) | (false, false) => inside_pieces.push(piece), + (true, true) => { + let mut cap_points = Vec::new(); + let outside = clip_polygon( + &piece.polygon.vertices, + plane, + KeepSide::Front, + &mut cap_points, + ); + if outside.len() >= 3 { + let mut outside_polygon = piece.polygon.with_vertices(outside); + outside_polygon.category = PolygonCategory::Outside; + categorized.outside.push(outside_polygon); + } + + let inside = clip_polygon( + &piece.polygon.vertices, + plane, + KeepSide::Back, + &mut cap_points, + ); + if inside.len() >= 3 { + inside_pieces.push(CategorizationPiece { + polygon: piece.polygon.with_vertices(inside), + aligned_category: piece.aligned_category, + }); + } + } + } +} + fn clip_polygon( vertices: &[Vec3], plane: Plane, @@ -518,6 +634,41 @@ mod tests { assert!(!common.bounds.contains_point(Vec3::new(-2.0, 0.0, 0.0))); } + #[test] + fn categorize_crossing_coplanar_face_splits_visible_categories() { + let source = ConvexSolid::box_from_center_size( + Vec3::ZERO, + Vec3::splat(4.0), + Quat::IDENTITY, + MaterialId(1), + ); + let cutter = ConvexSolid::box_from_center_size( + Vec3::new(0.0, 0.0, 1.0), + Vec3::new(2.0, 2.0, 2.0), + Quat::IDENTITY, + MaterialId(0), + ); + + let categorized = source.categorize_polygons_against(&cutter); + + assert_eq!(categorized.aligned.len(), 1); + assert!(categorized.outside.len() > 6); + assert!(categorized.inside.is_empty()); + assert!(categorized.reverse_aligned.is_empty()); + assert!( + categorized + .aligned + .iter() + .all(|polygon| polygon.category == PolygonCategory::Aligned) + ); + assert!( + categorized + .outside + .iter() + .all(|polygon| polygon.category == PolygonCategory::Outside) + ); + } + #[test] fn subtract_rotated_box_creates_non_axis_aligned_faces() { let source = ConvexSolid::box_from_center_size( diff --git a/crates/vg_csg/src/lib.rs b/crates/vg_csg/src/lib.rs index f1c2807..48fb597 100644 --- a/crates/vg_csg/src/lib.rs +++ b/crates/vg_csg/src/lib.rs @@ -16,7 +16,7 @@ mod tree; pub use assembler::{Assembler, BuildOutput, BuildReport, BuildWarning}; pub use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, PolygonCategory, Primitive}; -pub use convex::{ConvexPolygon, ConvexSolid, Plane}; +pub use convex::{CategorizedPolygons, ConvexPolygon, ConvexSolid, Plane}; pub use dsl::LevelDsl; pub use mesh::TriangleMesh; pub use primitives::{ diff --git a/crates/vg_csg/tests/realtime_csg_parity.rs b/crates/vg_csg/tests/realtime_csg_parity.rs index 7079802..fde5893 100644 --- a/crates/vg_csg/tests/realtime_csg_parity.rs +++ b/crates/vg_csg/tests/realtime_csg_parity.rs @@ -141,3 +141,26 @@ fn parity_outer_box_polygons_classify_as_outside_inner_box() { .all(|polygon| polygon.category == PolygonCategory::Outside) ); } + +#[test] +fn parity_crossing_polygons_split_before_final_category() { + let source = ConvexSolid::box_from_center_size( + Vec3::ZERO, + Vec3::splat(4.0), + Quat::IDENTITY, + MaterialId(1), + ); + let cutter = ConvexSolid::box_from_center_size( + Vec3::new(0.0, 0.0, 1.0), + Vec3::new(2.0, 2.0, 2.0), + Quat::IDENTITY, + MaterialId(0), + ); + + let categorized = source.categorize_polygons_against(&cutter); + + assert_eq!(categorized.aligned.len(), 1); + assert!(categorized.outside.len() > source.polygons.len()); + assert_eq!(categorized.inside.len(), 0); + assert_eq!(categorized.reverse_aligned.len(), 0); +} diff --git a/docs/research/csg-performance-fixtures.md b/docs/research/csg-performance-fixtures.md new file mode 100644 index 0000000..2bf9bf7 --- /dev/null +++ b/docs/research/csg-performance-fixtures.md @@ -0,0 +1,72 @@ +# CSG Performance Fixtures + +The timing surface is a fixture, not a victory poster. + +Run our current kernel: + +```powershell +.\tools\run_csg_perf.ps1 +``` + +The script builds `vg_csg` in release mode and writes JSONL to +`experiments/generated/csg-perf-latest.jsonl`. Each record carries: + +- `kernel` +- `scenario` +- `brushes` +- warmup and measured iteration counts +- `mean_ns`, `min_ns`, `p50_ns`, `p95_ns`, `max_ns` +- emitted triangle, fragment, and warning counts + +## Scenarios + +- `single_center_cut`: the current exact centered box-subtraction parity seam. +- `room_grid_8x8_doors`: repeated additive floors/walls and repeated door + voids, matching the level-editing workload we care about. +- `rotated_cut_stack_64`: one large slab cut by many oriented boxes. +- `common_box_chain_64`: repeated convex intersections, exercising `Common` + branch behavior through the ordered assembler. +- `distant_cutters_512`: many irrelevant cutters, proving bounds early-outs + stay cheap. + +## Reference Kernel Slot + +The same wrapper can append reference timings: + +```powershell +$env:VIBEGEOMETRY_REFERENCE_CSG_PERF = "path\to\reference-perf.exe" +.\tools\run_csg_perf.ps1 +``` + +or: + +```powershell +.\tools\run_csg_perf.ps1 -ReferenceCommand "path\to\reference-perf.exe" +``` + +The reference executable must emit JSONL with the same scenario names. Until +that harness exists, the script appends a `kernel=reference,status=missing` +record instead of faking a comparison. Sad, but hygienic. + +The public `LogicalError/Realtime-CSG-demo` source already exposes the relevant +algorithmic surface: half-edge control meshes, polygon bounds, visible flags, +inside/outside/aligned/reverse-aligned categories, crossing polygon splits, and +logical routing tables. A reference benchmark should call that public demo +kernel headlessly or through a small public-source harness, not the closed Unity +native plugin. + +## Latest Local Baseline + +Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1`: + +```jsonl +{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":34295,"min_ns":21200,"p50_ns":35400,"p95_ns":43000,"max_ns":52500,"triangles":72,"fragments":6,"warnings":0} +{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":11564459,"min_ns":4988300,"p50_ns":9626400,"p95_ns":20738900,"max_ns":33797500,"triangles":3072,"fragments":256,"warnings":0} +{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":12646675,"min_ns":8271900,"p50_ns":12512900,"p95_ns":16605400,"max_ns":19622900,"triangles":3404,"fragments":280,"warnings":0} +{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":829393,"min_ns":609900,"p50_ns":751800,"p95_ns":1082100,"max_ns":1478800,"triangles":45,"fragments":1,"warnings":0} +{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":534860,"min_ns":461700,"p50_ns":469400,"p95_ns":754700,"max_ns":1031900,"triangles":12,"fragments":1,"warnings":0} +{"status":"missing","reason":"Set VIBEGEOMETRY_REFERENCE_CSG_PERF or pass -ReferenceCommand with an executable that emits the same JSONL scenario records.","kernel":"reference"} +``` + +Read these as smoke timings, not final benchmark gospel. The fixtures are stable +enough to catch regressions and compare against a reference harness once wired. diff --git a/docs/research/realtime-csg-bevy-assembler.md b/docs/research/realtime-csg-bevy-assembler.md index 809b8a9..2742bca 100644 --- a/docs/research/realtime-csg-bevy-assembler.md +++ b/docs/research/realtime-csg-bevy-assembler.md @@ -75,10 +75,15 @@ angled box cuts, real `Common` branch output for convex boxes, and gives us the correct shape for a future general convex brush kernel. Polygons now carry the first routing metadata the mature kernel needs: -category, visibility, reversal, and bounds. Whole polygons can already be -classified as inside, outside, aligned, or reverse-aligned against a convex -brush when they do not cross cutter planes. That is deliberately not sold as -the full router yet. It is the labeled wire we will plug the splitter into. +category, visibility, reversal, and bounds. Source polygons can be classified +as inside, outside, aligned, or reverse-aligned against a convex brush. When a +polygon crosses a cutter plane, it is split first and each piece is routed to a +category bucket. That matches the public demo's important shape: split crossing +polygons before final category decisions. + +That is deliberately not sold as full parity yet. The current assembler still +emits meshes through convex fragment decomposition. The next parity step is to +make those category buckets drive boolean-tree mesh emission directly. The important doctrinal adjustment from the public demo and blog series is that real-time CSG should think in **classification**, not only carving. A polygon is @@ -160,6 +165,10 @@ kernel: split crossing polygons, route classified source polygons through boolean branches, preserve material/surface metadata, and then add a BVH or spatial hash so cutter cost does not scale like a punishment. +Performance fixtures live in `docs/research/csg-performance-fixtures.md` and +`tools/run_csg_perf.ps1`. They time our release fixture now and reserve a JSONL +slot for a public-demo/reference harness when one is available. + ## Limits `vg_csg` is not yet a full RealtimeCSG replacement. @@ -172,6 +181,8 @@ spatial hash so cutter cost does not scale like a punishment. - current subtraction emits split fragments with generated cap polygons; the target kernel should move toward article/demo-style category routing over brush polygons for speed and cleaner material behavior +- no reference timing executable is wired yet; the perf wrapper records this + explicitly instead of inventing comparison numbers Those limits are explicit because invisible ambition is how you get haunted by your own abstractions. The useful loop exists now: script brushes, assemble, diff --git a/experiments/generated/csg-perf-latest.jsonl b/experiments/generated/csg-perf-latest.jsonl new file mode 100644 index 0000000..8c647ba --- /dev/null +++ b/experiments/generated/csg-perf-latest.jsonl @@ -0,0 +1,6 @@ +{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":34295,"min_ns":21200,"p50_ns":35400,"p95_ns":43000,"max_ns":52500,"triangles":72,"fragments":6,"warnings":0} +{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":11564459,"min_ns":4988300,"p50_ns":9626400,"p95_ns":20738900,"max_ns":33797500,"triangles":3072,"fragments":256,"warnings":0} +{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":12646675,"min_ns":8271900,"p50_ns":12512900,"p95_ns":16605400,"max_ns":19622900,"triangles":3404,"fragments":280,"warnings":0} +{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":829393,"min_ns":609900,"p50_ns":751800,"p95_ns":1082100,"max_ns":1478800,"triangles":45,"fragments":1,"warnings":0} +{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":534860,"min_ns":461700,"p50_ns":469400,"p95_ns":754700,"max_ns":1031900,"triangles":12,"fragments":1,"warnings":0} +{"status":"missing","reason":"Set VIBEGEOMETRY_REFERENCE_CSG_PERF or pass -ReferenceCommand with an executable that emits the same JSONL scenario records.","kernel":"reference"} diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 7f23abe..038f676 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -46,9 +46,11 @@ API. Sander van Rossen's older public `LogicalError/Realtime-CSG-demo` and blog series are now the preferred clean research inputs. Current `vg_csg`: public CSG operation/branch/tree/brush handle surface, child and operation mutation, ordered assembler backend, exact AABB and oriented-box subtraction and -intersection via convex plane splitting, polygon category/visibility/reversal -metadata, primitive/solid/tree bounds, additive `CylinderZ`, `DomeCapZ`, and -`FloretArm` primitives, Bevy-math mesh buffers, tests, clippy, and examples. +intersection via convex plane splitting, crossing polygon classification into +inside/outside/aligned/reverse-aligned buckets, polygon +category/visibility/reversal metadata, primitive/solid/tree bounds, additive +`CylinderZ`, `DomeCapZ`, and `FloretArm` primitives, release perf fixtures, +Bevy-math mesh buffers, tests, clippy, and examples. Research note: `docs/research/realtime-csg-bevy-assembler.md`. Where `vg_csg` overlaps public RealtimeCSG/demo behavior, add exact observable parity fixtures instead of approximate plausibility tests. @@ -67,10 +69,11 @@ Repo-local Geometry Script clone notes live in missing. The current next action is to continue replacing the ordered assembler backend -with the category-router kernel behind the existing CSG tree API: split crossing -polygons into classifiable pieces, route inside/outside/aligned/reverse-aligned -categories through boolean branches, and preserve material/surface metadata -while keeping the current parity fixtures green. +with the category-router kernel behind the existing CSG tree API: make the +inside/outside/aligned/reverse-aligned category buckets drive mesh emission +through boolean branch routing, preserve material/surface metadata, and wire a +public-demo/reference performance harness for the existing JSONL timing +fixtures. ## Important Invariants diff --git a/state/evidence.jsonl b/state/evidence.jsonl index c12b7f4..425bf97 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -56,3 +56,4 @@ {"ts": "2026-05-07T00:21:11+00:00", "type": "implementation", "status": "accepted", "note": "Added exact parity testing doctrine and first vg_csg parity fixtures for public RealtimeCSG-overlapping seams: polygon category vocabulary, six-plane/six-polygon box brush counts, and six surviving regions for centered box subtraction. Future matching API surface should get countable parity fixtures, not approximate plausibility checks."} {"ts": "2026-05-07T00:31:59+00:00", "type": "implementation", "status": "accepted", "note": "Expanded the Rust API toward RealtimeCSG shape: added CsgTreeArena, CsgTree/CsgTreeBranch/CsgTreeBrush handles, CsgOperationType ordinals, CsgBranchOp vocabulary, branch child/operation mutation, brush operation mutation, tree-to-assembler compilation, grammar ClaimTree CSG tree emission, and parity fixtures for operation ordinals, branch vocabulary, and handle topology."} {"ts": "2026-05-07T00:41:39+00:00", "type": "implementation", "status": "accepted", "note": "Advanced vg_csg toward the category-router kernel: ConvexPolygon now carries category/visibility/reversal/bounds metadata, primitives/solids/tree nodes expose bounds for early rejection, whole non-crossing polygons classify as inside/outside/aligned/reverse-aligned against convex cutters, convex intersection now powers Intersect brushes and Common branches, and parity/workspace tests keep the RealtimeCSG-shaped API seams honest."} +{"ts": "2026-05-07T00:50:13+00:00", "type": "implementation", "status": "accepted", "note": "Added CSG performance fixtures and advanced parity surface: vg_csg now has a release JSONL timing fixture with room-grid, rotated-cut, common-chain, distant-cutter, and centered-cut scenarios; tools/run_csg_perf.ps1 records our timings and an explicit missing reference slot; ConvexSolid can now split crossing polygons into inside/outside/aligned/reverse-aligned category buckets, matching the public demo shape before boolean routing."} diff --git a/state/map.yaml b/state/map.yaml index daaf38c..ccd7f38 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -110,8 +110,8 @@ data_flow: - If state grows faster than understanding, cut it back. current_status: - summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus from-scratch Aetheria Bloom and Lucent tether habitat builds. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; spin-gravity habitats need ground planes orthogonal to their tether/rotation axis; utility petals, radiators, and solar arrays belong outside pressure glass and may need Citadel-scale presence; large habitat parts need visible load paths across coordinate frames; organic city maps need street fields, irregular parcels, parks, and land-use gradients rather than grids; dense detail must be batched; institutional spaces should model power as surfaces, rails, overlays, thresholds, shader behavior, and camera evidence; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom and Lucent builds. The Rust crate `vg_csg` now has a RealtimeCSG-shaped API skeleton plus a convex-brush stepping-stone kernel: CSG operation ordinals, branch vocabulary, tree/branch/brush handles, child and operation mutation, exact parity fixtures, ordered assembler compilation, AABB/oriented-box subtraction/intersection via convex plane splitting, whole-polygon category metadata, primitive/solid/tree bounds, additive cylinder/dome/floret primitives, mesh-array output, tests, clippy, and examples. The `vg_grammar` crate is the Dream Machine authoring layer: semantic solid/void claims with tags compile into flat `vg_csg` brushes or generated CSG tree intent; it supports local frames, oriented claims, deterministic seeded variation, rule composition, and generated gallery chains. Public RealtimeCSG demo/source research points the target away from fragment carving and toward faster polygon classification through boolean trees. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. - next_action: Continue replacing the ordered assembler backend with the category-router kernel behind the existing CSG tree API: split crossing polygons into classifiable pieces, route inside/outside/aligned/reverse-aligned categories through boolean branches, and preserve material/surface metadata while keeping current parity fixtures green. + summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus from-scratch Aetheria Bloom and Lucent tether habitat builds. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; spin-gravity habitats need ground planes orthogonal to their tether/rotation axis; utility petals, radiators, and solar arrays belong outside pressure glass and may need Citadel-scale presence; large habitat parts need visible load paths across coordinate frames; organic city maps need street fields, irregular parcels, parks, and land-use gradients rather than grids; dense detail must be batched; institutional spaces should model power as surfaces, rails, overlays, thresholds, shader behavior, and camera evidence; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom and Lucent builds. The Rust crate `vg_csg` now has a RealtimeCSG-shaped API skeleton plus a convex-brush stepping-stone kernel: CSG operation ordinals, branch vocabulary, tree/branch/brush handles, child and operation mutation, exact parity fixtures, ordered assembler compilation, AABB/oriented-box subtraction/intersection via convex plane splitting, crossing polygon category buckets, primitive/solid/tree bounds, additive cylinder/dome/floret primitives, JSONL performance fixtures, mesh-array output, tests, clippy, and examples. The `vg_grammar` crate is the Dream Machine authoring layer: semantic solid/void claims with tags compile into flat `vg_csg` brushes or generated CSG tree intent; it supports local frames, oriented claims, deterministic seeded variation, rule composition, and generated gallery chains. Public RealtimeCSG demo/source research points the target away from fragment carving and toward faster polygon classification through boolean trees. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. + next_action: Continue replacing the ordered assembler backend with the category-router kernel behind the existing CSG tree API: make inside/outside/aligned/reverse-aligned category buckets drive mesh emission through boolean branch routing, preserve material/surface metadata, and wire a public-demo/reference performance harness for the JSONL timing fixtures. remotes: vibegeometry: https://github.com/GameCult/VibeGeometry geometry_script_fork: https://github.com/GameCult/geometry-script diff --git a/tools/run_csg_perf.ps1 b/tools/run_csg_perf.ps1 new file mode 100644 index 0000000..ea30592 --- /dev/null +++ b/tools/run_csg_perf.ps1 @@ -0,0 +1,41 @@ +param( + [string]$OutputPath = ".\experiments\generated\csg-perf-latest.jsonl", + [string]$ReferenceCommand = $env:VIBEGEOMETRY_REFERENCE_CSG_PERF +) + +$ErrorActionPreference = "Stop" + +$root = Split-Path -Parent $PSScriptRoot +$resolvedOutput = Join-Path $root $OutputPath +$outputDir = Split-Path -Parent $resolvedOutput +New-Item -ItemType Directory -Force -Path $outputDir | Out-Null + +Push-Location $root +try { + cargo build --release -p vg_csg --example csg_perf_fixture | Out-Host + + $exe = Join-Path $root "target\release\examples\csg_perf_fixture.exe" + if (!(Test-Path $exe)) { + throw "Missing vg_csg perf fixture executable: $exe" + } + + & $exe | Set-Content -Path $resolvedOutput -Encoding utf8 + + if ($ReferenceCommand) { + $referenceOutput = & cmd /c $ReferenceCommand + foreach ($line in $referenceOutput) { + Add-Content -Path $resolvedOutput -Value $line -Encoding utf8 + } + } else { + $missing = @{ + kernel = "reference" + status = "missing" + reason = "Set VIBEGEOMETRY_REFERENCE_CSG_PERF or pass -ReferenceCommand with an executable that emits the same JSONL scenario records." + } | ConvertTo-Json -Compress + Add-Content -Path $resolvedOutput -Value $missing -Encoding utf8 + } + + Get-Content -Path $resolvedOutput +} finally { + Pop-Location +}