Add direct RealtimeCSG native bridge probe

This commit is contained in:
Meta Vens 2026-05-07 02:39:41 +01:00
commit 444802ccba
10 changed files with 595 additions and 33 deletions

2
.gitignore vendored
View file

@ -3,6 +3,8 @@ __pycache__/
.venv/
node_modules/
target/
bin/
obj/
external/
state/scratch-compaction-*.md
experiments/renders/

View file

@ -44,27 +44,48 @@ or:
.\tools\run_csg_perf.ps1 -ReferenceCommand "path\to\reference-perf.exe"
```
To run the native RealtimeCSG C++ plugin directly through the public P/Invoke
surface mirrored by its managed Unity library:
```powershell
.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp
```
That path builds `tools/realtimecsg_native_bridge`, copies
`RealtimeCSG[1_559].dll` beside the bridge executable, calls only exported
functions already declared by the plugin's managed P/Invoke layer, and writes
native timing records to `experiments/generated/realtimecsg-cpp-perf-latest.jsonl`.
It fails closed if the native plugin emits no mesh descriptions; zero-geometry
timings are poison, not data.
Current direct-DLL status: the bridge successfully loads the native plugin and
creates brush meshes, brushes, and trees through the mirrored P/Invoke surface,
but outside the Unity-hosted plugin lifecycle the native query currently returns
success with `meshDescriptionCount = 0` even for a single additive cube. The
failure is useful: it proves the exported heart can be reached directly, and it
also proves we still need the missing initialization or lifecycle surface before
using the C++ plugin as a timing oracle.
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
The public `LogicalError/Realtime-CSG-demo` source still 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.
logical routing tables. Use it as readable doctrine. Use the native bridge as
the performance target once the missing initialization path is identified.
## 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}
{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":37314,"min_ns":27300,"p50_ns":32400,"p95_ns":45600,"max_ns":203900,"triangles":72,"fragments":6,"warnings":0}
{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":8413054,"min_ns":4710000,"p50_ns":8020100,"p95_ns":11060600,"max_ns":18551700,"triangles":3072,"fragments":256,"warnings":0}
{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":14952303,"min_ns":8627100,"p50_ns":14678000,"p95_ns":19088600,"max_ns":28753400,"triangles":3404,"fragments":280,"warnings":0}
{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":891484,"min_ns":616700,"p50_ns":823700,"p95_ns":1312000,"max_ns":3726400,"triangles":45,"fragments":1,"warnings":0}
{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":552914,"min_ns":459100,"p50_ns":471500,"p95_ns":775900,"max_ns":1421600,"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

@ -1,6 +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}
{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":37314,"min_ns":27300,"p50_ns":32400,"p95_ns":45600,"max_ns":203900,"triangles":72,"fragments":6,"warnings":0}
{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":8413054,"min_ns":4710000,"p50_ns":8020100,"p95_ns":11060600,"max_ns":18551700,"triangles":3072,"fragments":256,"warnings":0}
{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":14952303,"min_ns":8627100,"p50_ns":14678000,"p95_ns":19088600,"max_ns":28753400,"triangles":3404,"fragments":280,"warnings":0}
{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":891484,"min_ns":616700,"p50_ns":823700,"p95_ns":1312000,"max_ns":3726400,"triangles":45,"fragments":1,"warnings":0}
{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":552914,"min_ns":459100,"p50_ns":471500,"p95_ns":775900,"max_ns":1421600,"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

@ -41,16 +41,21 @@ handles Blender scene machinery and evaluated evidence.
`crates/vg_csg` is the first Rust/Bevy-side interactive geometry organ.
RealtimeCSG's public Unity repo exposes a useful brush/tree/operation/generation
model, but not the optimized native C++ kernel or a supported runtime editing
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, 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.
model and public P/Invoke declarations for the optimized native C++ plugin.
The standalone direct-DLL bridge under `tools/realtimecsg_native_bridge` mirrors
that managed wrapper surface and intentionally fails closed if the plugin emits
zero mesh descriptions. Current bridge status: the DLL loads, exported calls
create brush meshes, brushes, and trees, but direct mesh generation outside
Unity's hosted lifecycle returns success with count 0 even for an additive cube.
Do not use those zero outputs as timings. Sander van Rossen's older public
`LogicalError/Realtime-CSG-demo` and blog series remain 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,
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.
@ -69,11 +74,9 @@ 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: 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.
with the category-router kernel behind the existing CSG tree API, while
isolating the missing RealtimeCSG native initialization path before treating the
C++ plugin as a timing oracle.
## Important Invariants

View file

@ -57,3 +57,4 @@
{"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."}
{"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."}

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, 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.
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. A standalone RealtimeCSG C++ P/Invoke bridge now mirrors the managed wrapper surface and fails closed, but direct DLL mesh generation is blocked on a missing Unity-hosted initialization/lifecycle step: native nodes and brush meshes are created, yet mesh descriptions return count 0. 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, and isolate the missing RealtimeCSG native initialization path before treating the C++ plugin as a timing oracle.
remotes:
vibegeometry: https://github.com/GameCult/VibeGeometry
geometry_script_fork: https://github.com/GameCult/geometry-script

View file

@ -0,0 +1,483 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
const int WarmupIterations = 8;
const int MeasureIterations = 64;
var output = args.Length > 0
? args[0]
: Path.GetFullPath(Path.Combine("experiments", "generated", "realtimecsg-cpp-perf-latest.jsonl"));
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
Native.RegisterNoopUnityMethods();
var cases = new PerfCase[]
{
new("single_center_cut", 2, SingleCenterCut),
new("room_grid_8x8_doors", 192, RoomGrid8x8Doors),
new("rotated_cut_stack_64", 65, RotatedCutStack64),
new("common_box_chain_64", 64, CommonBoxChain64),
new("distant_cutters_512", 513, DistantCutters512),
};
using var writer = new StreamWriter(output, false);
foreach (var perfCase in cases)
{
writer.WriteLine(RunCase(perfCase));
}
static string RunCase(PerfCase perfCase)
{
var timings = new List<long>(MeasureIterations);
var triangles = 0;
var vertices = 0;
var meshDescriptions = 0;
Native.ClearAllNodes();
var tree = perfCase.Build();
if (tree == 0)
throw new InvalidOperationException($"Failed to build native RealtimeCSG tree for {perfCase.Name}");
Native.RebuildAll();
if (Native.GetBrushCount() != perfCase.Brushes)
throw new InvalidOperationException($"Native brush count mismatch for {perfCase.Name}: expected {perfCase.Brushes}, got {Native.GetBrushCount()}, nodes={Native.GetNodeCount()}, trees={Native.GetTreeCount()}, brushMeshes={Native.GetBrushMeshCount()}");
for (var i = 0; i < WarmupIterations; i++)
MeasureOnce(tree, out triangles, out vertices, out meshDescriptions);
if (meshDescriptions == 0)
throw new InvalidOperationException($"Native generated no mesh descriptions for {perfCase.Name}: nodes={Native.GetNodeCount()}, brushes={Native.GetBrushCount()}, trees={Native.GetTreeCount()}, brushMeshes={Native.GetBrushMeshCount()}, treeBrushes={Native.GetNumberOfBrushesInTree(tree)}, treeChildren={Native.GetChildNodeCount(tree)}, treeDirty={Native.IsNodeDirty(tree)}");
for (var i = 0; i < MeasureIterations; i++)
{
var stopwatch = Stopwatch.StartNew();
MeasureOnce(tree, out triangles, out vertices, out meshDescriptions);
stopwatch.Stop();
timings.Add(stopwatch.ElapsedTicks);
}
Native.ClearAllNodes();
timings.Sort();
var mean = timings.Sum() / timings.Count;
return
$"{{\"kernel\":\"realtime_csg_cpp\",\"scenario\":\"{perfCase.Name}\",\"brushes\":{perfCase.Brushes},\"iterations\":{MeasureIterations},\"warmup_iterations\":{WarmupIterations},\"mean_ns\":{TicksToNs(mean)},\"min_ns\":{TicksToNs(timings[0])},\"p50_ns\":{TicksToNs(timings[(timings.Count - 1) * 50 / 100])},\"p95_ns\":{TicksToNs(timings[(timings.Count - 1) * 95 / 100])},\"max_ns\":{TicksToNs(timings[^1])},\"triangles\":{triangles},\"vertices\":{vertices},\"mesh_descriptions\":{meshDescriptions}}}";
}
static long TicksToNs(long ticks) => (long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency));
static void MeasureOnce(int tree, out int triangles, out int vertices, out int meshDescriptions)
{
Native.RebuildAll();
Native.UpdateAllTreeMeshes();
var meshTypes = new[] { MeshQuery.RenderableAll };
var meshTypesHandle = GCHandle.Alloc(meshTypes, GCHandleType.Pinned);
var generated = Native.GenerateMeshDescriptions(tree, meshTypes.Length, meshTypesHandle.AddrOfPinnedObject(), VertexChannelFlags.All, out meshDescriptions);
if (!generated || meshDescriptions == 0)
{
meshTypesHandle.Free();
triangles = 0;
vertices = 0;
return;
}
meshTypesHandle.Free();
var descriptions = new GeneratedMeshDescription[meshDescriptions];
var descriptionsHandle = GCHandle.Alloc(descriptions, GCHandleType.Pinned);
var ok = Native.GetMeshDescriptions(tree, meshDescriptions, descriptionsHandle.AddrOfPinnedObject());
descriptionsHandle.Free();
if (!ok)
{
triangles = 0;
vertices = 0;
return;
}
triangles = 0;
vertices = 0;
foreach (var description in descriptions)
{
if (description.VertexCount <= 0 || description.IndexCount <= 0)
continue;
var indices = new int[description.IndexCount];
var positions = new Vec3[description.VertexCount];
var indexHandle = GCHandle.Alloc(indices, GCHandleType.Pinned);
var positionHandle = GCHandle.Alloc(positions, GCHandleType.Pinned);
ok = Native.GetGeneratedMesh(
tree,
description.MeshQueryIndex,
description.SubMeshQueryIndex,
description.IndexCount,
indexHandle.AddrOfPinnedObject(),
description.VertexCount,
positionHandle.AddrOfPinnedObject(),
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero,
out _,
out _);
positionHandle.Free();
indexHandle.Free();
if (ok)
{
triangles += description.IndexCount / 3;
vertices += description.VertexCount;
}
}
}
static int SingleCenterCut()
{
return Tree(
Brush("source", Vec3.Zero, Vec3.Splat(4), Mat4.Identity, Operation.Additive),
Brush("void", Vec3.Zero, Vec3.Splat(2), Mat4.Identity, Operation.Subtractive));
}
static int RoomGrid8x8Doors()
{
var nodes = new List<int>();
const float cell = 6.0f;
for (var y = 0; y < 8; y++)
for (var x = 0; x < 8; x++)
{
var center = new Vec3((x - 3.5f) * cell, (y - 3.5f) * cell, 0);
nodes.Add(Brush("floor", center + new Vec3(0, 0, -0.1f), new Vec3(5.6f, 5.6f, 0.2f), Mat4.Identity, Operation.Additive));
nodes.Add(Brush("north_wall", center + new Vec3(0, 2.8f, 1.5f), new Vec3(5.6f, 0.25f, 3.0f), Mat4.Identity, Operation.Additive));
nodes.Add(Brush("door", center + new Vec3(0, 2.8f, 1.0f), new Vec3(1.2f, 0.5f, 2.0f), Mat4.Identity, Operation.Subtractive));
}
return Tree(nodes.ToArray());
}
static int RotatedCutStack64()
{
var nodes = new List<int> { Brush("slab", Vec3.Zero, new Vec3(32, 32, 4), Mat4.Identity, Operation.Additive) };
for (var index = 0; index < 64; index++)
{
var angle = index * 0.173f;
var radius = 11.0f + (index % 7) * 0.35f;
var center = new Vec3(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius, 0);
nodes.Add(Brush("rotated_void", center, new Vec3(1.0f + (index % 3) * 0.3f, 8.0f, 5.0f), Mat4.RotationZ(angle), Operation.Subtractive));
}
return Tree(nodes.ToArray());
}
static int CommonBoxChain64()
{
var nodes = new List<int> { Brush("source", Vec3.Zero, new Vec3(32, 16, 8), Mat4.Identity, Operation.Additive) };
for (var index = 1; index < 64; index++)
{
var t = index / 63.0f;
var center = new Vec3((t - 0.5f) * 6.0f, MathF.Sin(t * MathF.Tau) * 1.5f, 0);
nodes.Add(Brush("common", center, new Vec3(30.0f - t * 8.0f, 14.0f, 7.0f), Mat4.RotationZ(t * 0.2f), Operation.Intersecting));
}
return Tree(nodes.ToArray());
}
static int DistantCutters512()
{
var nodes = new List<int> { Brush("source", Vec3.Zero, Vec3.Splat(8), Mat4.Identity, Operation.Additive) };
for (var index = 0; index < 512; index++)
{
var row = index / 32;
var col = index % 32;
nodes.Add(Brush("far_void", new Vec3(1000.0f + col * 4.0f, 1000.0f + row * 4.0f, 0), Vec3.Splat(1), Mat4.Identity, Operation.Subtractive));
}
return Tree(nodes.ToArray());
}
static int Tree(params int[] children)
{
if (!Native.GenerateTree(0, out var tree))
return 0;
Native.SetTreeEnabled(tree, true);
if (!SetChildNodes(tree, children))
throw new InvalidOperationException($"SetChildNodes failed for tree {tree} with {children.Length} children");
foreach (var child in children)
Native.SetDirty(child);
Native.SetDirty(tree);
return tree;
}
static int Brush(string name, Vec3 center, Vec3 size, Mat4 rotation, Operation operation)
{
var mesh = Cube(size);
var meshId = CreateBrushMesh(mesh, name.GetHashCode());
if (meshId == 0 || !Native.GenerateBrush(name.GetHashCode(), out var brush))
throw new InvalidOperationException($"Failed to create brush {name}: meshId={meshId}");
if (!UpdateBrushMesh(meshId, mesh))
throw new InvalidOperationException($"UpdateBrushMesh failed for brush {name}: meshId={meshId}");
if (!Native.IsBrushMeshIDValid(meshId))
throw new InvalidOperationException($"Native rejected brush mesh {name}/{meshId}");
var transform = rotation.WithTranslation(center);
if (!Native.SetNodeLocalTransformation(brush, ref transform))
throw new InvalidOperationException($"SetNodeLocalTransformation failed for brush {name}/{brush}");
if (!Native.SetBrushMeshID(brush, meshId))
throw new InvalidOperationException($"SetBrushMeshID failed for brush {name}/{brush}, mesh={meshId}");
if (!Native.SetBrushOperationType(brush, operation))
throw new InvalidOperationException($"SetBrushOperationType failed for brush {name}/{brush}, op={operation}");
if (Native.GetBrushMeshID(brush) != meshId)
throw new InvalidOperationException($"Brush {name}/{brush} did not retain mesh id {meshId}");
Native.SetDirty(brush);
return brush;
}
static bool SetChildNodes(int node, int[] children)
{
var childNodes = children.Select(id => new CsgTreeNode(id)).ToArray();
var handle = GCHandle.Alloc(childNodes, GCHandleType.Pinned);
var ok = Native.SetChildNodes(node, childNodes.Length, handle.AddrOfPinnedObject());
handle.Free();
return ok;
}
static int CreateBrushMesh(BrushMesh mesh, int userId)
{
var vh = GCHandle.Alloc(mesh.Vertices, GCHandleType.Pinned);
var eh = GCHandle.Alloc(mesh.HalfEdges, GCHandleType.Pinned);
var ph = GCHandle.Alloc(mesh.Polygons, GCHandleType.Pinned);
var id = Native.CreateBrushMesh(userId, mesh.Vertices.Length, vh.AddrOfPinnedObject(), mesh.HalfEdges.Length, eh.AddrOfPinnedObject(), mesh.Polygons.Length, ph.AddrOfPinnedObject());
ph.Free();
eh.Free();
vh.Free();
return id;
}
static bool UpdateBrushMesh(int meshId, BrushMesh mesh)
{
var vh = GCHandle.Alloc(mesh.Vertices, GCHandleType.Pinned);
var eh = GCHandle.Alloc(mesh.HalfEdges, GCHandleType.Pinned);
var ph = GCHandle.Alloc(mesh.Polygons, GCHandleType.Pinned);
var ok = Native.UpdateBrushMesh(meshId, mesh.Vertices.Length, vh.AddrOfPinnedObject(), mesh.HalfEdges.Length, eh.AddrOfPinnedObject(), mesh.Polygons.Length, ph.AddrOfPinnedObject());
ph.Free();
eh.Free();
vh.Free();
return ok;
}
static BrushMesh Cube(Vec3 size)
{
var min = size * -0.5f;
var max = size * 0.5f;
var layers = new SurfaceLayers { LayerUsage = LayerUsageFlags.RenderReceiveCastShadows };
var surfaces = new[]
{
new SurfaceDescription { UV0 = new UvMatrix(new Vec4(-1, 0, 0, -min.X), new Vec4(0, 1, 0, min.Y)) },
new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 1, 0, 0, min.X), new Vec4(0, 1, 0, min.Y)) },
new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 0, 0, 1, min.Z), new Vec4(0, 1, 0, min.Y)) },
new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 0, 0, -1, -min.Z), new Vec4(0, 1, 0, min.Y)) },
new SurfaceDescription { UV0 = new UvMatrix(new Vec4(-1, 0, 0, -min.X), new Vec4(0, 0, -1, -min.Z)) },
new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 1, 0, 0, min.X), new Vec4(0, 0, -1, -min.Z)) },
};
return new BrushMesh(
new[]
{
new Vec3(min.X, min.Y, min.Z), new Vec3(min.X, max.Y, min.Z),
new Vec3(max.X, max.Y, min.Z), new Vec3(max.X, min.Y, min.Z),
new Vec3(min.X, min.Y, max.Z), new Vec3(max.X, min.Y, max.Z),
new Vec3(max.X, max.Y, max.Z), new Vec3(min.X, max.Y, max.Z),
},
new[]
{
new HalfEdge(0,17), new HalfEdge(1,8), new HalfEdge(2,20), new HalfEdge(3,13),
new HalfEdge(4,10), new HalfEdge(5,19), new HalfEdge(6,15), new HalfEdge(7,22),
new HalfEdge(0,1), new HalfEdge(4,16), new HalfEdge(7,4), new HalfEdge(1,21),
new HalfEdge(3,18), new HalfEdge(2,3), new HalfEdge(6,23), new HalfEdge(5,6),
new HalfEdge(0,9), new HalfEdge(3,0), new HalfEdge(5,12), new HalfEdge(4,5),
new HalfEdge(1,2), new HalfEdge(7,11), new HalfEdge(6,7), new HalfEdge(2,14),
},
new[]
{
new Polygon(0,4,0,surfaces[0],layers), new Polygon(4,4,1,surfaces[1],layers),
new Polygon(8,4,2,surfaces[2],layers), new Polygon(12,4,3,surfaces[3],layers),
new Polygon(16,4,4,surfaces[4],layers), new Polygon(20,4,5,surfaces[5],layers),
});
}
record PerfCase(string Name, int Brushes, Func<int> Build);
record BrushMesh(Vec3[] Vertices, HalfEdge[] HalfEdges, Polygon[] Polygons);
enum Operation : byte { Additive = 0, Subtractive = 1, Intersecting = 2 }
enum VertexChannelFlags : byte { Position = 0, Tangent = 2, Normal = 4, UV0 = 8, All = 14 }
enum LayerUsageFlags : int
{
None = 0,
Renderable = 1,
CastShadows = 2,
ReceiveShadows = 4,
RenderCastShadows = Renderable | CastShadows,
RenderReceiveShadows = Renderable | ReceiveShadows,
RenderReceiveCastShadows = Renderable | CastShadows | ReceiveShadows,
Collidable = 8,
Culled = 1 << 23,
}
enum LayerParameterIndex : byte { None = 0, LayerParameter1 = 1, LayerParameter2 = 2, LayerParameter3 = 3 }
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Vec2 { public float X, Y; }
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Vec3(float x, float y, float z)
{
public float X = x;
public float Y = y;
public float Z = z;
public static Vec3 Zero => new(0, 0, 0);
public static Vec3 Splat(float value) => new(value, value, value);
public static Vec3 operator +(Vec3 a, Vec3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
public static Vec3 operator *(Vec3 a, float s) => new(a.X * s, a.Y * s, a.Z * s);
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Vec4(float x, float y, float z, float w) { public float X = x, Y = y, Z = z, W = w; }
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Mat4
{
public float M00, M10, M20, M30;
public float M01, M11, M21, M31;
public float M02, M12, M22, M32;
public float M03, M13, M23, M33;
public static Mat4 Identity => new() { M00 = 1, M11 = 1, M22 = 1, M33 = 1 };
public static Mat4 RotationZ(float radians)
{
var c = MathF.Cos(radians);
var s = MathF.Sin(radians);
return new Mat4 { M00 = c, M10 = s, M01 = -s, M11 = c, M22 = 1, M33 = 1 };
}
public Mat4 WithTranslation(Vec3 t)
{
var m = this;
m.M03 = t.X; m.M13 = t.Y; m.M23 = t.Z; m.M33 = 1;
return m;
}
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct UvMatrix(Vec4 u, Vec4 v) { public Vec4 U = u; public Vec4 V = v; }
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct SurfaceDescription
{
public uint SmoothingGroup;
public int SurfaceFlags;
public UvMatrix UV0;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct SurfaceLayers
{
public LayerUsageFlags LayerUsage;
public int LayerParameter1;
public int LayerParameter2;
public int LayerParameter3;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Polygon(int firstEdge, int edgeCount, int polygonId, SurfaceDescription surface, SurfaceLayers layers)
{
public int FirstEdge = firstEdge;
public int EdgeCount = edgeCount;
public int PolygonId = polygonId;
public SurfaceDescription Surface = surface;
public SurfaceLayers Layers = layers;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct HalfEdge(int vertexIndex, int twinIndex)
{
public int VertexIndex = vertexIndex;
public int TwinIndex = twinIndex;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct CsgTreeNode(int nodeId) { public int NodeId = nodeId; }
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct MeshQuery
{
const int BitShift = 24;
public uint Layers;
public uint MaskAndChannels;
public static MeshQuery RenderableAll => new(LayerUsageFlags.Renderable, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All);
MeshQuery(LayerUsageFlags query, LayerUsageFlags mask, LayerParameterIndex parameterIndex, VertexChannelFlags vertexChannels)
{
if (mask == LayerUsageFlags.None)
mask = query;
Layers = (uint)query;
Layers |= (uint)parameterIndex << BitShift;
MaskAndChannels = (uint)mask | ((uint)vertexChannels << BitShift);
}
public int UsedVertexChannels => (int)(MaskAndChannels >> BitShift);
}
[StructLayout(LayoutKind.Sequential, Pack = 8)]
struct GeneratedMeshDescription
{
public MeshQuery MeshQuery;
public int SurfaceParameter;
public int MeshQueryIndex;
public int SubMeshQueryIndex;
public long GeometryHashValue;
public long SurfaceHashValue;
public int VertexCount;
public int IndexCount;
}
static partial class Native
{
const string Dll = "RealtimeCSG[1_559]";
static readonly StringLog Log = (message, userId) => Console.Error.WriteLine($"native[{userId}]: {message}");
static readonly ReturnString NameForUserId = _ => "<native-bridge>";
public static void RegisterNoopUnityMethods()
{
var methods = new UnityMethods
{
DebugLog = Log,
DebugLogError = Log,
DebugLogWarning = Log,
NameForUserID = NameForUserId,
};
RegisterMethods(ref methods);
}
public delegate void StringLog([MarshalAs(UnmanagedType.LPStr)] string text, int uniqueObjectID);
[return: MarshalAs(UnmanagedType.LPStr)]
public delegate string ReturnString(int uniqueObjectID);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)]
struct UnityMethods
{
public StringLog DebugLog;
public StringLog DebugLogError;
public StringLog DebugLogWarning;
public ReturnString NameForUserID;
}
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] static extern void RegisterMethods(ref UnityMethods unityMethods);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern void ClearAllNodes();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool UpdateAllTreeMeshes();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern void RebuildAll();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetNodeCount();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushCount();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetTreeCount();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushMeshCount();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateTree(int userID, out int generatedTreeNodeID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetNumberOfBrushesInTree(int nodeID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetDirty(int nodeID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool IsNodeDirty(int nodeID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetChildNodeCount(int nodeID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetTreeEnabled(int modelNodeID, bool isEnabled);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateBrush(int userID, out int generatedNodeID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateBranch(int userID, out int generatedOperationNodeID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBranchOperationType(int branchNodeID, Operation operation);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBrushOperationType(int brushNodeID, Operation operation);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBrushMeshID(int brushNodeID, int brushMeshID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushMeshID(int brushNodeID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetNodeLocalTransformation(int brushNodeID, ref Mat4 brushToTreeSpace);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetChildNodes(int nodeID, int childCount, IntPtr childrenNodeIDs);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int CreateBrushMesh(int userID, int vertexCount, IntPtr vertices, int halfEdgeCount, IntPtr halfEdges, int polygonCount, IntPtr polygons);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool UpdateBrushMesh(int brushMeshIndex, int vertexCount, IntPtr vertices, int halfEdgeCount, IntPtr halfEdges, int polygonCount, IntPtr polygons);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool IsBrushMeshIDValid(int brushMeshIndex);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateMeshDescriptions(int treeNodeID, int meshTypeCount, IntPtr meshTypes, VertexChannelFlags vertexChannelMask, out int meshDescriptionCount);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetMeshDescriptions(int treeNodeID, int meshDescriptionCount, IntPtr meshDescriptions);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetGeneratedMesh(int treeNodeID, int meshIndex, int subMeshIndex, int indexCount, IntPtr indices, int vertexCount, IntPtr positions, IntPtr tangents, IntPtr normals, IntPtr uvs, out Vec3 boundsCenter, out Vec3 boundsSize);
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>RealtimeCsgNativeBridge</AssemblyName>
</PropertyGroup>
</Project>

View file

@ -1,6 +1,7 @@
param(
[string]$OutputPath = ".\experiments\generated\csg-perf-latest.jsonl",
[string]$ReferenceCommand = $env:VIBEGEOMETRY_REFERENCE_CSG_PERF
[string]$ReferenceCommand = $env:VIBEGEOMETRY_REFERENCE_CSG_PERF,
[switch]$UseRealtimeCsgCpp
)
$ErrorActionPreference = "Stop"
@ -13,6 +14,9 @@ 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
if ($LASTEXITCODE -ne 0) {
throw "vg_csg perf fixture build failed with exit code $LASTEXITCODE"
}
$exe = Join-Path $root "target\release\examples\csg_perf_fixture.exe"
if (!(Test-Path $exe)) {
@ -20,8 +24,18 @@ try {
}
& $exe | Set-Content -Path $resolvedOutput -Encoding utf8
if ($LASTEXITCODE -ne 0) {
throw "vg_csg perf fixture failed with exit code $LASTEXITCODE"
}
if ($ReferenceCommand) {
if ($UseRealtimeCsgCpp) {
$nativeOutput = ".\experiments\generated\realtimecsg-cpp-perf-latest.jsonl"
& (Join-Path $root ".\tools\run_realtimecsg_cpp_perf.ps1") -OutputPath $nativeOutput | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "RealtimeCSG C++ perf fixture failed with exit code $LASTEXITCODE"
}
Get-Content -Path $nativeOutput | Add-Content -Path $resolvedOutput -Encoding utf8
} elseif ($ReferenceCommand) {
$referenceOutput = & cmd /c $ReferenceCommand
foreach ($line in $referenceOutput) {
Add-Content -Path $resolvedOutput -Value $line -Encoding utf8

View file

@ -0,0 +1,29 @@
param(
[string]$OutputPath = ".\experiments\generated\realtimecsg-cpp-perf-latest.jsonl",
[string]$PluginDll = ".\experiments\source-repos\realtime-CSG-for-unity\Plugins\Editor\External\x64\RealtimeCSG[1_559].dll"
)
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$project = Join-Path $root "tools\realtimecsg_native_bridge"
$resolvedOutput = Join-Path $root $OutputPath
$resolvedDll = Join-Path $root $PluginDll
$releaseDir = Join-Path $project "bin\Release\net10.0"
if (!(Test-Path -LiteralPath $resolvedDll)) {
throw "RealtimeCSG native DLL not found: $resolvedDll"
}
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $resolvedOutput) | Out-Null
dotnet build $project -c Release | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "RealtimeCSG native bridge build failed with exit code $LASTEXITCODE"
}
Copy-Item -LiteralPath $resolvedDll -Destination (Join-Path $releaseDir "RealtimeCSG[1_559].dll") -Force
& (Join-Path $releaseDir "RealtimeCsgNativeBridge.exe") $resolvedOutput
if ($LASTEXITCODE -ne 0) {
Remove-Item -LiteralPath $resolvedOutput -ErrorAction SilentlyContinue
throw "RealtimeCSG native bridge failed with exit code $LASTEXITCODE"
}
Get-Content -Path $resolvedOutput