Add direct RealtimeCSG native bridge probe
This commit is contained in:
parent
f7df565813
commit
71e2d823a7
5 changed files with 567 additions and 11 deletions
|
|
@ -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"}
|
||||
```
|
||||
|
||||
|
|
|
|||
483
tools/realtimecsg_native_bridge/Program.cs
Normal file
483
tools/realtimecsg_native_bridge/Program.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
29
tools/run_realtimecsg_cpp_perf.ps1
Normal file
29
tools/run_realtimecsg_cpp_perf.ps1
Normal 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
|
||||
Loading…
Reference in a new issue