GameCult.Geometry.Csg/tools/realtimecsg_native_bridge/Program.cs
2026-05-07 02:39:41 +01:00

483 lines
22 KiB
C#

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);
}