diff --git a/docs/csg-performance-fixtures.md b/docs/csg-performance-fixtures.md index 4eb881e..1ccfe94 100644 --- a/docs/csg-performance-fixtures.md +++ b/docs/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/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"