diff --git a/.gitignore b/.gitignore index a705d85..6b0f810 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ experiments/blender-cache/ experiments/source-blends/ experiments/source-repos/ experiments/inspection/ +experiments/generated/realtimecsg-cpp-*.jsonl *.blend1 *.blend2 diff --git a/docs/research/csg-performance-fixtures.md b/docs/research/csg-performance-fixtures.md index 4eb881e..1ccfe94 100644 --- a/docs/research/csg-performance-fixtures.md +++ b/docs/research/csg-performance-fixtures.md @@ -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. diff --git a/experiments/generated/csg-perf-latest.jsonl b/experiments/generated/csg-perf-latest.jsonl index 1415379..9e1187e 100644 --- a/experiments/generated/csg-perf-latest.jsonl +++ b/experiments/generated/csg-perf-latest.jsonl @@ -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} diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 6d70e04..fdc4ab9 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -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 diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 5e15413..c05a07c 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -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."} diff --git a/state/map.yaml b/state/map.yaml index d13b933..feecfa7 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -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 diff --git a/tools/realtimecsg_native_bridge/Program.cs b/tools/realtimecsg_native_bridge/Program.cs index cead8a1..f466c36 100644 --- a/tools/realtimecsg_native_bridge/Program.cs +++ b/tools/realtimecsg_native_bridge/Program.cs @@ -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(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 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 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); } diff --git a/tools/run_realtimecsg_cpp_perf.ps1 b/tools/run_realtimecsg_cpp_perf.ps1 index 56a538a..bd91a90 100644 --- a/tools/run_realtimecsg_cpp_perf.ps1 +++ b/tools/run_realtimecsg_cpp_perf.ps1 @@ -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"