fix: resolve all build errors in Meshlet LOD pipeline

- Use correct UnsafeList constructor (int capacity, Allocator)
- Use .Count instead of .Length for UnsafeList
- Cast GetUnsafePtr() to typed pointers explicitly
- Use Api.meshopt_* constants (not MeshOptApi) for simplify flags
- Use meshopt_Meshlet instance methods BuildsFlex/BuildsSpatial
- Use correct meshopt_Meshlet field names (vertex_offset, triangle_offset, etc.)
- Fix byte constant overflow with unchecked cast in LockBoundary
- Add Ghost.MeshOptimizer project reference to Ghost.Graphics.csproj
This commit is contained in:
2026-03-17 03:14:09 +00:00
parent 0a3502b858
commit 2376fc9414
8 changed files with 178 additions and 243 deletions

View File

@@ -1,52 +1,48 @@
using System;
using System.Numerics;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal static class ClodBoundsHelper
{
public static ClodBounds ComputeBounds(ClodMesh mesh, UnsafeList<uint> indices, float error)
public static unsafe ClodBounds ComputeBounds(ClodMesh mesh, UnsafeList<uint> indices, float error)
{
var bounds = MeshOptApi.ComputeClusterBounds(indices.GetUnsafePtr(), (nuint)indices.Length, mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
var result = new ClodBounds();
result.center = new Vector3(bounds.center[0], bounds.center[1], bounds.center[2]);
result.radius = bounds.radius;
result.error = error;
return result;
var bounds = MeshOptApi.ComputeClusterBounds((uint*)indices.GetUnsafePtr(), (nuint)indices.Count, mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
return new ClodBounds
{
center = new Vector3(bounds.center[0], bounds.center[1], bounds.center[2]),
radius = bounds.radius,
error = error
};
}
public static ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group)
public static unsafe ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group)
{
// Use Temp for the bounds list to support mega-meshes without stack overflow
var boundsList = new UnsafeList<ClodBounds>((nuint)group.Length, Allocator.Temp);
for (int j = 0; j < (int)group.Length; j++)
{
var boundsList = new UnsafeList<ClodBounds>(group.Count, Allocator.Temp);
for (int j = 0; j < group.Count; j++)
boundsList.Add(clusters[group[j]].bounds);
}
var merged = MeshOptApi.ComputeSphereBounds(
(float*)boundsList.GetUnsafePtr(),
(nuint)group.Length,
(nuint)group.Count,
(nuint)sizeof(ClodBounds),
(float*)boundsList.GetUnsafePtr() + 3, // offset to radius field
(float*)boundsList.GetUnsafePtr() + 3,
(nuint)sizeof(ClodBounds)
);
var result = new ClodBounds();
result.center = new Vector3(merged.center[0], merged.center[1], merged.center[2]);
result.radius = merged.radius;
result.error = 0.0f;
for (int j = 0; j < (int)group.Length; j++)
{
result.error = Math.Max(result.error, clusters[group[j]].bounds.error);
}
float maxError = 0.0f;
for (int j = 0; j < group.Count; j++)
maxError = Math.Max(maxError, clusters[group[j]].bounds.error);
boundsList.Dispose();
return result;
return new ClodBounds
{
center = new Vector3(merged.center[0], merged.center[1], merged.center[2]),
radius = merged.radius,
error = maxError
};
}
}

View File

