diff --git a/docs/csg-performance-fixtures.md b/docs/csg-performance-fixtures.md index 2bf9bf7..4eb881e 100644 --- a/docs/csg-performance-fixtures.md +++ b/docs/csg-performance-fixtures.md @@ -44,27 +44,48 @@ or: .\tools\run_csg_perf.ps1 -ReferenceCommand "path\to\reference-perf.exe" ``` +To run the native RealtimeCSG C++ plugin directly through the public P/Invoke +surface mirrored by its managed Unity library: + +```powershell +.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp +``` + +That path builds `tools/realtimecsg_native_bridge`, copies +`RealtimeCSG[1_559].dll` beside the bridge executable, calls only exported +functions already declared by the plugin's managed P/Invoke layer, and writes +native timing records to `experiments/generated/realtimecsg-cpp-perf-latest.jsonl`. +It fails closed if the native plugin emits no mesh descriptions; zero-geometry +timings are poison, not data. + +Current direct-DLL status: the bridge successfully loads the native plugin and +creates brush meshes, brushes, and trees through the mirrored P/Invoke surface, +but outside the Unity-hosted plugin lifecycle the native query currently returns +success with `meshDescriptionCount = 0` even for a single additive cube. The +failure is useful: it proves the exported heart can be reached directly, and it +also proves we still need the missing initialization or lifecycle surface before +using the C++ plugin as a timing oracle. + The reference executable must emit JSONL with the same scenario names. Until that harness exists, the script appends a `kernel=reference,status=missing` record instead of faking a comparison. Sad, but hygienic. -The public `LogicalError/Realtime-CSG-demo` source already exposes the relevant +The public `LogicalError/Realtime-CSG-demo` source still exposes the relevant algorithmic surface: half-edge control meshes, polygon bounds, visible flags, inside/outside/aligned/reverse-aligned categories, crossing polygon splits, and -logical routing tables. A reference benchmark should call that public demo -kernel headlessly or through a small public-source harness, not the closed Unity -native plugin. +logical routing tables. Use it as readable doctrine. Use the native bridge as +the performance target once the missing initialization path is identified. ## Latest Local Baseline Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1`: ```jsonl -{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":34295,"min_ns":21200,"p50_ns":35400,"p95_ns":43000,"max_ns":52500,"triangles":72,"fragments":6,"warnings":0} -{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":11564459,"min_ns":4988300,"p50_ns":9626400,"p95_ns":20738900,"max_ns":33797500,"triangles":3072,"fragments":256,"warnings":0} -{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":12646675,"min_ns":8271900,"p50_ns":12512900,"p95_ns":16605400,"max_ns":19622900,"triangles":3404,"fragments":280,"warnings":0} -{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":829393,"min_ns":609900,"p50_ns":751800,"p95_ns":1082100,"max_ns":1478800,"triangles":45,"fragments":1,"warnings":0} -{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":534860,"min_ns":461700,"p50_ns":469400,"p95_ns":754700,"max_ns":1031900,"triangles":12,"fragments":1,"warnings":0} +{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":37314,"min_ns":27300,"p50_ns":32400,"p95_ns":45600,"max_ns":203900,"triangles":72,"fragments":6,"warnings":0} +{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":8413054,"min_ns":4710000,"p50_ns":8020100,"p95_ns":11060600,"max_ns":18551700,"triangles":3072,"fragments":256,"warnings":0} +{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":14952303,"min_ns":8627100,"p50_ns":14678000,"p95_ns":19088600,"max_ns":28753400,"triangles":3404,"fragments":280,"warnings":0} +{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":891484,"min_ns":616700,"p50_ns":823700,"p95_ns":1312000,"max_ns":3726400,"triangles":45,"fragments":1,"warnings":0} +{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":552914,"min_ns":459100,"p50_ns":471500,"p95_ns":775900,"max_ns":1421600,"triangles":12,"fragments":1,"warnings":0} {"status":"missing","reason":"Set VIBEGEOMETRY_REFERENCE_CSG_PERF or pass -ReferenceCommand with an executable that emits the same JSONL scenario records.","kernel":"reference"} ``` diff --git a/tools/realtimecsg_native_bridge/Program.cs b/tools/realtimecsg_native_bridge/Program.cs new file mode 100644 index 0000000..cead8a1 --- /dev/null +++ b/tools/realtimecsg_native_bridge/Program.cs @@ -0,0 +1,483 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +const int WarmupIterations = 8; +const int MeasureIterations = 64; + +var output = args.Length > 0 + ? args[0] + : Path.GetFullPath(Path.Combine("experiments", "generated", "realtimecsg-cpp-perf-latest.jsonl")); + +Directory.CreateDirectory(Path.GetDirectoryName(output)!); +Native.RegisterNoopUnityMethods(); +var cases = new PerfCase[] +{ + new("single_center_cut", 2, SingleCenterCut), + new("room_grid_8x8_doors", 192, RoomGrid8x8Doors), + new("rotated_cut_stack_64", 65, RotatedCutStack64), + new("common_box_chain_64", 64, CommonBoxChain64), + new("distant_cutters_512", 513, DistantCutters512), +}; + +using var writer = new StreamWriter(output, false); +foreach (var perfCase in cases) +{ + writer.WriteLine(RunCase(perfCase)); +} + +static string RunCase(PerfCase perfCase) +{ + var timings = new List(MeasureIterations); + var triangles = 0; + var vertices = 0; + var meshDescriptions = 0; + + Native.ClearAllNodes(); + var tree = perfCase.Build(); + if (tree == 0) + throw new InvalidOperationException($"Failed to build native RealtimeCSG tree for {perfCase.Name}"); + + Native.RebuildAll(); + if (Native.GetBrushCount() != perfCase.Brushes) + throw new InvalidOperationException($"Native brush count mismatch for {perfCase.Name}: expected {perfCase.Brushes}, got {Native.GetBrushCount()}, nodes={Native.GetNodeCount()}, trees={Native.GetTreeCount()}, brushMeshes={Native.GetBrushMeshCount()}"); + for (var i = 0; i < WarmupIterations; i++) + MeasureOnce(tree, out triangles, out vertices, out meshDescriptions); + if (meshDescriptions == 0) + throw new InvalidOperationException($"Native generated no mesh descriptions for {perfCase.Name}: nodes={Native.GetNodeCount()}, brushes={Native.GetBrushCount()}, trees={Native.GetTreeCount()}, brushMeshes={Native.GetBrushMeshCount()}, treeBrushes={Native.GetNumberOfBrushesInTree(tree)}, treeChildren={Native.GetChildNodeCount(tree)}, treeDirty={Native.IsNodeDirty(tree)}"); + + for (var i = 0; i < MeasureIterations; i++) + { + var stopwatch = Stopwatch.StartNew(); + MeasureOnce(tree, out triangles, out vertices, out meshDescriptions); + stopwatch.Stop(); + timings.Add(stopwatch.ElapsedTicks); + } + Native.ClearAllNodes(); + + timings.Sort(); + var mean = timings.Sum() / timings.Count; + return + $"{{\"kernel\":\"realtime_csg_cpp\",\"scenario\":\"{perfCase.Name}\",\"brushes\":{perfCase.Brushes},\"iterations\":{MeasureIterations},\"warmup_iterations\":{WarmupIterations},\"mean_ns\":{TicksToNs(mean)},\"min_ns\":{TicksToNs(timings[0])},\"p50_ns\":{TicksToNs(timings[(timings.Count - 1) * 50 / 100])},\"p95_ns\":{TicksToNs(timings[(timings.Count - 1) * 95 / 100])},\"max_ns\":{TicksToNs(timings[^1])},\"triangles\":{triangles},\"vertices\":{vertices},\"mesh_descriptions\":{meshDescriptions}}}"; +} + +static long TicksToNs(long ticks) => (long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency)); + +static void MeasureOnce(int tree, out int triangles, out int vertices, out int meshDescriptions) +{ + Native.RebuildAll(); + Native.UpdateAllTreeMeshes(); + + var meshTypes = new[] { MeshQuery.RenderableAll }; + var meshTypesHandle = GCHandle.Alloc(meshTypes, GCHandleType.Pinned); + var generated = Native.GenerateMeshDescriptions(tree, meshTypes.Length, meshTypesHandle.AddrOfPinnedObject(), VertexChannelFlags.All, out meshDescriptions); + if (!generated || meshDescriptions == 0) + { + meshTypesHandle.Free(); + triangles = 0; + vertices = 0; + return; + } + meshTypesHandle.Free(); + + var descriptions = new GeneratedMeshDescription[meshDescriptions]; + var descriptionsHandle = GCHandle.Alloc(descriptions, GCHandleType.Pinned); + var ok = Native.GetMeshDescriptions(tree, meshDescriptions, descriptionsHandle.AddrOfPinnedObject()); + descriptionsHandle.Free(); + if (!ok) + { + triangles = 0; + vertices = 0; + return; + } + + triangles = 0; + vertices = 0; + foreach (var description in descriptions) + { + if (description.VertexCount <= 0 || description.IndexCount <= 0) + continue; + + var indices = new int[description.IndexCount]; + var positions = new Vec3[description.VertexCount]; + var indexHandle = GCHandle.Alloc(indices, GCHandleType.Pinned); + var positionHandle = GCHandle.Alloc(positions, GCHandleType.Pinned); + ok = Native.GetGeneratedMesh( + tree, + description.MeshQueryIndex, + description.SubMeshQueryIndex, + description.IndexCount, + indexHandle.AddrOfPinnedObject(), + description.VertexCount, + positionHandle.AddrOfPinnedObject(), + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + out _, + out _); + positionHandle.Free(); + indexHandle.Free(); + if (ok) + { + triangles += description.IndexCount / 3; + vertices += description.VertexCount; + } + } +} + +static int SingleCenterCut() +{ + return Tree( + Brush("source", Vec3.Zero, Vec3.Splat(4), Mat4.Identity, Operation.Additive), + Brush("void", Vec3.Zero, Vec3.Splat(2), Mat4.Identity, Operation.Subtractive)); +} + +static int RoomGrid8x8Doors() +{ + var nodes = new List(); + const float cell = 6.0f; + for (var y = 0; y < 8; y++) + for (var x = 0; x < 8; x++) + { + var center = new Vec3((x - 3.5f) * cell, (y - 3.5f) * cell, 0); + nodes.Add(Brush("floor", center + new Vec3(0, 0, -0.1f), new Vec3(5.6f, 5.6f, 0.2f), Mat4.Identity, Operation.Additive)); + nodes.Add(Brush("north_wall", center + new Vec3(0, 2.8f, 1.5f), new Vec3(5.6f, 0.25f, 3.0f), Mat4.Identity, Operation.Additive)); + nodes.Add(Brush("door", center + new Vec3(0, 2.8f, 1.0f), new Vec3(1.2f, 0.5f, 2.0f), Mat4.Identity, Operation.Subtractive)); + } + return Tree(nodes.ToArray()); +} + +static int RotatedCutStack64() +{ + var nodes = new List { Brush("slab", Vec3.Zero, new Vec3(32, 32, 4), Mat4.Identity, Operation.Additive) }; + for (var index = 0; index < 64; index++) + { + var angle = index * 0.173f; + var radius = 11.0f + (index % 7) * 0.35f; + var center = new Vec3(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius, 0); + nodes.Add(Brush("rotated_void", center, new Vec3(1.0f + (index % 3) * 0.3f, 8.0f, 5.0f), Mat4.RotationZ(angle), Operation.Subtractive)); + } + return Tree(nodes.ToArray()); +} + +static int CommonBoxChain64() +{ + var nodes = new List { Brush("source", Vec3.Zero, new Vec3(32, 16, 8), Mat4.Identity, Operation.Additive) }; + for (var index = 1; index < 64; index++) + { + var t = index / 63.0f; + var center = new Vec3((t - 0.5f) * 6.0f, MathF.Sin(t * MathF.Tau) * 1.5f, 0); + nodes.Add(Brush("common", center, new Vec3(30.0f - t * 8.0f, 14.0f, 7.0f), Mat4.RotationZ(t * 0.2f), Operation.Intersecting)); + } + return Tree(nodes.ToArray()); +} + +static int DistantCutters512() +{ + var nodes = new List { Brush("source", Vec3.Zero, Vec3.Splat(8), Mat4.Identity, Operation.Additive) }; + for (var index = 0; index < 512; index++) + { + var row = index / 32; + var col = index % 32; + nodes.Add(Brush("far_void", new Vec3(1000.0f + col * 4.0f, 1000.0f + row * 4.0f, 0), Vec3.Splat(1), Mat4.Identity, Operation.Subtractive)); + } + return Tree(nodes.ToArray()); +} + +static int Tree(params int[] children) +{ + if (!Native.GenerateTree(0, out var tree)) + return 0; + Native.SetTreeEnabled(tree, true); + if (!SetChildNodes(tree, children)) + throw new InvalidOperationException($"SetChildNodes failed for tree {tree} with {children.Length} children"); + foreach (var child in children) + Native.SetDirty(child); + Native.SetDirty(tree); + return tree; +} + +static int Brush(string name, Vec3 center, Vec3 size, Mat4 rotation, Operation operation) +{ + var mesh = Cube(size); + var meshId = CreateBrushMesh(mesh, name.GetHashCode()); + if (meshId == 0 || !Native.GenerateBrush(name.GetHashCode(), out var brush)) + throw new InvalidOperationException($"Failed to create brush {name}: meshId={meshId}"); + if (!UpdateBrushMesh(meshId, mesh)) + throw new InvalidOperationException($"UpdateBrushMesh failed for brush {name}: meshId={meshId}"); + if (!Native.IsBrushMeshIDValid(meshId)) + throw new InvalidOperationException($"Native rejected brush mesh {name}/{meshId}"); + var transform = rotation.WithTranslation(center); + if (!Native.SetNodeLocalTransformation(brush, ref transform)) + throw new InvalidOperationException($"SetNodeLocalTransformation failed for brush {name}/{brush}"); + if (!Native.SetBrushMeshID(brush, meshId)) + throw new InvalidOperationException($"SetBrushMeshID failed for brush {name}/{brush}, mesh={meshId}"); + if (!Native.SetBrushOperationType(brush, operation)) + throw new InvalidOperationException($"SetBrushOperationType failed for brush {name}/{brush}, op={operation}"); + if (Native.GetBrushMeshID(brush) != meshId) + throw new InvalidOperationException($"Brush {name}/{brush} did not retain mesh id {meshId}"); + Native.SetDirty(brush); + return brush; +} + +static bool SetChildNodes(int node, int[] children) +{ + var childNodes = children.Select(id => new CsgTreeNode(id)).ToArray(); + var handle = GCHandle.Alloc(childNodes, GCHandleType.Pinned); + var ok = Native.SetChildNodes(node, childNodes.Length, handle.AddrOfPinnedObject()); + handle.Free(); + return ok; +} + +static int CreateBrushMesh(BrushMesh mesh, int userId) +{ + var vh = GCHandle.Alloc(mesh.Vertices, GCHandleType.Pinned); + var eh = GCHandle.Alloc(mesh.HalfEdges, GCHandleType.Pinned); + var ph = GCHandle.Alloc(mesh.Polygons, GCHandleType.Pinned); + var id = Native.CreateBrushMesh(userId, mesh.Vertices.Length, vh.AddrOfPinnedObject(), mesh.HalfEdges.Length, eh.AddrOfPinnedObject(), mesh.Polygons.Length, ph.AddrOfPinnedObject()); + ph.Free(); + eh.Free(); + vh.Free(); + return id; +} + +static bool UpdateBrushMesh(int meshId, BrushMesh mesh) +{ + var vh = GCHandle.Alloc(mesh.Vertices, GCHandleType.Pinned); + var eh = GCHandle.Alloc(mesh.HalfEdges, GCHandleType.Pinned); + var ph = GCHandle.Alloc(mesh.Polygons, GCHandleType.Pinned); + var ok = Native.UpdateBrushMesh(meshId, mesh.Vertices.Length, vh.AddrOfPinnedObject(), mesh.HalfEdges.Length, eh.AddrOfPinnedObject(), mesh.Polygons.Length, ph.AddrOfPinnedObject()); + ph.Free(); + eh.Free(); + vh.Free(); + return ok; +} + +static BrushMesh Cube(Vec3 size) +{ + var min = size * -0.5f; + var max = size * 0.5f; + var layers = new SurfaceLayers { LayerUsage = LayerUsageFlags.RenderReceiveCastShadows }; + var surfaces = new[] + { + new SurfaceDescription { UV0 = new UvMatrix(new Vec4(-1, 0, 0, -min.X), new Vec4(0, 1, 0, min.Y)) }, + new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 1, 0, 0, min.X), new Vec4(0, 1, 0, min.Y)) }, + new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 0, 0, 1, min.Z), new Vec4(0, 1, 0, min.Y)) }, + new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 0, 0, -1, -min.Z), new Vec4(0, 1, 0, min.Y)) }, + new SurfaceDescription { UV0 = new UvMatrix(new Vec4(-1, 0, 0, -min.X), new Vec4(0, 0, -1, -min.Z)) }, + new SurfaceDescription { UV0 = new UvMatrix(new Vec4( 1, 0, 0, min.X), new Vec4(0, 0, -1, -min.Z)) }, + }; + return new BrushMesh( + new[] + { + new Vec3(min.X, min.Y, min.Z), new Vec3(min.X, max.Y, min.Z), + new Vec3(max.X, max.Y, min.Z), new Vec3(max.X, min.Y, min.Z), + new Vec3(min.X, min.Y, max.Z), new Vec3(max.X, min.Y, max.Z), + new Vec3(max.X, max.Y, max.Z), new Vec3(min.X, max.Y, max.Z), + }, + new[] + { + new HalfEdge(0,17), new HalfEdge(1,8), new HalfEdge(2,20), new HalfEdge(3,13), + new HalfEdge(4,10), new HalfEdge(5,19), new HalfEdge(6,15), new HalfEdge(7,22), + new HalfEdge(0,1), new HalfEdge(4,16), new HalfEdge(7,4), new HalfEdge(1,21), + new HalfEdge(3,18), new HalfEdge(2,3), new HalfEdge(6,23), new HalfEdge(5,6), + new HalfEdge(0,9), new HalfEdge(3,0), new HalfEdge(5,12), new HalfEdge(4,5), + new HalfEdge(1,2), new HalfEdge(7,11), new HalfEdge(6,7), new HalfEdge(2,14), + }, + new[] + { + new Polygon(0,4,0,surfaces[0],layers), new Polygon(4,4,1,surfaces[1],layers), + new Polygon(8,4,2,surfaces[2],layers), new Polygon(12,4,3,surfaces[3],layers), + new Polygon(16,4,4,surfaces[4],layers), new Polygon(20,4,5,surfaces[5],layers), + }); +} + +record PerfCase(string Name, int Brushes, Func Build); +record BrushMesh(Vec3[] Vertices, HalfEdge[] HalfEdges, Polygon[] Polygons); + +enum Operation : byte { Additive = 0, Subtractive = 1, Intersecting = 2 } +enum VertexChannelFlags : byte { Position = 0, Tangent = 2, Normal = 4, UV0 = 8, All = 14 } +enum LayerUsageFlags : int +{ + None = 0, + Renderable = 1, + CastShadows = 2, + ReceiveShadows = 4, + RenderCastShadows = Renderable | CastShadows, + RenderReceiveShadows = Renderable | ReceiveShadows, + RenderReceiveCastShadows = Renderable | CastShadows | ReceiveShadows, + Collidable = 8, + Culled = 1 << 23, +} +enum LayerParameterIndex : byte { None = 0, LayerParameter1 = 1, LayerParameter2 = 2, LayerParameter3 = 3 } + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct Vec2 { public float X, Y; } + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct Vec3(float x, float y, float z) +{ + public float X = x; + public float Y = y; + public float Z = z; + public static Vec3 Zero => new(0, 0, 0); + public static Vec3 Splat(float value) => new(value, value, value); + public static Vec3 operator +(Vec3 a, Vec3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); + public static Vec3 operator *(Vec3 a, float s) => new(a.X * s, a.Y * s, a.Z * s); +} + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct Vec4(float x, float y, float z, float w) { public float X = x, Y = y, Z = z, W = w; } + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct Mat4 +{ + public float M00, M10, M20, M30; + public float M01, M11, M21, M31; + public float M02, M12, M22, M32; + public float M03, M13, M23, M33; + public static Mat4 Identity => new() { M00 = 1, M11 = 1, M22 = 1, M33 = 1 }; + public static Mat4 RotationZ(float radians) + { + var c = MathF.Cos(radians); + var s = MathF.Sin(radians); + return new Mat4 { M00 = c, M10 = s, M01 = -s, M11 = c, M22 = 1, M33 = 1 }; + } + public Mat4 WithTranslation(Vec3 t) + { + var m = this; + m.M03 = t.X; m.M13 = t.Y; m.M23 = t.Z; m.M33 = 1; + return m; + } +} + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct UvMatrix(Vec4 u, Vec4 v) { public Vec4 U = u; public Vec4 V = v; } + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct SurfaceDescription +{ + public uint SmoothingGroup; + public int SurfaceFlags; + public UvMatrix UV0; +} + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct SurfaceLayers +{ + public LayerUsageFlags LayerUsage; + public int LayerParameter1; + public int LayerParameter2; + public int LayerParameter3; +} + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct Polygon(int firstEdge, int edgeCount, int polygonId, SurfaceDescription surface, SurfaceLayers layers) +{ + public int FirstEdge = firstEdge; + public int EdgeCount = edgeCount; + public int PolygonId = polygonId; + public SurfaceDescription Surface = surface; + public SurfaceLayers Layers = layers; +} + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct HalfEdge(int vertexIndex, int twinIndex) +{ + public int VertexIndex = vertexIndex; + public int TwinIndex = twinIndex; +} + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct CsgTreeNode(int nodeId) { public int NodeId = nodeId; } + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +struct MeshQuery +{ + const int BitShift = 24; + public uint Layers; + public uint MaskAndChannels; + public static MeshQuery RenderableAll => new(LayerUsageFlags.Renderable, LayerUsageFlags.RenderReceiveCastShadows, LayerParameterIndex.LayerParameter1, VertexChannelFlags.All); + MeshQuery(LayerUsageFlags query, LayerUsageFlags mask, LayerParameterIndex parameterIndex, VertexChannelFlags vertexChannels) + { + if (mask == LayerUsageFlags.None) + mask = query; + Layers = (uint)query; + Layers |= (uint)parameterIndex << BitShift; + MaskAndChannels = (uint)mask | ((uint)vertexChannels << BitShift); + } + public int UsedVertexChannels => (int)(MaskAndChannels >> BitShift); +} + +[StructLayout(LayoutKind.Sequential, Pack = 8)] +struct GeneratedMeshDescription +{ + public MeshQuery MeshQuery; + public int SurfaceParameter; + public int MeshQueryIndex; + public int SubMeshQueryIndex; + public long GeometryHashValue; + public long SurfaceHashValue; + public int VertexCount; + public int IndexCount; +} + +static partial class Native +{ + const string Dll = "RealtimeCSG[1_559]"; + static readonly StringLog Log = (message, userId) => Console.Error.WriteLine($"native[{userId}]: {message}"); + static readonly ReturnString NameForUserId = _ => ""; + + public static void RegisterNoopUnityMethods() + { + var methods = new UnityMethods + { + DebugLog = Log, + DebugLogError = Log, + DebugLogWarning = Log, + NameForUserID = NameForUserId, + }; + RegisterMethods(ref methods); + } + + public delegate void StringLog([MarshalAs(UnmanagedType.LPStr)] string text, int uniqueObjectID); + [return: MarshalAs(UnmanagedType.LPStr)] + public delegate string ReturnString(int uniqueObjectID); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)] + struct UnityMethods + { + public StringLog DebugLog; + public StringLog DebugLogError; + public StringLog DebugLogWarning; + public ReturnString NameForUserID; + } + + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] static extern void RegisterMethods(ref UnityMethods unityMethods); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern void ClearAllNodes(); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool UpdateAllTreeMeshes(); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern void RebuildAll(); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetNodeCount(); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushCount(); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetTreeCount(); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushMeshCount(); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateTree(int userID, out int generatedTreeNodeID); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetNumberOfBrushesInTree(int nodeID); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetDirty(int nodeID); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool IsNodeDirty(int nodeID); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetChildNodeCount(int nodeID); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetTreeEnabled(int modelNodeID, bool isEnabled); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateBrush(int userID, out int generatedNodeID); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateBranch(int userID, out int generatedOperationNodeID); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBranchOperationType(int branchNodeID, Operation operation); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBrushOperationType(int brushNodeID, Operation operation); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBrushMeshID(int brushNodeID, int brushMeshID); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBrushMeshID(int brushNodeID); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetNodeLocalTransformation(int brushNodeID, ref Mat4 brushToTreeSpace); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetChildNodes(int nodeID, int childCount, IntPtr childrenNodeIDs); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern int CreateBrushMesh(int userID, int vertexCount, IntPtr vertices, int halfEdgeCount, IntPtr halfEdges, int polygonCount, IntPtr polygons); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool UpdateBrushMesh(int brushMeshIndex, int vertexCount, IntPtr vertices, int halfEdgeCount, IntPtr halfEdges, int polygonCount, IntPtr polygons); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool IsBrushMeshIDValid(int brushMeshIndex); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GenerateMeshDescriptions(int treeNodeID, int meshTypeCount, IntPtr meshTypes, VertexChannelFlags vertexChannelMask, out int meshDescriptionCount); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetMeshDescriptions(int treeNodeID, int meshDescriptionCount, IntPtr meshDescriptions); + [DllImport(Dll, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetGeneratedMesh(int treeNodeID, int meshIndex, int subMeshIndex, int indexCount, IntPtr indices, int vertexCount, IntPtr positions, IntPtr tangents, IntPtr normals, IntPtr uvs, out Vec3 boundsCenter, out Vec3 boundsSize); +} diff --git a/tools/realtimecsg_native_bridge/RealtimeCsgNativeBridge.csproj b/tools/realtimecsg_native_bridge/RealtimeCsgNativeBridge.csproj new file mode 100644 index 0000000..805af6e --- /dev/null +++ b/tools/realtimecsg_native_bridge/RealtimeCsgNativeBridge.csproj @@ -0,0 +1,9 @@ + + + Exe + net10.0 + enable + enable + RealtimeCsgNativeBridge + + diff --git a/tools/run_csg_perf.ps1 b/tools/run_csg_perf.ps1 index ea30592..f7aafa5 100644 --- a/tools/run_csg_perf.ps1 +++ b/tools/run_csg_perf.ps1 @@ -1,6 +1,7 @@ param( [string]$OutputPath = ".\experiments\generated\csg-perf-latest.jsonl", - [string]$ReferenceCommand = $env:VIBEGEOMETRY_REFERENCE_CSG_PERF + [string]$ReferenceCommand = $env:VIBEGEOMETRY_REFERENCE_CSG_PERF, + [switch]$UseRealtimeCsgCpp ) $ErrorActionPreference = "Stop" @@ -13,6 +14,9 @@ New-Item -ItemType Directory -Force -Path $outputDir | Out-Null Push-Location $root try { cargo build --release -p vg_csg --example csg_perf_fixture | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "vg_csg perf fixture build failed with exit code $LASTEXITCODE" + } $exe = Join-Path $root "target\release\examples\csg_perf_fixture.exe" if (!(Test-Path $exe)) { @@ -20,8 +24,18 @@ try { } & $exe | Set-Content -Path $resolvedOutput -Encoding utf8 + if ($LASTEXITCODE -ne 0) { + throw "vg_csg perf fixture failed with exit code $LASTEXITCODE" + } - if ($ReferenceCommand) { + if ($UseRealtimeCsgCpp) { + $nativeOutput = ".\experiments\generated\realtimecsg-cpp-perf-latest.jsonl" + & (Join-Path $root ".\tools\run_realtimecsg_cpp_perf.ps1") -OutputPath $nativeOutput | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "RealtimeCSG C++ perf fixture failed with exit code $LASTEXITCODE" + } + Get-Content -Path $nativeOutput | Add-Content -Path $resolvedOutput -Encoding utf8 + } elseif ($ReferenceCommand) { $referenceOutput = & cmd /c $ReferenceCommand foreach ($line in $referenceOutput) { Add-Content -Path $resolvedOutput -Value $line -Encoding utf8 diff --git a/tools/run_realtimecsg_cpp_perf.ps1 b/tools/run_realtimecsg_cpp_perf.ps1 new file mode 100644 index 0000000..56a538a --- /dev/null +++ b/tools/run_realtimecsg_cpp_perf.ps1 @@ -0,0 +1,29 @@ +param( + [string]$OutputPath = ".\experiments\generated\realtimecsg-cpp-perf-latest.jsonl", + [string]$PluginDll = ".\experiments\source-repos\realtime-CSG-for-unity\Plugins\Editor\External\x64\RealtimeCSG[1_559].dll" +) + +$ErrorActionPreference = "Stop" + +$root = Split-Path -Parent $PSScriptRoot +$project = Join-Path $root "tools\realtimecsg_native_bridge" +$resolvedOutput = Join-Path $root $OutputPath +$resolvedDll = Join-Path $root $PluginDll +$releaseDir = Join-Path $project "bin\Release\net10.0" + +if (!(Test-Path -LiteralPath $resolvedDll)) { + throw "RealtimeCSG native DLL not found: $resolvedDll" +} + +New-Item -ItemType Directory -Force -Path (Split-Path -Parent $resolvedOutput) | Out-Null +dotnet build $project -c Release | Out-Host +if ($LASTEXITCODE -ne 0) { + throw "RealtimeCSG native bridge build failed with exit code $LASTEXITCODE" +} +Copy-Item -LiteralPath $resolvedDll -Destination (Join-Path $releaseDir "RealtimeCSG[1_559].dll") -Force +& (Join-Path $releaseDir "RealtimeCsgNativeBridge.exe") $resolvedOutput +if ($LASTEXITCODE -ne 0) { + Remove-Item -LiteralPath $resolvedOutput -ErrorAction SilentlyContinue + throw "RealtimeCSG native bridge failed with exit code $LASTEXITCODE" +} +Get-Content -Path $resolvedOutput