Expose dirty CSG perf mode

This commit is contained in:
Meta Vens 2026-05-07 04:31:59 +01:00
commit fc93094323
8 changed files with 145 additions and 40 deletions

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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.

View file

@ -1,10 +1,15 @@
{"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":800,"p95_ns":900,"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}

View file

@ -60,8 +60,9 @@ 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,
axis-aligned box fast path, compiled brush geometry cache, owned convex
splitting, stable-generation output cache, release perf fixtures, Bevy-math
mesh buffers, tests, clippy, and examples.
splitting, convex containment shortcuts, stable-generation output cache, stable
and dirty release perf fixture modes, 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.
@ -79,10 +80,9 @@ Repo-local Geometry Script clone notes live in
`.\tools\setup_geometry_script_clone.ps1` if `external/geometry-script` is
missing.
The current next action is to add a dirty-rebuild benchmark mode, then use the
RealtimeCSG C++ timing oracle to guide replacement of the ordered assembler
backend with the category-router kernel where cached stable output cannot hide
fragment-carving costs.
The current next action is to replace the ordered `vg_csg` dense rotated
subtraction path with the category-router kernel while keeping cached-vs-dirty
output parity tests green and comparing against the C++ timing oracle.
## Important Invariants

View file

@ -60,3 +60,4 @@
{"ts": "2026-05-07T01:35:18+00:00", "type": "implementation", "status": "blocked", "note": "Added a standalone RealtimeCSG C++ P/Invoke bridge that mirrors the managed Unity wrapper surface and fails closed for zero mesh descriptions. The native DLL loads and creates brush meshes, brushes, and trees, but outside Unity's hosted lifecycle GenerateMeshDescriptions currently returns success with count 0 even for an additive cube, so native timings are not yet valid."}
{"ts": "2026-05-07T02:27:49+00:00", "type": "implementation", "status": "ok", "note": "RealtimeCSG native bridge now reaches mesh extraction and timing: matching the public Foundation path (brushes first, tree second, InsertChildNodeRange, no redundant default brush setters) converts the direct DLL bridge from zero mesh descriptions to health cube mesh descriptions and full perf fixture records."}
{"ts": "2026-05-07T02:49:43+00:00", "type": "implementation", "status": "ok", "note": "CSG speed pass kept parity tests green while adding an axis-aligned box fast path, compiled brush geometry cache, owned convex splitting, clipping allocation reserves, and stable-generation output caching. Stable-tree fixture now beats or matches the direct C++ bridge on sampled scenarios, but dirty dense rotated subtraction still needs category-router work."}
{"ts": "2026-05-07T03:31:28+00:00", "type": "implementation", "status": "ok", "note": "Added explicit stable and dirty vg_csg perf modes, public rebuild() bypassing stable output cache, cached-vs-dirty output parity test, and convex containment shortcuts. Latest fixture shows dirty box-heavy and distant-rejection paths are respectable; dirty dense rotated subtraction remains the next category-router target."}

View file

@ -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, 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, and cached stable-tree output; unchanged fixture timings are now competitive with or faster than the C++ bridge, while 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: Build a dirty-rebuild benchmark mode and then replace the ordered `vg_csg` assembler backend with the category-router kernel where cached stable output cannot hide fragment-carving costs.
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: Replace the ordered `vg_csg` dense rotated subtraction path with the category-router kernel while keeping cached-vs-dirty output parity tests green and comparing against the C++ timing oracle.
remotes:
vibegeometry: https://github.com/GameCult/VibeGeometry
geometry_script_fork: https://github.com/GameCult/geometry-script