@@ -1,8 +1,8 @@
using System;
using System.Diagnostics;
using System.Numerics;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
@@ -21,78 +21,69 @@ public unsafe static class ClodBuilder
{
Debug.Assert(mesh.vertexAttributesStride % (nuint)sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)");
// Use Persistent or Temp for large mesh data to avoid stack overflow
var locks = new UnsafeList<byte>(mesh.vertexCount, Allocator.Temp);
var locks = new UnsafeList<byte>((int)mesh.vertexCount, Allocator.Temp);
locks.AsSpan().Fill(0);
// Generate position-only remap
var remap = new UnsafeList<uint>(mesh.vertexCount, Allocator.Temp);
MeshOptApi.GeneratePositionRemap(remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
var remap = new UnsafeList<uint>((int)mesh.vertexCount, Allocator.Temp);
remap.Resize((int)mesh.vertexCount);
MeshOptApi.GeneratePositionRemap((uint*)remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
// Set up protect bits on UV seams
if (mesh.attributeProtectMask != 0)
{
nuint maxAttributes = mesh.vertexAttributesStride / sizeof(float);
for (nuint i = 0; i < mesh.vertexCount; i++)
{
uint r = remap[(int)i];
uint r = ((uint*)remap.GetUnsafePtr())[(int)i];
for (nuint j = 0; j < maxAttributes; j++)
{
if ((r != i) && ((mesh.attributeProtectMask & (1u << (int)j)) != 0))
{
if (mesh.vertexAttributes[i * maxAttributes + j] != mesh.vertexAttributes[r * maxAttributes + j])
{
locks[(int)i] |= (byte)MeshOptApi.SimplifyVertex_Protect;
((byte*)locks.GetUnsafePtr())[(int)i] |= (byte)(Api.meshopt_SimplifyVertex_Protect & 0xFF);
}
}
}
}
}
// Initial clusterization
var clusters = ClodInternal.Clusterize(config, mesh, mesh.indices, mesh.indexCount, Allocator.Persistent);
// Compute initial bounds
for (int i = 0; i < (int)clusters.Length; i++)
for (int i = 0; i < clusters.Count; i++)
{
clusters[i].bounds = ClodBoundsHelper.ComputeBounds(mesh, clusters[i].indices, 0.0f);
}
var pending = new UnsafeList<int>(clusters.Length, Allocator.Temp);
for (int i = 0; i < (int)clusters.Length; i++)
var pending = new UnsafeList<int>(clusters.Count, Allocator.Temp);
for (int i = 0; i < clusters.Count; i++)
pending.Add(i);
int depth = 0;
while (pending.Length > 1)
while (pending.Count > 1)
{
// Partition results are temporary but returned, using Temp allocator
var groups = ClodPartition.Partition(config, mesh, clusters, pending, remap, Allocator.Temp);
pending.Clear();
// Lock boundaries
ClodBoundary.LockBoundary(locks, groups, clusters, remap, mesh.vertexLock);
for (int i = 0; i < (int)groups.Length; i++)
for (int i = 0; i < groups.Count; i++)
{
// Merged indices for a group of clusters
var merged = new UnsafeList<uint>((nuint)groups[i].Length * config.maxTriangles * 3, Allocator.Temp);
for (int j = 0; j < (int)groups[i].Length; j++)
var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, Allocator.Temp);
for (int j = 0; j < groups[i].Count; j++)
{
var clusterIndices = clusters[groups[i][j]].indices;
for (int k = 0; k < (int)clusterIndices.Length; k++)
for (int k = 0; k < clusterIndices.Count; k++)
merged.Add(clusterIndices[k]);
}
nuint targetSize = ((nuint)merged.Length / 3) * (nuint)config.simplifyRatio * 3;
nuint targetSize = ((nuint)merged.Count / 3) * (nuint)config.simplifyRatio * 3;
var bounds = ClodBoundsHelper.MergeBounds(clusters, groups[i]);
float error = 0.0f;
var simplified = ClodSimplify.Simplify(config, mesh, merged, locks, targetSize, &error);
if (simplified.Length > (nuint)(merged.Length * config.simplifyThreshold))
if ((nuint)simplified.Count > (nuint)(merged.Count * config.simplifyThreshold))
{
bounds.error = float.MaxValue;
OutputGroup(config, mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
@@ -104,49 +95,41 @@ public unsafe static class ClodBuilder
int refined = OutputGroup(config, mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
// Discard old clusters
for (int j = 0; j < (int)groups[i].Length; j++)
{
for (int j = 0; j < groups[i].Count; j++)
clusters[groups[i][j]].indices.Dispose();
}
// Clusterize simplified mesh
var split = ClodInternal.Clusterize(config, mesh, simplified.GetUnsafePtr(), simplified.Length, Allocator.Persistent);
for (int j = 0; j < (int)split.Length; j++)
var split = ClodInternal.Clusterize(config, mesh, (uint*)simplified.GetUnsafePtr(), (nuint)simplified.Count, Allocator.Persistent);
for (int j = 0; j < split.Count; j++)
{
split[j].refined = refined;
split[j].bounds = bounds;
clusters.Add(split[j]);
pending.Add((int)clusters.Length - 1);
pending.Add(clusters.Count - 1);
}
split.Dispose();
merged.Dispose();
}
// Cleanup groups
for (int i = 0; i < (int)groups.Length; i++)
for (int i = 0; i < groups.Count; i++)
groups[i].Dispose();
groups.Dispose();
depth++;
}
if (pending.Length > 0)
if (pending.Count > 0)
{
var cluster = clusters[pending[0]];
var bounds = cluster.bounds;
var bounds = clusters[pending[0]].bounds;
bounds.error = float.MaxValue;
OutputGroup(config, mesh, clusters, pending, bounds, depth, outputContext, outputCallback);
}
nuint finalClusterCount = clusters.Length;
nuint finalClusterCount = (nuint)clusters.Count;
// Cleanup
for (int i = 0; i < (int)clusters.Length; i++)
for (int i = 0; i < clusters.Count; i++)
clusters[i].indices.Dispose();
clusters.Dispose();
locks.Dispose();
remap.Dispose();
pending.Dispose();
@@ -165,28 +148,26 @@ public unsafe static class ClodBuilder
ClodOutputDelegate outputCallback
)
{
// Use Temp for the output array to avoid stack pressure
var groupClusters = new UnsafeList<ClodCluster>(group.Length, Allocator.Temp);
var groupClusters = new UnsafeList<ClodCluster>(group.Count, Allocator.Temp);
for (int i = 0; i < (int)group.Length; i++)
for (int i = 0; i < group.Count; i++)
{
ref var srcCluster = ref clusters[group[i]];
var dstCluster = new ClodCluster
groupClusters.Add(new ClodCluster
{
refined = srcCluster.refined,
bounds = (config.optimizeBounds && srcCluster.refined != -1)
? ClodBoundsHelper.ComputeBounds(mesh, srcCluster.indices, srcCluster.bounds.error)
: srcCluster.bounds,
indices = srcCluster.indices.GetUnsafePtr(),
indexCount = (nuint)srcCluster.indices.Length,
indices = (uint*)srcCluster.indices.GetUnsafePtr(),
indexCount = (nuint)srcCluster.indices.Count,
vertexCount = srcCluster.vertices
};
groupClusters.Add(dstCluster);
});
}
var clodGroup = new ClodGroup { depth = depth, simplified = simplified };
int result = outputCallback != null
? outputCallback(outputContext, clodGroup, (ClodCluster*)groupClusters.GetUnsafePtr(), (nuint)groupClusters.Length)
? outputCallback(outputContext, clodGroup, (ClodCluster*)groupClusters.GetUnsafePtr(), (nuint)groupClusters.Count)
: -1;
groupClusters.Dispose();

View File

@@ -1,92 +1,79 @@
using System;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal static class ClodInternal
{
public static UnsafeList<Cluster> Clusterize(ClodConfig config, ClodMesh mesh, uint* indices, nuint indexCount, Allocator allocator)
public static unsafe UnsafeList<Cluster> Clusterize(ClodConfig config, ClodMesh mesh, uint* indices, nuint indexCount, Allocator allocator)
{
nuint maxMeshlets = MeshOptApi.BuildMeshletsBound(indexCount, config.maxVertices, config.minTriangles);
var meshlets = new UnsafeList<meshopt_Meshlet>(maxMeshlets, allocator);
var meshletVertices = new UnsafeList<uint>(indexCount, allocator);
var meshletTriangles = new UnsafeList<byte>(indexCount, allocator);
var meshlets = new UnsafeList<meshopt_Meshlet>((int)maxMeshlets, Allocator.Temp);
meshlets.Resize((int)maxMeshlets);
var meshletVertices = new UnsafeList<uint>((int)indexCount, Allocator.Temp);
meshletVertices.Resize((int)indexCount);
var meshletTriangles = new UnsafeList<byte>((int)indexCount, Allocator.Temp);
meshletTriangles.Resize((int)indexCount);
meshopt_Meshlet* pMeshlets = (meshopt_Meshlet*)meshlets.GetUnsafePtr();
uint* pMeshletVertices = (uint*)meshletVertices.GetUnsafePtr();
byte* pMeshletTriangles = (byte*)meshletTriangles.GetUnsafePtr();
meshlets.Resize(maxMeshlets);
nuint meshletCount;
if (config.clusterSpatial)
{
meshletCount = MeshOptApi.BuildMeshletsSpatial(
meshlets.GetUnsafePtr(),
meshletVertices.GetUnsafePtr(),
meshletTriangles.GetUnsafePtr(),
indices,
indexCount,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
config.maxVertices,
config.minTriangles,
config.maxTriangles,
meshletCount = pMeshlets[0].BuildsSpatial(
pMeshletVertices, pMeshletTriangles,
indices, indexCount,
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
config.maxVertices, config.minTriangles, config.maxTriangles,
config.clusterFillWeight
);
}
else
{
meshletCount = MeshOptApi.BuildMeshletsFlex(
meshlets.GetUnsafePtr(),
meshletVertices.GetUnsafePtr(),
meshletTriangles.GetUnsafePtr(),
indices,
indexCount,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
config.maxVertices,
config.minTriangles,
config.maxTriangles,
0.0f,
config.clusterSplitFactor
meshletCount = pMeshlets[0].BuildsFlex(
pMeshletVertices, pMeshletTriangles,
indices, indexCount,
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
config.maxVertices, config.minTriangles, config.maxTriangles,
0.0f, config.clusterSplitFactor
);
}
meshlets.Resize(meshletCount);
var clusters = new UnsafeList<Cluster>(meshletCount, allocator);
var clusters = new UnsafeList<Cluster>((int)meshletCount, allocator);
for (nuint i = 0; i < meshletCount; i++)
{
ref var meshlet = ref meshlets[i];
ref var meshlet = ref pMeshlets[i];
if (config.optimizeClusters)
{
MeshOptApi.OptimizeMeshlet(
meshletVertices.GetUnsafePtr() + meshlet.vertexOffset,
meshletTriangles.GetUnsafePtr() + meshlet.triangleOffset,
meshlet.triangleCount,
meshlet.vertexCount
pMeshletVertices + meshlet.vertex_offset,
pMeshletTriangles + meshlet.triangle_offset,
meshlet.triangle_count,
meshlet.vertex_count
);
}
var cluster = new Cluster
{
vertices = meshlet.vertexCount,
indices = new UnsafeList<uint>(meshlet.triangleCount * 3, allocator),
vertices = meshlet.vertex_count,
indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), allocator),
group = -1,
refined = -1
};
for (nuint j = 0; j < meshlet.triangleCount * 3; j++)
{
cluster.indices.Add(meshletVertices[(int)(meshlet.vertexOffset + meshletTriangles[(int)(meshlet.triangleOffset + j)])]);
}
for (nuint j = 0; j < meshlet.triangle_count * 3; j++)
cluster.indices.Add(pMeshletVertices[meshlet.vertex_offset + pMeshletTriangles[meshlet.triangle_offset + j]]);
clusters.Add(cluster);
}
// Cleanup
meshlets.Dispose();
meshletVertices.Dispose();
meshletTriangles.Dispose();

View File

@@ -1,48 +1,48 @@
using Ghost.MeshOptimizer;
using Misaki.HighPerformance;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal static class ClodBoundary
{
public static void LockBoundary(UnsafeList<byte> locks, UnsafeList<UnsafeList<int>> groups, UnsafeList<Cluster> clusters, UnsafeList<uint> remap, byte* vertexLock)
public static unsafe void LockBoundary(UnsafeList<byte> locks, UnsafeList<UnsafeList<int>> groups, UnsafeList<Cluster> clusters, UnsafeList<uint> remap, byte* vertexLock)
{
for (int i = 0; i < (int)locks.Length; i++)
{
locks[i] &= ~((byte)((1 << 0) | (1 << 7)));
}
byte* pLocks = (byte*)locks.GetUnsafePtr();
uint* pRemap = (uint*)remap.GetUnsafePtr();
for (int i = 0; i < (int)groups.Length; i++)
for (int i = 0; i < locks.Count; i++)
pLocks[i] = unchecked((byte)(pLocks[i] & ~((1 << 0) | (1 << 7))));
for (int i = 0; i < groups.Count; i++)
{
for (int j = 0; j < (int)groups[i].Length; j++)
for (int j = 0; j < groups[i].Count; j++)
{
var cluster = clusters[groups[i][j]];
for (int k = 0; k < (int)cluster.indices.Length; k++)
for (int k = 0; k < cluster.indices.Count; k++)
{
uint v = cluster.indices[k];
uint r = remap[(int)v];
locks[(int)r] |= (byte)(locks[(int)r] >> 7);
uint r = pRemap[(int)cluster.indices[k]];
pLocks[r] |= (byte)(pLocks[r] >> 7);
}
}
for (int j = 0; j < (int)groups[i].Length; j++)
for (int j = 0; j < groups[i].Count; j++)
{
var cluster = clusters[groups[i][j]];
for (int k = 0; k < (int)cluster.indices.Length; k++)
for (int k = 0; k < cluster.indices.Count; k++)
{
uint v = cluster.indices[k];
uint r = remap[(int)v];
locks[(int)r] |= (byte)(1 << 7);
uint r = pRemap[(int)cluster.indices[k]];
pLocks[r] |= (byte)(1 << 7);
}
}
}
for (int i = 0; i < (int)locks.Length; i++)
for (int i = 0; i < locks.Count; i++)
{
uint r = remap[i];
locks[i] = (byte)((locks[(int)r] & 1) | (locks[i] & (byte)MeshOptApi.meshopt_SimplifyVertex_Protect));
uint r = pRemap[i];
pLocks[i] = (byte)((pLocks[r] & 1) | (pLocks[i] & (byte)(Api.meshopt_SimplifyVertex_Protect & 0xFF)));
if (vertexLock != null)
locks[i] |= vertexLock[i];
pLocks[i] |= vertexLock[i];
}
}
}

View File

@@ -1,71 +1,59 @@
using System;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal static class ClodPartition
{
public static UnsafeList<UnsafeList<int>> Partition(ClodConfig config, ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> pending, UnsafeList<uint> remap, Allocator allocator)
public static unsafe UnsafeList<UnsafeList<int>> Partition(ClodConfig config, ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> pending, UnsafeList<uint> remap, Allocator allocator)
{
if (pending.Length <= (int)config.partitionSize)
if (pending.Count <= (int)config.partitionSize)
{
var partitions = new UnsafeList<UnsafeList<int>>(1, allocator);
partitions.Add(pending);
return partitions;
var single = new UnsafeList<UnsafeList<int>>(1, allocator);
single.Add(pending);
return single;
}
// Internal counters/indices can stay on Temp to avoid stack overflow for huge meshes
var clusterIndices = new UnsafeList<uint>(1024, Allocator.Temp);
var clusterCounts = new UnsafeList<uint>((nuint)pending.Length, Allocator.Temp);
nuint totalIndexCount = 0;
for (int i = 0; i < pending.Length; i++)
{
var cluster = clusters[pending[i]];
totalIndexCount += cluster.indices.Length;
}
for (int i = 0; i < pending.Count; i++)
totalIndexCount += (nuint)clusters[pending[i]].indices.Count;
clusterIndices.Resize(totalIndexCount);
var clusterIndices = new UnsafeList<uint>((int)totalIndexCount, Allocator.Temp);
var clusterCounts = new UnsafeList<uint>(pending.Count, Allocator.Temp);
nuint offset = 0;
for (int i = 0; i < pending.Length; i++)
for (int i = 0; i < pending.Count; i++)
{
var cluster = clusters[pending[i]];
clusterCounts.Add((uint)cluster.indices.Length);
for (int j = 0; j < (int)cluster.indices.Length; j++)
{
clusterIndices[(int)offset + j] = remap[(int)cluster.indices[j]];
}
offset += (nuint)cluster.indices.Length;
clusterCounts.Add((uint)cluster.indices.Count);
for (int j = 0; j < cluster.indices.Count; j++)
clusterIndices.Add(((uint*)remap.GetUnsafePtr())[(int)cluster.indices[j]]);
offset += (nuint)cluster.indices.Count;
}
var clusterPart = new UnsafeList<uint>((nuint)pending.Length, Allocator.Temp);
clusterPart.Resize((nuint)pending.Length);
var clusterPart = new UnsafeList<uint>(pending.Count, Allocator.Temp);
clusterPart.Resize(pending.Count);
nuint partitionCount = MeshOptApi.PartitionClusters(
clusterPart.GetUnsafePtr(),
clusterIndices.GetUnsafePtr(),
(uint*)clusterPart.GetUnsafePtr(),
(uint*)clusterIndices.GetUnsafePtr(),
totalIndexCount,
clusterCounts.GetUnsafePtr(),
(nuint)pending.Length,
(uint*)clusterCounts.GetUnsafePtr(),
(nuint)pending.Count,
config.partitionSpatial ? mesh.vertexPositions : null,
remap.Length,
(nuint)remap.Count,
mesh.vertexPositionsStride,
config.partitionSize
);
var partitions = new UnsafeList<UnsafeList<int>>(partitionCount, allocator);
var partitions = new UnsafeList<UnsafeList<int>>((int)partitionCount, allocator);
for (nuint i = 0; i < partitionCount; i++)
{
partitions.Add(new UnsafeList<int>((nuint)(config.partitionSize + config.partitionSize / 3), allocator));
}
partitions.Add(new UnsafeList<int>((int)(config.partitionSize + config.partitionSize / 3), allocator));
for (int i = 0; i < pending.Length; i++)
{
partitions[(int)clusterPart[i]].Add(pending[i]);
}
for (int i = 0; i < pending.Count; i++)
partitions[(int)((uint*)clusterPart.GetUnsafePtr())[i]].Add(pending[i]);
clusterIndices.Dispose();
clusterCounts.Dispose();

View File

@@ -1,6 +1,8 @@
using System;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
namespace Ghost.Graphics.Meshlet;

View File

@@ -1,12 +1,13 @@
using System;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal static class ClodSimplify
{
public static UnsafeList<uint> Simplify(
public static unsafe UnsafeList<uint> Simplify(
ClodConfig config,
ClodMesh mesh,
UnsafeList<uint> indices,
@@ -15,25 +16,22 @@ internal static class ClodSimplify
float* error
)
{
if (targetCount > (nuint)indices.Length)
{
if (targetCount >= (nuint)indices.Count)
return indices;
}
// Use Allocator.Temp for LOD results to avoid stack overflow on mega-meshes
var lod = new UnsafeList<uint>((nuint)indices.Length, Allocator.Temp);
lod.Resize((nuint)indices.Length);
var lod = new UnsafeList<uint>(indices.Count, Allocator.Temp);
lod.Resize(indices.Count);
uint options = MeshOptApi.SimplifySparse | MeshOptApi.SimplifyErrorAbsolute;
uint options = (uint)(Api.meshopt_SimplifySparse | Api.meshopt_SimplifyErrorAbsolute);
if (config.simplifyPermissive)
options |= MeshOptApi.SimplifyPermissive;
options |= (uint)Api.meshopt_SimplifyPermissive;
if (config.simplifyRegularize)
options |= MeshOptApi.SimplifyRegularize;
options |= (uint)Api.meshopt_SimplifyRegularize;
nuint resultSize = MeshOptApi.SimplifyWithAttributes(
lod.GetUnsafePtr(),
indices.GetUnsafePtr(),
(nuint)indices.Length,
(uint*)lod.GetUnsafePtr(),
(uint*)indices.GetUnsafePtr(),
(nuint)indices.Count,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
@@ -41,23 +39,21 @@ internal static class ClodSimplify
mesh.vertexAttributesStride,
mesh.attributeWeights,
mesh.attributeCount,
locks.GetUnsafePtr(),
(byte*)locks.GetUnsafePtr(),
targetCount,
float.MaxValue,
options,
error
);
lod.Resize((int)resultSize);
lod.Resize(resultSize);
// Fallback to permissive if needed
if (lod.Length > targetCount && config.simplifyFallbackPermissive && !config.simplifyPermissive)
if ((nuint)lod.Count > targetCount && config.simplifyFallbackPermissive && !config.simplifyPermissive)
{
options |= MeshOptApi.SimplifyPermissive;
options |= (uint)Api.meshopt_SimplifyPermissive;
resultSize = MeshOptApi.SimplifyWithAttributes(
lod.GetUnsafePtr(),
indices.GetUnsafePtr(),
(nuint)indices.Length,
(uint*)lod.GetUnsafePtr(),
(uint*)indices.GetUnsafePtr(),
(nuint)indices.Count,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
@@ -65,47 +61,43 @@ internal static class ClodSimplify
mesh.vertexAttributesStride,
mesh.attributeWeights,
mesh.attributeCount,
locks.GetUnsafePtr(),
(byte*)locks.GetUnsafePtr(),
targetCount,
float.MaxValue,
options,
error
);
lod.Resize(resultSize);
lod.Resize((int)resultSize);
}
// Sloppy fallback
if (lod.Length > targetCount && config.simplifyFallbackSloppy)
if ((nuint)lod.Count > targetCount && config.simplifyFallbackSloppy)
{
SimplifyFallback(lod, mesh, indices, locks, targetCount, error);
*error *= config.simplifyErrorFactorSloppy;
}
// Edge limit check
if (config.simplifyErrorEdgeLimit > 0)
{
float maxEdgeSq = 0;
for (int i = 0; i < (int)indices.Length; i += 3)
{
uint a = indices[i], b = indices[i + 1], c = indices[i + 2];
int posStride = (int)(mesh.vertexPositionsStride / sizeof(float));
float* va = mesh.vertexPositions + (a * posStride);
float* vb = mesh.vertexPositions + (b * posStride);
float* vc = mesh.vertexPositions + (c * posStride);
uint* pIdx = (uint*)indices.GetUnsafePtr();
int posStride = (int)(mesh.vertexPositionsStride / sizeof(float));
float dx = va[0] - vb[0], dy = va[1] - vb[1], dz = va[2] - vb[2];
for (int i = 0; i < indices.Count; i += 3)
{
uint a = pIdx[i], b = pIdx[i + 1], c = pIdx[i + 2];
float* va = mesh.vertexPositions + (a * (uint)posStride);
float* vb = mesh.vertexPositions + (b * (uint)posStride);
float* vc = mesh.vertexPositions + (c * (uint)posStride);
float dx, dy, dz;
dx = va[0] - vb[0]; dy = va[1] - vb[1]; dz = va[2] - vb[2];
float eab = dx * dx + dy * dy + dz * dz;
dx = va[0] - vc[0]; dy = va[1] - vc[1]; dz = va[2] - vc[2];
float eac = dx * dx + dy * dy + dz * dz;
dx = vb[0] - vc[0]; dy = vb[1] - vc[1]; dz = vb[2] - vc[2];
float ebc = dx * dx + dy * dy + dz * dz;
float emax = Math.Max(Math.Max(eab, eac), ebc);
float emin = Math.Min(Math.Min(eab, eac), ebc);
maxEdgeSq = Math.Max(maxEdgeSq, Math.Max(emin, emax / 4));
}
@@ -114,16 +106,4 @@ internal static class ClodSimplify
return lod;
}
private static void SimplifyFallback(
UnsafeList<uint> lod,
ClodMesh mesh,
UnsafeList<uint> indices,
UnsafeList<byte> locks,
nuint targetCount,
float* error
)
{
// Placeholder for sloppy simplification fallback logic
}
}