Add direct RealtimeCSG native bridge probe

This commit is contained in:
Meta Vens 2026-05-07 02:39:41 +01:00
commit 71e2d823a7
5 changed files with 567 additions and 11 deletions

View file

@ -44,27 +44,48 @@ or:
.\tools\run_csg_perf.ps1 -ReferenceCommand "path\to\reference-perf.exe"
```
To run the native RealtimeCSG C++ plugin directly through the public P/Invoke
surface mirrored by its managed Unity library:
```powershell
.\tools\run_csg_perf.ps1 -UseRealtimeCsgCpp
```
That path builds `tools/realtimecsg_native_bridge`, copies
`RealtimeCSG[1_559].dll` beside the bridge executable, calls only exported
functions already declared by the plugin's managed P/Invoke layer, and writes
native timing records to `experiments/generated/realtimecsg-cpp-perf-latest.jsonl`.
It fails closed if the native plugin emits no mesh descriptions; zero-geometry
timings are poison, not data.
Current direct-DLL status: the bridge successfully loads the native plugin and
creates brush meshes, brushes, and trees through the mirrored P/Invoke surface,
but outside the Unity-hosted plugin lifecycle the native query currently returns
success with `meshDescriptionCount = 0` even for a single additive cube. The
failure is useful: it proves the exported heart can be reached directly, and it
also proves we still need the missing initialization or lifecycle surface before
using the C++ plugin as a timing oracle.
The reference executable must emit JSONL with the same scenario names. Until
that harness exists, the script appends a `kernel=reference,status=missing`
record instead of faking a comparison. Sad, but hygienic.
The public `LogicalError/Realtime-CSG-demo` source already exposes the relevant
The public `LogicalError/Realtime-CSG-demo` source still exposes the relevant
algorithmic surface: half-edge control meshes, polygon bounds, visible flags,
inside/outside/aligned/reverse-aligned categories, crossing polygon splits, and
logical routing tables. A reference benchmark should call that public demo
kernel headlessly or through a small public-source harness, not the closed Unity
native plugin.
logical routing tables. Use it as readable doctrine. Use the native bridge as
the performance target once the missing initialization path is identified.
## Latest Local Baseline
Captured on 2026-05-07 with `.\tools\run_csg_perf.ps1`:
```jsonl
{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":34295,"min_ns":21200,"p50_ns":35400,"p95_ns":43000,"max_ns":52500,"triangles":72,"fragments":6,"warnings":0}
{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":11564459,"min_ns":4988300,"p50_ns":9626400,"p95_ns":20738900,"max_ns":33797500,"triangles":3072,"fragments":256,"warnings":0}
{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":12646675,"min_ns":8271900,"p50_ns":12512900,"p95_ns":16605400,"max_ns":19622900,"triangles":3404,"fragments":280,"warnings":0}
{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":829393,"min_ns":609900,"p50_ns":751800,"p95_ns":1082100,"max_ns":1478800,"triangles":45,"fragments":1,"warnings":0}
{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":534860,"min_ns":461700,"p50_ns":469400,"p95_ns":754700,"max_ns":1031900,"triangles":12,"fragments":1,"warnings":0}
{"kernel":"vg_csg","scenario":"single_center_cut","brushes":2,"iterations":64,"warmup_iterations":8,"mean_ns":37314,"min_ns":27300,"p50_ns":32400,"p95_ns":45600,"max_ns":203900,"triangles":72,"fragments":6,"warnings":0}
{"kernel":"vg_csg","scenario":"room_grid_8x8_doors","brushes":192,"iterations":64,"warmup_iterations":8,"mean_ns":8413054,"min_ns":4710000,"p50_ns":8020100,"p95_ns":11060600,"max_ns":18551700,"triangles":3072,"fragments":256,"warnings":0}
{"kernel":"vg_csg","scenario":"rotated_cut_stack_64","brushes":65,"iterations":64,"warmup_iterations":8,"mean_ns":14952303,"min_ns":8627100,"p50_ns":14678000,"p95_ns":19088600,"max_ns":28753400,"triangles":3404,"fragments":280,"warnings":0}
{"kernel":"vg_csg","scenario":"common_box_chain_64","brushes":64,"iterations":64,"warmup_iterations":8,"mean_ns":891484,"min_ns":616700,"p50_ns":823700,"p95_ns":1312000,"max_ns":3726400,"triangles":45,"fragments":1,"warnings":0}
{"kernel":"vg_csg","scenario":"distant_cutters_512","brushes":513,"iterations":64,"warmup_iterations":8,"mean_ns":552914,"min_ns":459100,"p50_ns":471500,"p95_ns":775900,"max_ns":1421600,"triangles":12,"fragments":1,"warnings":0}
{"status":"missing","reason":"Set VIBEGEOMETRY_REFERENCE_CSG_PERF or pass -ReferenceCommand with an executable that emits the same JSONL scenario records.","kernel":"reference"}
```

View file

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

View file

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

View file

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

View file

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