From 492f343b16daf75725a6b1814b699bf557650bfa Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 7 May 2026 15:50:58 +0100 Subject: [PATCH] Correct CSG dirty rebuild baselines --- crates/vg_csg/examples/csg_perf_fixture.rs | 27 +++++++++++++++++ crates/vg_csg/src/assembler.rs | 35 ++++++++++++++++------ docs/research/csg-performance-fixtures.md | 15 ++++++++++ notes/fresh-workspace-handoff.md | 5 +++- state/evidence.jsonl | 1 + state/map.yaml | 2 +- 6 files changed, 74 insertions(+), 11 deletions(-) diff --git a/crates/vg_csg/examples/csg_perf_fixture.rs b/crates/vg_csg/examples/csg_perf_fixture.rs index 40fd995..b6f1bd1 100644 --- a/crates/vg_csg/examples/csg_perf_fixture.rs +++ b/crates/vg_csg/examples/csg_perf_fixture.rs @@ -36,6 +36,11 @@ fn main() { brushes: 513, build: distant_cutters_512, }, + PerfCase { + name: "distant_oriented_cutters_128", + brushes: 129, + build: distant_oriented_cutters_128, + }, ]; for case in cases { @@ -331,3 +336,25 @@ fn distant_cutters_512() -> Assembler { 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 +} diff --git a/crates/vg_csg/src/assembler.rs b/crates/vg_csg/src/assembler.rs index 27930cb..d01e47c 100644 --- a/crates/vg_csg/src/assembler.rs +++ b/crates/vg_csg/src/assembler.rs @@ -253,7 +253,7 @@ impl Assembler { return output.clone(); } - let output = self.build_uncached(); + let output = self.build_with_checkpoints(); *self.cache.borrow_mut() = Some(output.clone()); *self.first_dirty_index.borrow_mut() = None; output @@ -418,12 +418,17 @@ impl Assembler { .iter() .all(|brush| matches!(brush.geometry, CompiledGeometry::Box(_))) { - return self.build_axis_aligned_boxes(); + return self.build_axis_aligned_boxes_direct(); } self.build_general_direct() } + fn build_with_checkpoints(&self) -> BuildOutput { + self.try_build_axis_aligned_boxes_from_cache(Some(0)) + .unwrap_or_else(|| self.build_general_from_cache(Some(0))) + } + fn build_general_direct(&self) -> BuildOutput { let mut state = GeneralBuildState::default(); for brush in &self.compiled { @@ -432,9 +437,12 @@ impl Assembler { state.into_output(self.brushes.len(), self.generation) } - fn build_axis_aligned_boxes(&self) -> BuildOutput { - self.try_build_axis_aligned_boxes_from_cache(Some(0)) - .expect("box-only build path received a non-box brush") + fn build_axis_aligned_boxes_direct(&self) -> BuildOutput { + let mut state = BoxBuildState::default(); + for brush in &self.compiled { + state.apply_compiled_box(brush); + } + state.into_output_with_mesh(self.brushes.len(), self.generation, None) } fn try_build_axis_aligned_boxes_from_cache( @@ -451,6 +459,11 @@ impl Assembler { let brush_count = self.compiled.len(); let previous_output = self.cache.borrow().as_ref().cloned(); + if let Some(first_dirty_index) = first_dirty_index + && first_dirty_index > 0 + { + return Some(self.build_axis_aligned_boxes_direct()); + } let rebuild_from = match first_dirty_index { Some(index) => index.min(brush_count), None => self @@ -1352,16 +1365,20 @@ mod tests { Aabb::from_center_size(Vec3::ZERO, Vec3::splat(8.0)), MaterialId(1), ); - let far = asm.cut_box( + let far = asm.cut_oriented_box( "far", - Aabb::from_center_size(Vec3::splat(100.0), Vec3::splat(1.0)), + Vec3::splat(100.0), + Vec3::splat(1.0), + bevy_math::Quat::from_rotation_z(0.2), ); let before = asm.build_incremental(); assert!(asm.set_brush_primitive( far, - Primitive::Box { - bounds: Aabb::from_center_size(Vec3::splat(120.0), Vec3::splat(1.0)), + Primitive::OrientedBox { + center: Vec3::splat(120.0), + size: Vec3::splat(1.0), + rotation: bevy_math::Quat::from_rotation_z(0.2), }, )); let after = asm.build_incremental(); diff --git a/docs/research/csg-performance-fixtures.md b/docs/research/csg-performance-fixtures.md index 75e3290..3a07a1f 100644 --- a/docs/research/csg-performance-fixtures.md +++ b/docs/research/csg-performance-fixtures.md @@ -136,6 +136,13 @@ is intentionally exact and conservative: distant rejected edits can skip mesh emission, while candidate cuts still emit the full output mesh. The report exposes `reused_mesh` so the fixture and tests do not infer reuse from timing. +Benchmark correction: `rebuild()` must bypass checkpoint caches. An earlier +fixture pass accidentally let the dirty box baseline travel through cached box +state, which made incremental box edits look better than they were. The live +policy is stricter: box dirty edits use the direct builder unless a future real +mesh patcher proves otherwise; general convex dirty edits keep the valid-prefix +checkpoint path. + ## Latest Local Baseline Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp`: @@ -202,3 +209,11 @@ previous mesh when no geometry was touched. The single-cut case is still slightly slower because cache machinery dominates. The next hard target is dirty mesh range patching for candidate edits; replaying less CSG work is not enough when every real cut still serializes the whole result mesh. + +Corrected lesson after separating `build()` from `rebuild()`: box candidate +edits are currently better served by the direct box builder, not checkpoint +replay. The checkpoint path still wins hard for general convex tail edits: +`rotated_cut_stack_64` remains an order-of-magnitude style win in smoke runs, +and `common_box_chain_64` stays far below full dirty rebuild. Rejected general +convex suffixes can expose `reused_mesh:true`; the fixture includes +`distant_oriented_cutters_128` for that case. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index f588cad..8540d7f 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -125,7 +125,10 @@ Output parity is tested against full rebuilds. Checkpoint validity is tracked as a prefix boundary to avoid deep-copying stale history after tail edits. Mesh emission is still whole-output for candidate edits; rejected dirty suffixes can reuse the previous mesh when replay proves no geometry was touched. The report -flag is `reused_mesh`. The category-router path is still separate. +flag is `reused_mesh`. `rebuild()` intentionally bypasses checkpoint caches; +box dirty edits currently use the direct builder because the honest baseline is +faster than checkpoint replay until real mesh patching exists. The +category-router path is still separate. ## Important Invariants diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 2418c92..045b592 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -69,3 +69,4 @@ {"ts": "2026-05-07T14:05:07+00:00", "type": "implementation", "status": "ok", "note": "Added box-only dirty prefix checkpoints to vg_csg. build_incremental resumes from the checkpoint before the first dirty brush and parity tests compare incremental output against full rebuilds. Perf fixture now includes incremental_dirty; tail edits improve in large box-only scenarios, but final mesh emission remains whole-output and non-box paths fall back."} {"ts": "2026-05-07T14:14:21+00:00", "type": "implementation", "status": "ok", "note": "Extended vg_csg incremental dirty rebuilds from box-only state to the general convex builder. The cache now tracks a valid prefix boundary so tail edits reuse the checkpoint before the dirty brush without deep-copying stale checkpoint history. Latest smoke fixture shows large tail-edit wins, including rotated_cut_stack_64 dirty mean dropping from about 3.49 ms full rebuild to about 0.42 ms incremental, with parity tests still green."} {"ts": "2026-05-07T14:39:10+00:00", "type": "implementation", "status": "ok", "note": "Added conservative mesh reuse for vg_csg incremental dirty rebuilds. Mutations no longer discard the previous BuildOutput; suffix replay tracks whether geometry was touched, and rejected dirty suffixes reuse the prior mesh exactly. BuildReport exposes reused_mesh and tests cover distant tail edits. Candidate edits still emit the whole mesh."} +{"ts": "2026-05-07T14:50:34+00:00", "type": "implementation", "status": "ok", "note": "Corrected vg_csg dirty perf semantics: rebuild now bypasses checkpoint caches, while build populates them. The honest fixture showed box candidate edits are faster through the direct box builder than checkpoint replay, so incremental box dirty edits now fall back to direct unless future mesh patching changes that. Added distant_oriented_cutters_128 to exercise conservative reused_mesh on the general convex path."} diff --git a/state/map.yaml b/state/map.yaml index 229fa5a..1d9dac9 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -110,7 +110,7 @@ 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, crossing polygon category buckets, primitive/solid/tree bounds, additive cylinder/dome/floret primitives, JSONL performance fixtures with stable, dirty, and incremental dirty modes, mesh-array output, stable-generation output caching, dirty prefix checkpoints for box and general convex builders, conservative mesh reuse for rejected dirty suffixes, 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. A standalone RealtimeCSG C++ P/Invoke bridge now mirrors the managed wrapper surface, verifies a native health cube, extracts generated mesh descriptions, and appends direct native timing records. Current perf pass: `vg_csg` has a fast axis-aligned path, cached compiled brush geometry, owned convex splitting, convex containment shortcuts, stable-tree output caching, valid-prefix incremental dirty caching, and a `reused_mesh` report flag; tail edits in large box, distant-cutter, common-intersection, and rotated-convex scenes are much faster, but candidate edits still serialize the whole mesh and the category-router kernel still needs its own compact incremental cache. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. + 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 with stable, dirty, and incremental dirty modes, mesh-array output, stable-generation output caching, dirty prefix checkpoints for general convex builders, conservative mesh reuse for rejected dirty suffixes, 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. A standalone RealtimeCSG C++ P/Invoke bridge now mirrors the managed wrapper surface, verifies a native health cube, extracts generated mesh descriptions, and appends direct native timing records. Current perf pass: `vg_csg` has a fast axis-aligned direct path, cached compiled brush geometry, owned convex splitting, convex containment shortcuts, stable-tree output caching, valid-prefix incremental dirty caching for general convex edits, and a `reused_mesh` report flag; box candidate edits use the direct builder after an honest dirty baseline showed checkpoint replay is slower without real mesh patching. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. next_action: Build dirty mesh range patching for candidate edits: track emitted mesh spans for checkpointed CSG states, patch only changed ranges when suffix replay touches geometry, then port the valid-prefix checkpoint model into the compact category-router path without relaxing output parity. remotes: vibegeometry: https://github.com/GameCult/VibeGeometry