Get RealtimeCSG native bridge generating meshes

This commit is contained in:
Meta Vens 2026-05-07 03:28:33 +01:00
commit fed3864bd8
8 changed files with 205 additions and 67 deletions

1
.gitignore vendored
View file

@ -12,5 +12,6 @@ experiments/blender-cache/
experiments/source-blends/
experiments/source-repos/
experiments/inspection/
experiments/generated/realtimecsg-cpp-*.jsonl
*.blend1
*.blend2

View file

@ -51,6 +51,12 @@ surface mirrored by its managed Unity library:
.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp
```
To test the native bridge without running the full timing suite:
```powershell
.\tools\run_realtimecsg_cpp_perf.ps1 -OutputPath .\experiments\generated\realtimecsg-cpp-health-latest.jsonl -Health
```
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
@ -58,13 +64,12 @@ native timing records to `experiments/generated/realtimecsg-cpp-perf-latest.json
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.
Current direct-DLL status: the bridge successfully loads the native plugin,
creates brush meshes, brushes, and trees, verifies bounds, outlines, raycasts,
and now extracts generated mesh descriptions. The fragile part was construction
discipline: build child nodes before the tree, insert them through the public
Foundation range-insert path, and avoid eagerly setting default brush operation
or flag state.
The reference executable must emit JSONL with the same scenario names. Until
that harness exists, the script appends a `kernel=reference,status=missing`
@ -74,20 +79,25 @@ 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. Use it as readable doctrine. Use the native bridge as
the performance target once the missing initialization path is identified.
the performance target.
## Latest Local Baseline
Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1`:
Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp`:
```jsonl
{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":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"}
{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":30817,"min_ns":21200,"p50_ns":25300,"p95_ns":41600,"max_ns":144900,"triangles":72,"fragments":6,"warnings":0}
{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":6558703,"min_ns":4415500,"p50_ns":6232300,"p95_ns":8580100,"max_ns":10348300,"triangles":3072,"fragments":256,"warnings":0}
{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":12701656,"min_ns":9057100,"p50_ns":12045700,"p95_ns":17285200,"max_ns":29792900,"triangles":3404,"fragments":280,"warnings":0}
{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":890965,"min_ns":617100,"p50_ns":871800,"p95_ns":1214900,"max_ns":1989700,"triangles":45,"fragments":1,"warnings":0}
{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":635082,"min_ns":460900,"p50_ns":578300,"p95_ns":873800,"max_ns":1361200,"triangles":12,"fragments":1,"warnings":0}
{"kernel":"realtime_csg_cpp","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":8300,"min_ns":5500,"p50_ns":7300,"p95_ns":11700,"max_ns":22000,"triangles":72,"vertices":144,"mesh_descriptions":3}
{"kernel":"realtime_csg_cpp","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":530000,"min_ns":379700,"p50_ns":446300,"p95_ns":915500,"max_ns":1537100,"triangles":12800,"vertices":20992,"mesh_descriptions":4}
{"kernel":"realtime_csg_cpp","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":112800,"min_ns":93600,"p50_ns":99900,"p95_ns":143600,"max_ns":314200,"triangles":3498,"vertices":5102,"mesh_descriptions":4}
{"kernel":"realtime_csg_cpp","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":76800,"min_ns":57200,"p50_ns":64400,"p95_ns":104900,"max_ns":428700,"triangles":1536,"vertices":2412,"mesh_descriptions":4}
{"kernel":"realtime_csg_cpp","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":324600,"min_ns":240400,"p50_ns":263400,"p95_ns":534100,"max_ns":1530400,"triangles":6180,"vertices":12360,"mesh_descriptions":4}
```
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.
Read these as smoke timings, not final benchmark gospel. They are already sharp
enough to show the target: our current ordered Rust backend is not yet competing
with the C++ classifier/router on mesh-heavy workloads.

View file

@ -1,6 +1,10 @@
{"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"}
{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":30817,"min_ns":21200,"p50_ns":25300,"p95_ns":41600,"max_ns":144900,"triangles":72,"fragments":6,"warnings":0}
{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":6558703,"min_ns":4415500,"p50_ns":6232300,"p95_ns":8580100,"max_ns":10348300,"triangles":3072,"fragments":256,"warnings":0}
{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":12701656,"min_ns":9057100,"p50_ns":12045700,"p95_ns":17285200,"max_ns":29792900,"triangles":3404,"fragments":280,"warnings":0}
{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":890965,"min_ns":617100,"p50_ns":871800,"p95_ns":1214900,"max_ns":1989700,"triangles":45,"fragments":1,"warnings":0}
{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":635082,"min_ns":460900,"p50_ns":578300,"p95_ns":873800,"max_ns":1361200,"triangles":12,"fragments":1,"warnings":0}
{"kernel":"realtime_csg_cpp","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":8300,"min_ns":5500,"p50_ns":7300,"p95_ns":11700,"max_ns":22000,"triangles":72,"vertices":144,"mesh_descriptions":3}
{"kernel":"realtime_csg_cpp","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":530000,"min_ns":379700,"p50_ns":446300,"p95_ns":915500,"max_ns":1537100,"triangles":12800,"vertices":20992,"mesh_descriptions":4}
{"kernel":"realtime_csg_cpp","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":112800,"min_ns":93600,"p50_ns":99900,"p95_ns":143600,"max_ns":314200,"triangles":3498,"vertices":5102,"mesh_descriptions":4}
{"kernel":"realtime_csg_cpp","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":76800,"min_ns":57200,"p50_ns":64400,"p95_ns":104900,"max_ns":428700,"triangles":1536,"vertices":2412,"mesh_descriptions":4}
{"kernel":"realtime_csg_cpp","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":324600,"min_ns":240400,"p50_ns":263400,"p95_ns":534100,"max_ns":1530400,"triangles":6180,"vertices":12360,"mesh_descriptions":4}

View file

@ -45,8 +45,12 @@ 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.
create brush meshes, brushes, and trees, the health probe verifies bounds,
outlines, raycasts, and mesh-description extraction, and the combined perf
fixture appends native C++ timing records. The key construction lesson: build
brush nodes before the tree, create the tree, insert children through the public
Foundation range-insert path, and avoid eagerly setting default brush operation
or flag state.
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
@ -73,10 +77,9 @@ Repo-local Geometry Script clone notes live in
`.\tools\setup_geometry_script_clone.ps1` if `external/geometry-script` is
missing.
The current next action is to continue replacing the ordered assembler backend
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.
The current next action is to use the now-working RealtimeCSG C++ timing oracle
to guide replacement of the ordered assembler backend with the category-router
kernel behind the existing CSG tree API.
## Important Invariants

View file

@ -58,3 +58,4 @@
{"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."}
{"ts": "2026-05-07T02:27:49+00:00", "type": "implementation", "status": "ok", "note": "RealtimeCSG native bridge now reaches mesh extraction and timing: matching the public Foundation path (brushes first, tree second, InsertChildNodeRange, no redundant default brush setters) converts the direct DLL bridge from zero mesh descriptions to health cube mesh descriptions and full perf fixture records."}

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. 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.
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, verifies a native health cube, extracts generated mesh descriptions, and appends direct native timing records; the bridge only worked after matching the public Foundation construction path: build brushes first, create the tree, insert child ranges, and avoid redundant default brush setters. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes.
next_action: Use the now-working RealtimeCSG C++ timing oracle to guide replacement of the ordered `vg_csg` assembler backend with the category-router kernel behind the existing CSG tree API.
remotes:
vibegeometry: https://github.com/GameCult/VibeGeometry
geometry_script_fork: https://github.com/GameCult/geometry-script

View file

@ -3,13 +3,23 @@ using System.Runtime.InteropServices;
const int WarmupIterations = 8;
const int MeasureIterations = 64;
var debugNative = args.Any(arg => arg == "--debug-native");
var healthOnly = args.Any(arg => arg == "--health");
var output = args.Length > 0
? args[0]
var outputArg = args.FirstOrDefault(arg => arg != "--debug-native" && arg != "--health");
var output = outputArg is not null
? outputArg
: Path.GetFullPath(Path.Combine("experiments", "generated", "realtimecsg-cpp-perf-latest.jsonl"));
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
Native.RegisterNoopUnityMethods();
if (healthOnly)
{
using var healthWriter = new StreamWriter(output, false);
healthWriter.WriteLine(RunHealthProbe());
return;
}
var cases = new PerfCase[]
{
new("single_center_cut", 2, SingleCenterCut),
@ -25,7 +35,7 @@ foreach (var perfCase in cases)
writer.WriteLine(RunCase(perfCase));
}
static string RunCase(PerfCase perfCase)
string RunCase(PerfCase perfCase)
{
var timings = new List<long>(MeasureIterations);
var triangles = 0;
@ -38,12 +48,17 @@ static string RunCase(PerfCase perfCase)
throw new InvalidOperationException($"Failed to build native RealtimeCSG tree for {perfCase.Name}");
Native.RebuildAll();
if (debugNative)
DumpTree(perfCase.Name, tree);
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)
{
DumpTree(perfCase.Name, tree);
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++)
{
@ -60,14 +75,76 @@ static string RunCase(PerfCase perfCase)
$"{{\"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 string RunHealthProbe()
{
Native.ClearAllNodes();
var tree = TreeFrom(() => new[]
{
Brush("native_health_cube", Vec3.Zero, Vec3.Splat(4), Mat4.Identity, Operation.Additive),
});
Native.RebuildAll();
var updateOk = Native.UpdateAllTreeMeshes();
var child = Native.GetChildNodeAtIndex(tree, 0);
var bounds = new Aabb();
var boundsOk = Native.GetBrushBounds(child, ref bounds);
var outlineOk = Native.GetBrushOutlineSizes(child, out var outlineVertices, out var visibleOuter, out var visibleInner, out var invisibleOuter, out var invisibleInner, out var invalid);
var rayStart = new Vec3(-8, 0, 0);
var rayEnd = new Vec3(8, 0, 0);
var matrix = Mat4.Identity;
var rayHits = Native.RayCastIntoTreeMultiCount(tree, ref rayStart, ref rayEnd, ref matrix, 0, false, IntPtr.Zero, 0);
var meshTypes = MeshQuery.RenderOnlyTypes;
var meshTypesHandle = GCHandle.Alloc(meshTypes, GCHandleType.Pinned);
var generated = Native.GenerateMeshDescriptions(tree, meshTypes.Length, meshTypesHandle.AddrOfPinnedObject(), VertexChannelFlags.All, out var meshDescriptions);
meshTypesHandle.Free();
var heartbeat = updateOk && Native.GetTreeEnabled(tree) && Native.GetChildNodeCount(tree) == 1 && boundsOk && outlineOk && rayHits > 0;
var meshExtractionReady = generated && meshDescriptions > 0;
var json =
$"{{\"kernel\":\"realtime_csg_cpp\",\"scenario\":\"native_health_cube\",\"heartbeat\":{JsonBool(heartbeat)},\"mesh_extraction_ready\":{JsonBool(meshExtractionReady)},\"update_ok\":{JsonBool(updateOk)},\"tree\":{tree},\"tree_enabled\":{JsonBool(Native.GetTreeEnabled(tree))},\"nodes\":{Native.GetNodeCount()},\"brushes\":{Native.GetBrushCount()},\"trees\":{Native.GetTreeCount()},\"brush_meshes\":{Native.GetBrushMeshCount()},\"tree_children\":{Native.GetChildNodeCount(tree)},\"tree_brushes\":{Native.GetNumberOfBrushesInTree(tree)},\"tree_dirty\":{JsonBool(Native.IsNodeDirty(tree))},\"child_type\":{Native.GetTypeOfNode(child)},\"child_tree\":{Native.GetTreeOfNode(child)},\"child_mesh\":{Native.GetBrushMeshID(child)},\"child_operation\":{Native.GetBrushOperationType(child)},\"bounds_ok\":{JsonBool(boundsOk)},\"bounds_min\":[{bounds.MinX},{bounds.MinY},{bounds.MinZ}],\"bounds_max\":[{bounds.MaxX},{bounds.MaxY},{bounds.MaxZ}],\"outline_ok\":{JsonBool(outlineOk)},\"outline_vertices\":{outlineVertices},\"visible_outer_lines\":{visibleOuter},\"visible_inner_lines\":{visibleInner},\"invisible_outer_lines\":{invisibleOuter},\"invisible_inner_lines\":{invisibleInner},\"invalid_lines\":{invalid},\"raycast_hits\":{rayHits},\"mesh_descriptions_generated\":{JsonBool(generated)},\"mesh_descriptions\":{meshDescriptions}}}";
Native.ClearAllNodes();
return json;
}
static void DumpTree(string name, int tree)
{
Native.RebuildAll();
var updated = Native.UpdateAllTreeMeshes();
Console.Error.WriteLine($"native debug {name}: updated={updated} tree={tree} type={Native.GetTypeOfNode(tree)} enabled={Native.GetTreeEnabled(tree)} children={Native.GetChildNodeCount(tree)} dirty={Native.IsNodeDirty(tree)} nodes={Native.GetNodeCount()} brushes={Native.GetBrushCount()} trees={Native.GetTreeCount()} brushMeshes={Native.GetBrushMeshCount()}");
for (var i = 0; i < Native.GetChildNodeCount(tree); i++)
{
var child = Native.GetChildNodeAtIndex(tree, i);
var bounds = new Aabb();
var boundsOk = Native.GetBrushBounds(child, ref bounds);
var outlineOk = Native.GetBrushOutlineSizes(child, out var outlineVertices, out var visibleOuter, out var visibleInner, out var invisibleOuter, out var invisibleInner, out var invalid);
Console.Error.WriteLine($" child[{i}] id={child} type={Native.GetTypeOfNode(child)} tree={Native.GetTreeOfNode(child)} mesh={Native.GetBrushMeshID(child)} op={Native.GetBrushOperationType(child)} dirty={Native.IsNodeDirty(child)} boundsOk={boundsOk} bounds=({bounds.MinX},{bounds.MinY},{bounds.MinZ})..({bounds.MaxX},{bounds.MaxY},{bounds.MaxZ}) outlineOk={outlineOk} outlineVertices={outlineVertices} visibleOuter={visibleOuter} visibleInner={visibleInner} invisibleOuter={invisibleOuter} invisibleInner={invisibleInner} invalid={invalid}");
}
var rayStart = new Vec3(-8, 0, 0);
var rayEnd = new Vec3(8, 0, 0);
var matrix = Mat4.Identity;
var rayHits = Native.RayCastIntoTreeMultiCount(tree, ref rayStart, ref rayEnd, ref matrix, 0, false, IntPtr.Zero, 0);
Console.Error.WriteLine($" raycast hits={rayHits}");
ProbeGeneratedMesh(tree);
}
static void ProbeGeneratedMesh(int tree)
{
var indices = new int[8192];
var positions = new Vec3[8192];
var indexHandle = GCHandle.Alloc(indices, GCHandleType.Pinned);
var positionHandle = GCHandle.Alloc(positions, GCHandleType.Pinned);
var ok = Native.GetGeneratedMesh(tree, 0, 0, indices.Length, indexHandle.AddrOfPinnedObject(), positions.Length, positionHandle.AddrOfPinnedObject(), IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out var center, out var size);
positionHandle.Free();
indexHandle.Free();
Console.Error.WriteLine($" direct mesh probe ok={ok} boundsCenter=({center.X},{center.Y},{center.Z}) boundsSize=({size.X},{size.Y},{size.Z}) firstIndex={indices[0]} firstPos=({positions[0].X},{positions[0].Y},{positions[0].Z})");
}
static long TicksToNs(long ticks) => (long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency));
static string JsonBool(bool value) => value ? "true" : "false";
static void MeasureOnce(int tree, out int triangles, out int vertices, out int meshDescriptions)
{
Native.RebuildAll();
Native.UpdateAllTreeMeshes();
var meshTypes = new[] { MeshQuery.RenderableAll };
var meshTypes = MeshQuery.RenderOnlyTypes;
var meshTypesHandle = GCHandle.Alloc(meshTypes, GCHandleType.Pinned);
var generated = Native.GenerateMeshDescriptions(tree, meshTypes.Length, meshTypesHandle.AddrOfPinnedObject(), VertexChannelFlags.All, out meshDescriptions);
if (!generated || meshDescriptions == 0)
@ -126,9 +203,11 @@ static void MeasureOnce(int tree, out int triangles, out int vertices, out int m
static int SingleCenterCut()
{
return Tree(
return TreeFrom(() => new[]
{
Brush("source", Vec3.Zero, Vec3.Splat(4), Mat4.Identity, Operation.Additive),
Brush("void", Vec3.Zero, Vec3.Splat(2), Mat4.Identity, Operation.Subtractive));
Brush("void", Vec3.Zero, Vec3.Splat(2), Mat4.Identity, Operation.Subtractive),
});
}
static int RoomGrid8x8Doors()
@ -185,11 +264,18 @@ static int DistantCutters512()
static int Tree(params int[] children)
{
return TreeFrom(() => children);
}
static int TreeFrom(Func<int[]> buildChildren)
{
var children = buildChildren();
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");
if (!Native.SetTreeEnabled(tree, true) || !Native.GetTreeEnabled(tree))
throw new InvalidOperationException($"SetTreeEnabled failed for tree {tree}");
if (!InsertChildNodes(tree, children))
throw new InvalidOperationException($"InsertChildNodes failed for tree {tree} with {children.Length} children");
foreach (var child in children)
Native.SetDirty(child);
Native.SetDirty(tree);
@ -202,8 +288,6 @@ static int Brush(string name, Vec3 center, Vec3 size, Mat4 rotation, Operation o
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);
@ -211,7 +295,7 @@ static int Brush(string name, Vec3 center, Vec3 size, Mat4 rotation, Operation o
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))
if (operation != Operation.Additive && !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}");
@ -219,11 +303,10 @@ static int Brush(string name, Vec3 center, Vec3 size, Mat4 rotation, Operation o
return brush;
}
static bool SetChildNodes(int node, int[] children)
static bool InsertChildNodes(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());
var handle = GCHandle.Alloc(children, GCHandleType.Pinned);
var ok = Native.InsertChildNodeRange(node, 0, children.Length, handle.AddrOfPinnedObject());
handle.Free();
return ok;
}
@ -240,23 +323,11 @@ static int CreateBrushMesh(BrushMesh mesh, int userId)
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 layers = new SurfaceLayers { LayerUsage = LayerUsageFlags.RenderReceiveCastShadows | LayerUsageFlags.Collidable };
var surfaces = new[]
{
new SurfaceDescription { UV0 = new UvMatrix(new Vec4(-1, 0, 0, -min.X), new Vec4(0, 1, 0, min.Y)) },
@ -295,6 +366,7 @@ 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 BrushFlags : int { Default = 0, Infinite = 1 }
enum VertexChannelFlags : byte { Position = 0, Tangent = 2, Normal = 4, UV0 = 8, All = 14 }
enum LayerUsageFlags : int
{
@ -370,6 +442,14 @@ struct SurfaceLayers
public int LayerParameter3;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Aabb
{
public float MinX, MaxX;
public float MinY, MaxY;
public float MinZ, MaxZ;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Polygon(int firstEdge, int edgeCount, int polygonId, SurfaceDescription surface, SurfaceLayers layers)
{
@ -387,9 +467,6 @@ struct HalfEdge(int vertexIndex, int twinIndex)
public int TwinIndex = twinIndex;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct CsgTreeNode(int nodeId) { public int NodeId = nodeId; }
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct MeshQuery
{
@ -397,7 +474,29 @@ struct MeshQuery
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)
public static MeshQuery[] SimpleTypes => new[]
{
new MeshQuery(LayerUsageFlags.Renderable, LayerUsageFlags.Renderable, LayerParameterIndex.None, VertexChannelFlags.Position),
new MeshQuery(LayerUsageFlags.RenderReceiveCastShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.None, VertexChannelFlags.Position),
new MeshQuery(LayerUsageFlags.Renderable, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.None, VertexChannelFlags.All),
new MeshQuery(LayerUsageFlags.RenderReceiveCastShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
new MeshQuery(LayerUsageFlags.None, LayerUsageFlags.None, LayerParameterIndex.None, VertexChannelFlags.Position),
new MeshQuery(LayerUsageFlags.Culled, LayerUsageFlags.Culled, LayerParameterIndex.None, VertexChannelFlags.Position),
};
public static MeshQuery[] RenderOnlyTypes => new[]
{
new MeshQuery(LayerUsageFlags.CastShadows, LayerUsageFlags.RenderCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
new MeshQuery(LayerUsageFlags.Renderable, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
new MeshQuery(LayerUsageFlags.RenderCastShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
new MeshQuery(LayerUsageFlags.RenderReceiveShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
new MeshQuery(LayerUsageFlags.RenderReceiveCastShadows, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All),
new MeshQuery(LayerUsageFlags.None, LayerUsageFlags.Renderable, LayerParameterIndex.None, VertexChannelFlags.Position),
new MeshQuery(LayerUsageFlags.CastShadows, LayerUsageFlags.None, LayerParameterIndex.None, VertexChannelFlags.Position),
new MeshQuery(LayerUsageFlags.ReceiveShadows, LayerUsageFlags.None, LayerParameterIndex.None, VertexChannelFlags.Position),
new MeshQuery(LayerUsageFlags.Culled, LayerUsageFlags.None, LayerParameterIndex.None, VertexChannelFlags.Position),
};
public MeshQuery(LayerUsageFlags query, LayerUsageFlags mask, LayerParameterIndex parameterIndex, VertexChannelFlags vertexChannels)
{
if (mask == LayerUsageFlags.None)
mask = query;
@ -463,21 +562,32 @@ static partial class Native
[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 ClearDirty(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 int GetChildNodeAtIndex(int nodeID, int index);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetTreeOfNode(int nodeID);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern byte GetTypeOfNode(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 GetTreeEnabled(int modelNodeID);
[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 SetBrushFlags(int brushNodeID, BrushFlags flags);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushOperationType(int brushNodeID);
[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 GetBrushBounds(int brushNodeID, ref Aabb bounds);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetBrushOutlineSizes(int brushNodeID, out int vertexCount, out int visibleOuterLineCount, out int visibleInnerLineCount, out int invisibleOuterLineCount, out int invisibleInnerLineCount, out int invalidLineCount);
[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 bool InsertChildNodeRange(int nodeID, int index, 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);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int RayCastIntoTreeMultiCount(int modelNodeID, ref Vec3 worldRayStart, ref Vec3 worldRayEnd, ref Mat4 modelLocalToWorldMatrix, int inFilterLayerParameter0, bool ignoreInvisiblePolygons, IntPtr ignoreNodeIds, int ignoreNodeIdCount);
}

View file

@ -1,6 +1,8 @@
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"
[string]$PluginDll = ".\experiments\source-repos\realtime-CSG-for-unity\Plugins\Editor\External\x64\RealtimeCSG[1_559].dll",
[switch]$Health,
[switch]$DebugNative
)
$ErrorActionPreference = "Stop"
@ -21,7 +23,14 @@ 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
$bridgeArgs = @($resolvedOutput)
if ($Health) {
$bridgeArgs += "--health"
}
if ($DebugNative) {
$bridgeArgs += "--debug-native"
}
& (Join-Path $releaseDir "RealtimeCsgNativeBridge.exe") @bridgeArgs
if ($LASTEXITCODE -ne 0) {
Remove-Item -LiteralPath $resolvedOutput -ErrorAction SilentlyContinue
throw "RealtimeCSG native bridge failed with exit code $LASTEXITCODE"