From 5733e823a5d7b880eef53caefc25dc8199c279df Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 7 May 2026 15:05:55 +0100 Subject: [PATCH] Add box CSG incremental dirty rebuild --- crates/vg_csg/examples/csg_perf_fixture.rs | 62 +++- crates/vg_csg/src/assembler.rs | 351 +++++++++++++++++---- crates/vg_csg/src/lib.rs | 9 +- docs/research/csg-performance-fixtures.md | 39 ++- notes/fresh-workspace-handoff.md | 7 +- state/evidence.jsonl | 1 + state/map.yaml | 4 +- 7 files changed, 392 insertions(+), 81 deletions(-) diff --git a/crates/vg_csg/examples/csg_perf_fixture.rs b/crates/vg_csg/examples/csg_perf_fixture.rs index 9b370ec..ed6a94a 100644 --- a/crates/vg_csg/examples/csg_perf_fixture.rs +++ b/crates/vg_csg/examples/csg_perf_fixture.rs @@ -41,6 +41,7 @@ fn main() { for case in cases { run_case(case, PerfMode::Stable); run_case(case, PerfMode::Dirty); + run_case(case, PerfMode::IncrementalDirty); run_case(case, PerfMode::Routed); } } @@ -56,6 +57,7 @@ struct PerfCase { enum PerfMode { Stable, Dirty, + IncrementalDirty, Routed, } @@ -64,29 +66,37 @@ impl PerfMode { match self { Self::Stable => "stable", Self::Dirty => "dirty", + Self::IncrementalDirty => "incremental_dirty", Self::Routed => "routed", } } - fn build(self, assembler: &Assembler) -> vg_csg::BuildOutput { + fn build(self, assembler: &mut Assembler, iteration: usize) -> vg_csg::BuildOutput { match self { Self::Stable => assembler.build(), - Self::Dirty => assembler.rebuild(), + 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 assembler = (case.build)(); + let mut assembler = (case.build)(); if matches!(mode, PerfMode::Routed) && !assembler.supports_routed_surfaces() { return; } - let warmup = mode.build(&assembler); + let warmup = assembler.build(); black_box(warmup.mesh.triangle_count()); for _ in 0..WARMUP_ITERS { - let output = mode.build(&assembler); + let output = mode.build(&mut assembler, 0); black_box(output.mesh.triangle_count()); } @@ -97,9 +107,9 @@ fn run_case(case: PerfCase, mode: PerfMode) { let mut candidate_pairs = 0; let mut rejected_pairs = 0; - for _ in 0..MEASURE_ITERS { + for iteration in 0..MEASURE_ITERS { let start = Instant::now(); - let output = mode.build(&assembler); + let output = mode.build(&mut assembler, iteration); let elapsed = start.elapsed(); triangles = output.mesh.triangle_count(); fragments = output.report.emitted_convex_fragments; @@ -146,6 +156,44 @@ fn run_case(case: PerfCase, mode: PerfMode) { ); } +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; diff --git a/crates/vg_csg/src/assembler.rs b/crates/vg_csg/src/assembler.rs index 865e8df..181b603 100644 --- a/crates/vg_csg/src/assembler.rs +++ b/crates/vg_csg/src/assembler.rs @@ -37,6 +37,8 @@ pub struct Assembler { brushes: Vec, compiled: Vec, cache: RefCell>, + box_cache: RefCell>, + first_dirty_index: RefCell>, next_id: u32, generation: u64, } @@ -78,6 +80,18 @@ enum CompiledGeometry { }, } +#[derive(Clone, Debug, Default)] +struct BoxBuildCache { + generation: u64, + checkpoints: Vec, +} + +#[derive(Clone, Debug, Default)] +struct BoxBuildState { + boxes: Vec<(Aabb, MaterialId)>, + report: BuildReport, +} + impl Assembler { pub fn new() -> Self { Self::default() @@ -122,11 +136,13 @@ impl Assembler { ) -> BrushId { let id = BrushId(self.next_id); self.next_id += 1; + let index = self.brushes.len(); let brush = Brush::new(id, name, op, primitive, material); self.compiled.push(CompiledBrush::from_brush(&brush)); self.brushes.push(brush); self.generation = self.generation.wrapping_add(1); self.cache.borrow_mut().take(); + self.mark_dirty_index(index); id } @@ -139,6 +155,7 @@ impl Assembler { self.compiled[index] = CompiledBrush::from_brush(&self.brushes[index]); self.generation = self.generation.wrapping_add(1); self.cache.borrow_mut().take(); + self.mark_dirty_index(index); true } @@ -151,6 +168,7 @@ impl Assembler { self.compiled[index] = CompiledBrush::from_brush(&self.brushes[index]); self.generation = self.generation.wrapping_add(1); self.cache.borrow_mut().take(); + self.mark_dirty_index(index); true } @@ -223,9 +241,51 @@ impl Assembler { let output = self.build_uncached(); *self.cache.borrow_mut() = Some(output.clone()); + *self.first_dirty_index.borrow_mut() = None; output } + pub fn build_incremental(&self) -> BuildOutput { + if let Some(output) = self + .cache + .borrow() + .as_ref() + .filter(|output| output.generation == self.generation) + { + return output.clone(); + } + + let first_dirty_index = *self.first_dirty_index.borrow(); + let output = self + .try_build_axis_aligned_boxes_from_cache(first_dirty_index) + .unwrap_or_else(|| self.build_uncached()); + *self.cache.borrow_mut() = Some(output.clone()); + *self.first_dirty_index.borrow_mut() = None; + output + } + + pub fn rebuild_incremental_for_indices( + &self, + dirty_indices: impl IntoIterator, + ) -> BuildOutput { + let first_dirty_index = self + .dirty_frontier_for_indices(dirty_indices) + .first_dirty_index; + self.try_build_axis_aligned_boxes_from_cache(first_dirty_index) + .unwrap_or_else(|| self.build_uncached()) + } + + pub fn rebuild_incremental_for_brushes( + &self, + dirty_brushes: impl IntoIterator, + ) -> BuildOutput { + let indices = dirty_brushes + .into_iter() + .filter_map(|id| self.brush_index(id)) + .collect::>(); + self.rebuild_incremental_for_indices(indices) + } + pub fn rebuild(&self) -> BuildOutput { self.build_uncached() } @@ -239,6 +299,11 @@ impl Assembler { self.supports_routed_surface_subset() } + fn mark_dirty_index(&self, index: usize) { + let mut first_dirty_index = self.first_dirty_index.borrow_mut(); + *first_dirty_index = Some(first_dirty_index.map_or(index, |dirty| dirty.min(index))); + } + fn try_build_routed_surfaces(&self) -> Option { if !self.supports_routed_surface_subset() { return None; @@ -470,75 +535,72 @@ impl Assembler { } fn build_axis_aligned_boxes(&self) -> BuildOutput { - let mut report = BuildReport { - input_brushes: self.brushes.len(), - ..BuildReport::default() + self.try_build_axis_aligned_boxes_from_cache(Some(0)) + .expect("box-only build path received a non-box brush") + } + + fn try_build_axis_aligned_boxes_from_cache( + &self, + first_dirty_index: Option, + ) -> Option { + if !self + .compiled + .iter() + .all(|brush| matches!(brush.geometry, CompiledGeometry::Box(_))) + { + return None; + } + + let brush_count = self.compiled.len(); + let rebuild_from = match first_dirty_index { + Some(index) => index.min(brush_count), + None => self + .box_cache + .borrow() + .as_ref() + .filter(|cache| { + cache.generation == self.generation + && cache.checkpoints.len() == brush_count + 1 + }) + .map_or(0, |_| brush_count), + }; + let cached_state = self + .box_cache + .borrow() + .as_ref() + .and_then(|cache| cache.checkpoints.get(rebuild_from).cloned()); + let (mut state, start_index) = if let Some(state) = cached_state { + (state, rebuild_from) + } else { + (BoxBuildState::default(), 0) }; - let mut boxes: Vec<(Aabb, MaterialId)> = Vec::new(); - for brush in &self.compiled { - let CompiledGeometry::Box(bounds) = brush.geometry else { - unreachable!("box-only build path received a non-box brush"); - }; - match brush.op { - BrushOp::Add => boxes.push((bounds, brush.material)), - BrushOp::Subtract => { - report.operator_brushes += 1; - if !bounds.intersects_any(boxes.iter().map(|(bounds, _)| *bounds)) { - report.rejected_pairs += boxes.len(); - continue; - } - let mut out = Vec::with_capacity(boxes.len() + 4); - for (solid, material) in boxes { - if solid.intersects(bounds) { - report.candidate_pairs += 1; - out.extend( - solid - .subtract_box(bounds) - .into_iter() - .map(|piece| (piece, material)), - ); - } else { - report.rejected_pairs += 1; - out.push((solid, material)); - } - } - boxes = out; - } - BrushOp::Intersect => { - report.operator_brushes += 1; - if !bounds.intersects_any(boxes.iter().map(|(bounds, _)| *bounds)) { - report.rejected_pairs += boxes.len(); - boxes.clear(); - continue; - } - boxes = boxes - .into_iter() - .filter_map(|(solid, material)| { - if let Some(hit) = solid.intersection(bounds) { - report.candidate_pairs += 1; - Some((hit, material)) - } else { - report.rejected_pairs += 1; - None - } - }) - .collect(); - } - } + let mut checkpoints = self + .box_cache + .borrow() + .as_ref() + .map(|cache| { + cache + .checkpoints + .iter() + .take(start_index + 1) + .cloned() + .collect::>() + }) + .filter(|checkpoints| checkpoints.len() == start_index + 1) + .unwrap_or_else(|| vec![BoxBuildState::default()]); + + for brush in &self.compiled[start_index..] { + state.apply_compiled_box(brush); + checkpoints.push(state.clone()); } - let mut mesh = TriangleMesh::new(); - report.emitted_convex_fragments = boxes.len(); - for (bounds, material) in boxes { - mesh.append_box(bounds, material); - } - - BuildOutput { - mesh, - report, + *self.box_cache.borrow_mut() = Some(BoxBuildCache { generation: self.generation, - } + checkpoints, + }); + + Some(state.into_output(self.brushes.len(), self.generation)) } pub fn sample_room_with_door() -> Self { @@ -639,6 +701,79 @@ impl CompiledBrush { } } +impl BoxBuildState { + fn apply_compiled_box(&mut self, brush: &CompiledBrush) { + let CompiledGeometry::Box(bounds) = brush.geometry else { + unreachable!("box build state received a non-box brush"); + }; + + match brush.op { + BrushOp::Add => self.boxes.push((bounds, brush.material)), + BrushOp::Subtract => { + self.report.operator_brushes += 1; + if !bounds.intersects_any(self.boxes.iter().map(|(bounds, _)| *bounds)) { + self.report.rejected_pairs += self.boxes.len(); + return; + } + + let mut out = Vec::with_capacity(self.boxes.len() + 4); + for (solid, material) in self.boxes.drain(..) { + if solid.intersects(bounds) { + self.report.candidate_pairs += 1; + out.extend( + solid + .subtract_box(bounds) + .into_iter() + .map(|piece| (piece, material)), + ); + } else { + self.report.rejected_pairs += 1; + out.push((solid, material)); + } + } + self.boxes = out; + } + BrushOp::Intersect => { + self.report.operator_brushes += 1; + if !bounds.intersects_any(self.boxes.iter().map(|(bounds, _)| *bounds)) { + self.report.rejected_pairs += self.boxes.len(); + self.boxes.clear(); + return; + } + + self.boxes = self + .boxes + .drain(..) + .filter_map(|(solid, material)| { + if let Some(hit) = solid.intersection(bounds) { + self.report.candidate_pairs += 1; + Some((hit, material)) + } else { + self.report.rejected_pairs += 1; + None + } + }) + .collect(); + } + } + } + + fn into_output(mut self, input_brushes: usize, generation: u64) -> BuildOutput { + let mut mesh = TriangleMesh::new(); + self.report.input_brushes = input_brushes; + self.report.emitted_convex_fragments = self.boxes.len(); + for (bounds, material) in self.boxes { + mesh.append_box(bounds, material); + } + + BuildOutput { + mesh, + report: self.report, + generation, + } + } +} + fn subtract_from_solids( solids: Vec, cutter: &ConvexSolid, @@ -986,6 +1121,94 @@ mod tests { assert_eq!(asm.brush_index(cutter), Some(1)); } + #[test] + fn incremental_box_rebuild_reuses_prefix_and_matches_full_rebuild() { + let mut asm = Assembler::new(); + asm.solid_box( + "source", + Aabb::from_center_size(Vec3::ZERO, Vec3::splat(8.0)), + MaterialId(1), + ); + asm.cut_box( + "early", + Aabb::from_center_size(Vec3::new(-2.0, 0.0, 0.0), Vec3::splat(2.0)), + ); + let dirty = asm.cut_box( + "dirty", + Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)), + ); + asm.cut_box( + "late", + Aabb::from_center_size(Vec3::new(2.0, 0.0, 0.0), Vec3::splat(2.0)), + ); + let before = asm.build(); + + assert!(asm.set_brush_primitive( + dirty, + Primitive::Box { + bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(3.0)), + }, + )); + let incremental = asm.build_incremental(); + let full = asm.rebuild(); + + assert_eq!(incremental.mesh.positions, full.mesh.positions); + assert_eq!(incremental.mesh.indices, full.mesh.indices); + assert_eq!( + incremental.report.candidate_pairs, + full.report.candidate_pairs + ); + assert_eq!( + incremental.report.rejected_pairs, + full.report.rejected_pairs + ); + assert_eq!(incremental.generation, full.generation); + assert_ne!(incremental.mesh.positions, before.mesh.positions); + } + + #[test] + fn explicit_incremental_box_rebuild_matches_full_rebuild() { + let mut asm = Assembler::new(); + asm.solid_box( + "source", + Aabb::from_center_size(Vec3::ZERO, Vec3::splat(8.0)), + MaterialId(1), + ); + asm.cut_box( + "early", + Aabb::from_center_size(Vec3::new(-2.0, 0.0, 0.0), Vec3::splat(2.0)), + ); + let dirty = asm.cut_box( + "dirty", + Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)), + ); + asm.cut_box( + "late", + Aabb::from_center_size(Vec3::new(2.0, 0.0, 0.0), Vec3::splat(2.0)), + ); + asm.build(); + + assert!(asm.set_brush_primitive( + dirty, + Primitive::Box { + bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(3.0)), + }, + )); + let incremental = asm.rebuild_incremental_for_brushes([dirty]); + let full = asm.rebuild(); + + assert_eq!(incremental.mesh.positions, full.mesh.positions); + assert_eq!(incremental.mesh.indices, full.mesh.indices); + assert_eq!( + incremental.report.candidate_pairs, + full.report.candidate_pairs + ); + assert_eq!( + incremental.report.rejected_pairs, + full.report.rejected_pairs + ); + } + #[test] fn mutating_brush_operation_changes_ordered_output() { let mut asm = Assembler::new(); diff --git a/crates/vg_csg/src/lib.rs b/crates/vg_csg/src/lib.rs index 82fae68..b1e80db 100644 --- a/crates/vg_csg/src/lib.rs +++ b/crates/vg_csg/src/lib.rs @@ -1,10 +1,11 @@ //! Lean constructive geometry tools for VibeGeometry. //! //! This crate borrows RealtimeCSG's useful public shape: ordered brushes, -//! operation types, dirtied generations, and a rebuildable output mesh. The -//! hidden native RealtimeCSG kernel is not public, so this starts with the -//! smallest honest CSG organ: exact box subtraction against additive boxes, -//! plus additive procedural primitives for the habitat forms we keep needing. +//! operation types, dirtied generations, prefix checkpoints, and rebuildable +//! output meshes. The hidden native RealtimeCSG kernel is not public, so this +//! starts with the smallest honest CSG organ: exact box subtraction against +//! additive boxes, plus additive procedural primitives for the habitat forms we +//! keep needing. mod assembler; mod brush; diff --git a/docs/research/csg-performance-fixtures.md b/docs/research/csg-performance-fixtures.md index 5d201a4..ed43bc9 100644 --- a/docs/research/csg-performance-fixtures.md +++ b/docs/research/csg-performance-fixtures.md @@ -119,8 +119,16 @@ Realtime editing seam: `Assembler` can now mutate brush primitives and operations by `BrushId`, invalidating cached output and incrementing generation. `DirtyDemandFrontier` computes the conservative ordered suffix after the first dirty brush. The prefix before that index is the cacheable region; the suffix is -the live region because ordered CSG decisions propagate forward. This is still a -planning surface, not an incremental mesh rebuild yet. +the live region because ordered CSG decisions propagate forward. The first +implementation now uses that boundary only for box-only prefix checkpoints; the +convex/router path still needs the same treatment. + +Incremental edit seam: box-only ordered CSG now stores prefix checkpoints after +each brush. A tail edit can resume from the checkpoint immediately before the +dirty brush and replay only the live suffix. The result is parity-checked +against a full rebuild. This is a real cache boundary, but final mesh emission is +still whole-output, so the current win appears in larger tail-edit cases rather +than tiny scenes. ## Latest Local Baseline @@ -154,3 +162,30 @@ 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. + +## Incremental Box-Only Smoke Run + +Captured on 2026-05-07 with +`cargo run -p vg_csg --release --example csg_perf_fixture`. Dirty modes mutate +the tail brush each iteration. `incremental_dirty` uses the box-prefix +checkpoint cache when the scene is box-only and falls back for non-box/rotated +cases. + +```jsonl +{"kernel":"vg_csg","mode":"stable","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":518,"min_ns":500,"p50_ns":500,"p95_ns":600,"max_ns":700,"triangles":72,"fragments":6,"warnings":0,"candidate_pairs":1,"rejected_pairs":0} +{"kernel":"vg_csg","mode":"dirty","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":4287,"min_ns":3800,"p50_ns":4000,"p95_ns":4700,"max_ns":14000,"triangles":72,"fragments":6,"warnings":0,"candidate_pairs":1,"rejected_pairs":0} +{"kernel":"vg_csg","mode":"incremental_dirty","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":4617,"min_ns":4100,"p50_ns":4200,"p95_ns":6600,"max_ns":10600,"triangles":72,"fragments":6,"warnings":0,"candidate_pairs":1,"rejected_pairs":0} +{"kernel":"vg_csg","mode":"stable","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":46415,"min_ns":41500,"p50_ns":42200,"p95_ns":52400,"max_ns":195100,"triangles":3072,"fragments":256,"warnings":0,"candidate_pairs":64,"rejected_pairs":8128} +{"kernel":"vg_csg","mode":"dirty","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":825753,"min_ns":464400,"p50_ns":715400,"p95_ns":1257500,"max_ns":3452200,"triangles":3108,"fragments":259,"warnings":0,"candidate_pairs":65,"rejected_pairs":8127} +{"kernel":"vg_csg","mode":"incremental_dirty","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":516351,"min_ns":112000,"p50_ns":363900,"p95_ns":983900,"max_ns":4876400,"triangles":3108,"fragments":259,"warnings":0,"candidate_pairs":65,"rejected_pairs":8127} +{"kernel":"vg_csg","mode":"stable","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":240,"min_ns":200,"p50_ns":200,"p95_ns":300,"max_ns":500,"triangles":12,"fragments":1,"warnings":0,"candidate_pairs":0,"rejected_pairs":512} +{"kernel":"vg_csg","mode":"dirty","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":52087,"min_ns":35900,"p50_ns":44800,"p95_ns":56700,"max_ns":281000,"triangles":12,"fragments":1,"warnings":0,"candidate_pairs":0,"rejected_pairs":512} +{"kernel":"vg_csg","mode":"incremental_dirty","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":35528,"min_ns":32200,"p50_ns":32500,"p95_ns":47000,"max_ns":50000,"triangles":12,"fragments":1,"warnings":0,"candidate_pairs":0,"rejected_pairs":512} +``` + +Tail-edit lesson: prefix caching cuts the big box-room dirty mean by roughly a +third and distant-cutter dirty mean by roughly a third. The single-cut case is +slower because cache machinery dominates, and rotated/non-box cases still use +the full path. The next hard target is cached mesh emission or dirty output +patching; replaying less CSG work is not enough if every edit still serializes +the whole result mesh. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index b193c76..bcce583 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -118,8 +118,11 @@ Current realtime-editing seam: `Assembler` can mutate brush primitives and operations by `BrushId`, invalidating cached output and incrementing generation. `DirtyDemandFrontier` computes the conservative ordered suffix after the first dirty brush. The prefix before that index is the future cache boundary; the -suffix is live because ordered CSG decisions propagate forward. There is not yet -an incremental mesh rebuild from that plan. +suffix is live because ordered CSG decisions propagate forward. Box-only ordered +CSG now has a real prefix checkpoint cache, and `build_incremental()` resumes +from the checkpoint before the first dirty brush. Output parity is tested against +full rebuilds. Mesh emission is still whole-output, and rotated/non-box paths +still fall back. ## Important Invariants diff --git a/state/evidence.jsonl b/state/evidence.jsonl index dfa7bdd..6dbe999 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -66,3 +66,4 @@ {"ts": "2026-05-07T07:30:39+00:00", "type": "implementation", "status": "ok", "note": "Added the first experimental vg_csg routed-surface path: single convex source plus exactly one candidate subtractive convex cutter emits surviving source boundary polygons plus reversed cutter cap polygons, reducing centered-cut triangles from 72 fragment-carver triangles to 48 boundary triangles. Dense repeated cutters are deliberately not routed yet: naive surface-list routing caused surface-piece explosion, so the next dense-kernel move must use compact category/frontier batching and scratch storage rather than unbounded polygon Vec churn."} {"ts": "2026-05-07T07:37:47+00:00", "type": "implementation", "status": "ok", "note": "Added PolygonRouteScratch for routed polygon classification and wired it into rebuild_routed_surfaces. An attempted in-place fragment frontier sped nothing safely: it changed rotated_cut_stack_64 output shape, so it was reverted and a regression now pins that scenario at 280 fragments, 3404 triangles, 804 candidate pairs, and 9417 rejected pairs. Future dense speed work must preserve ordered output or introduce an explicit accepted-output transition, not silently mutate semantics."} {"ts": "2026-05-07T07:47:54+00:00", "type": "implementation", "status": "ok", "note": "Added the first realtime-editing seam to vg_csg: brushes can now mutate primitive/op by BrushId, invalidating cached output and incrementing generation; DirtyDemandFrontier computes the conservative ordered suffix after the first dirty brush, identifying the future prefix-cache boundary. This is a planning/cache seam, not yet incremental mesh rebuild."} +{"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."} diff --git a/state/map.yaml b/state/map.yaml index f520ce0..8db4020 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, crossing polygon category buckets, primitive/solid/tree bounds, additive cylinder/dome/floret primitives, JSONL performance fixtures with stable and dirty modes, mesh-array output, stable-generation output caching, 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, and stable/dirty perf records; dirty box-heavy and distant-rejection paths are respectable, but dirty dense rotated subtraction still needs the category-router kernel. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. - next_action: Turn `DirtyDemandFrontier` from a conservative suffix plan into an incremental rebuild cache: preserve the prefix before the first dirty brush, rebuild the live suffix through the compact category/router path, keep cached-vs-dirty output parity green, and only add BVH/grid storage after batching proves it needs one. + 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, box-only dirty prefix checkpoints, 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, and box-only incremental dirty prefix caching; tail edits in large box-only scenes are faster, but mesh emission is still whole-output and dirty dense rotated subtraction still needs the category-router kernel. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. + next_action: Extend the dirty-prefix cache beyond box-only state: either cache emitted mesh ranges for box scenes or move the same checkpoint/live-suffix model into the convex/category-router path, then benchmark against full dirty rebuilds without relaxing output parity. remotes: vibegeometry: https://github.com/GameCult/VibeGeometry geometry_script_fork: https://github.com/GameCult/geometry-script