Add CSG perf fixtures and polygon category splitting
This commit is contained in:
parent
f8591e0920
commit
38c969c57f
11 changed files with 556 additions and 14 deletions
234
crates/vg_csg/examples/csg_perf_fixture.rs
Normal file
234
crates/vg_csg/examples/csg_perf_fixture.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
72
docs/research/csg-performance-fixtures.md
Normal file
72
docs/research/csg-performance-fixtures.md
Normal 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.
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
6
experiments/generated/csg-perf-latest.jsonl
Normal file
6
experiments/generated/csg-perf-latest.jsonl
Normal 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"}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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
41
tools/run_csg_perf.ps1
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue