Add CSG perf fixtures and polygon category splitting

This commit is contained in:
Meta Vens 2026-05-07 01:50:49 +01:00
commit 38c969c57f
11 changed files with 556 additions and 14 deletions

View file

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

View file

@ -81,6 +81,14 @@ pub struct ConvexSolid {
pub bounds: Aabb,
}
#[derive(Clone, Debug, Default)]
pub struct CategorizedPolygons {
pub inside: Vec<ConvexPolygon>,
pub outside: Vec<ConvexPolygon>,
pub aligned: Vec<ConvexPolygon>,
pub reverse_aligned: Vec<ConvexPolygon>,
}
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<PolygonCategory>,
}
#[derive(Clone, Copy)]
enum KeepSide {
Front,
Back,
}
fn classify_piece_against_plane(
mut piece: CategorizationPiece,
plane: Plane,
inside_pieces: &mut Vec<CategorizationPiece>,
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(

View file

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

View file

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

View file

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

View file

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

View file

@ -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"}

View file

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

View file

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

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

41
tools/run_csg_perf.ps1 Normal file
View file

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