6 Commits

Author SHA1 Message Date
a35321df89 feat: implement CPU meshlet baking and update pipeline shaders 2026-03-20 07:53:23 +00:00
db0be367ef feat(meshopt): add typed enums and improve naming logic
Introduce SimplifyOptions and SimplifyVertexOptions enums for mesh simplification, replacing magic numbers with type-safe flags. Update MeshOptApi with strongly-typed wrapper methods. Refactor MeshletUtility to use new enums and nullable delegates, and fix stride calculation for pointer arithmetic.

Rename NamingConventions.GetMethodName to GetName, update name removal logic to use "$TBare", and add ALL_CAPS style for constants. Update config files to match new naming conventions and add ALL_CAPS constant rule for meshopt. Refactor BindingParser and related classes to support constant member kind. Apply minor bug fixes and code style improvements throughout.
2026-03-20 15:17:38 +09:00
4a98e44630 feat(meshlet)!: consolidate and modernize Cluster LOD logic
Refactored Cluster LOD mesh generation by merging ClodBounds, ClodConfig, ClodMesh, ClodGroup, ClodCluster, Cluster, and related logic into a new MeshletUtility.cs under Ghost.Graphics.Utilities.
Removed legacy Clod* files and updated to use improved memory management (UnsafeArray, Allocator.FreeList) and more idiomatic C# patterns.
Updated .csproj package versions for compatibility.
Minor code style improvements in RenderGraphResourcePool.cs.

BREAKING CHANGE: Cluster LOD API has been consolidated and refactored; previous Clod* types and entry points have been removed or replaced. Callers must update to use MeshletUtility.cs.
2026-03-18 21:18:41 +09:00
9cf03e0b6f Merge pull request 'docs: add XML summary comments to public Meshlet types and methods' (#3) from Julian/GhostEngine:feature/meshlet-docs into develop
Reviewed-on: Misaki/GhostEngine#3
2026-03-17 04:10:16 +00:00
bc78c8fbee docs: add XML summary comments to public Meshlet types and methods 2026-03-17 04:02:28 +00:00
fe49e57330 Merge pull request 'feat: implement ClusterLOD C# bindings in Ghost.Graphics.Meshlet' (#2) from Julian/GhostEngine:develop into develop
Reviewed-on: Misaki/GhostEngine#2
Reviewed-by: Misaki <misaki_39@outlook.com>
2026-03-17 03:52:12 +00:00
27 changed files with 1055 additions and 674 deletions

View File

@@ -16,4 +16,5 @@ Misaki has set up my environment to work from my own fork of "GhostEngine." Here
We'll refine separately; I'll merge when... We'll refine separately; I'll merge when...
...the functionality is complete and tested. ...the functionality is complete and tested.
**PR Description Tip:** Include concise changelog markdown. **PR Description Tip:** write everything in the description, do not include concise changelog markdown.
Also, I should not include AGENT.md and README_julian.md in the PR.

View File

@@ -9,20 +9,19 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
<DefineConstants>$(DefineConstants);PLATEFORME_WIN64</DefineConstants> <DefineConstants>$(DefineConstants);ENABLE_DEBUG_LAYER</DefineConstants>
<IsTrimmable>True</IsTrimmable> <IsTrimmable>True</IsTrimmable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
<DefineConstants>$(DefineConstants);PLATEFORME_WIN64</DefineConstants>
<IsTrimmable>True</IsTrimmable> <IsTrimmable>True</IsTrimmable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.4" /> <PackageReference Include="Misaki.HighPerformance" Version="1.0.4" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.5.1" /> <PackageReference Include="Misaki.HighPerformance.Jobs" Version="1.5.3" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.4.4"> <PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.5.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@@ -72,4 +72,7 @@ public struct PerObjectData
public uint vertexBuffer; public uint vertexBuffer;
public float3 worldBoundsMax; public float3 worldBoundsMax;
public uint indexBuffer; public uint indexBuffer;
public uint meshletBuffer;
public uint meshletVerticesBuffer;
public uint meshletTrianglesBuffer;
}; };

View File

@@ -6,6 +6,8 @@ using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry; using Misaki.HighPerformance.Mathematics.Geometry;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
namespace Ghost.Graphics.Core; namespace Ghost.Graphics.Core;
@@ -68,6 +70,8 @@ public struct Mesh : IResourceReleasable
private UnsafeList<uint> _indices; private UnsafeList<uint> _indices;
private MeshletMeshData _meshletData; private MeshletMeshData _meshletData;
public MeshletMeshData MeshletData => _meshletData;
internal bool IsMeshDataDirty internal bool IsMeshDataDirty
{ {
get; private set; get; private set;
@@ -149,6 +153,22 @@ public struct Mesh : IResourceReleasable
get; internal set; get; internal set;
} }
/// <summary>
/// Gets the handle to the meshlet vertices buffer on the GPU.
/// </summary>
public Handle<GraphicsBuffer> MeshletVerticesBuffer
{
get; internal set;
}
/// <summary>
/// Gets the handle to the meshlet triangles buffer on the GPU.
/// </summary>
public Handle<GraphicsBuffer> MeshletTrianglesBuffer
{
get; internal set;
}
/// <summary> /// <summary>
/// Gets the handle to the mesh data buffer on the GPU. /// Gets the handle to the mesh data buffer on the GPU.
/// </summary> /// </summary>
@@ -176,6 +196,92 @@ public struct Mesh : IResourceReleasable
_meshletData.Dispose(); _meshletData.Dispose();
} }
public unsafe void CookMeshlets()
{
// 1. Prepare Configuration
var config = new ClodConfig
{
maxVertices = 64,
minTriangles = 32,
maxTriangles = 124,
partitionSize = 128,
clusterSpatial = true,
clusterFillWeight = 1.0f,
clusterSplitFactor = 1.0f,
simplifyRatio = 0.5f,
simplifyThreshold = 0.5f,
simplifyErrorMergePrevious = 0.5f,
simplifyErrorMergeAdditive = 0.5f,
simplifyErrorFactorSloppy = 1.0f,
simplifyErrorEdgeLimit = 1.0f,
optimizeBounds = true,
optimizeClusters = true
};
// 2. Map Mesh to ClodMesh
ClodMesh clodMesh = new ClodMesh
{
vertexPositions = (float*)_vertices.GetUnsafePtr(),
vertexCount = (nuint)_vertices.Count,
vertexPositionsStride = (nuint)sizeof(Vertex),
indices = (uint*)_indices.GetUnsafePtr(),
indexCount = (nuint)_indices.Count,
attributeProtectMask = 0
};
// 3. Build
MeshletUtility.Build(config, clodMesh, Unsafe.AsPointer(ref this), MeshletOutputCallback);
}
private static unsafe int MeshletOutputCallback(void* context, ClodGroup group, ClodCluster* clusters, nuint clusterCount)
{
Mesh* mesh = (Mesh*)context;
ref var data = ref mesh->_meshletData;
// Ensure lists are initialized
if (!data.groups.IsCreated) data.groups = new UnsafeList<MeshletGroup>(16, Allocator.Persistent);
if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList<Meshlet>(64, Allocator.Persistent);
if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList<uint>(128, Allocator.Persistent);
if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList<byte>(128, Allocator.Persistent);
var meshletGroup = new MeshletGroup
{
meshletStartIndex = (uint)data.meshlets.Count,
meshletCount = (uint)clusterCount,
lodLevel = (uint)group.depth
};
data.groups.Add(meshletGroup);
for (nuint i = 0; i < clusterCount; i++)
{
var cluster = clusters[i];
var meshlet = new Meshlet
{
vertexCount = (byte)cluster.vertexCount,
triangleCount = (byte)(cluster.indexCount / 3),
vertexOffset = (uint)data.meshletVertices.Count,
triangleOffset = (uint)data.meshletTriangles.Count,
groupIndex = (uint)data.groups.Count - 1
};
data.meshlets.Add(meshlet);
// Add indices
for (nuint j = 0; j < cluster.indexCount; j++)
{
data.meshletVertices.Add(cluster.indices[j]);
}
// Add triangles (packed indices or byte offsets)
// Assuming 8-bit local indices for meshlets as per standard convention
for (nuint j = 0; j < cluster.indexCount; j++)
{
data.meshletTriangles.Add((byte)j);
}
}
return 0;
}
public readonly void ReleaseResource(IResourceDatabase database) public readonly void ReleaseResource(IResourceDatabase database)
{ {
ReleaseCpuResources(); ReleaseCpuResources();
@@ -183,6 +289,8 @@ public struct Mesh : IResourceReleasable
database.ReleaseResource(VertexBuffer.AsResource()); database.ReleaseResource(VertexBuffer.AsResource());
database.ReleaseResource(IndexBuffer.AsResource()); database.ReleaseResource(IndexBuffer.AsResource());
database.ReleaseResource(MeshLetBuffer.AsResource()); database.ReleaseResource(MeshLetBuffer.AsResource());
database.ReleaseResource(MeshletVerticesBuffer.AsResource());
database.ReleaseResource(MeshletTrianglesBuffer.AsResource());
database.ReleaseResource(ObjectDataBuffer.AsResource()); database.ReleaseResource(ObjectDataBuffer.AsResource());
} }
} }

View File

@@ -157,6 +157,68 @@ public readonly unsafe ref struct RenderingContext
} }
} }
public void UploadMeshlets(Handle<Mesh> mesh)
{
var r = _resourceManager.GetMeshReference(mesh);
if (r.IsFailure) return;
ref var meshRef = ref r.Value;
var meshletData = meshRef.MeshletData;
if (!meshletData.meshlets.IsCreated || meshletData.meshlets.Count == 0) return;
var meshletDesc = new BufferDesc
{
Size = (uint)(meshletData.meshlets.Count * sizeof(Meshlet)),
Stride = (uint)sizeof(Meshlet),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
MemoryType = ResourceMemoryType.Default,
};
var verticesDesc = new BufferDesc
{
Size = (uint)(meshletData.meshletVertices.Count * sizeof(uint)),
Stride = sizeof(uint),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
MemoryType = ResourceMemoryType.Default,
};
// Ensure size is multiple of 4 for Raw buffer
var trianglesSize = (uint)meshletData.meshletTriangles.Count;
trianglesSize = (trianglesSize + 3u) & ~3u;
var trianglesDesc = new BufferDesc
{
Size = trianglesSize,
Stride = sizeof(byte),
Usage = BufferUsage.Raw | BufferUsage.ShaderResource,
MemoryType = ResourceMemoryType.Default,
};
meshRef.MeshLetBuffer = _engine.ResourceAllocator.CreateBuffer(in meshletDesc, "Meshlets");
meshRef.MeshletVerticesBuffer = _engine.ResourceAllocator.CreateBuffer(in verticesDesc, "MeshletVertices");
meshRef.MeshletTrianglesBuffer = _engine.ResourceAllocator.CreateBuffer(in trianglesDesc, "MeshletTriangles");
TransitionBarrier(meshRef.MeshLetBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(meshRef.MeshletVerticesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
TransitionBarrier(meshRef.MeshletTrianglesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.CopyDest, BarrierSync.Copy);
_directCmd.UploadBuffer(meshRef.MeshLetBuffer, meshletData.meshlets.AsSpan());
_directCmd.UploadBuffer(meshRef.MeshletVerticesBuffer, meshletData.meshletVertices.AsSpan());
// Padding for triangle data if needed
if (trianglesSize > meshletData.meshletTriangles.Count)
{
var paddedData = new byte[trianglesSize];
meshletData.meshletTriangles.AsSpan().CopyTo(paddedData);
_directCmd.UploadBuffer(meshRef.MeshletTrianglesBuffer, paddedData.AsSpan());
}
else
{
_directCmd.UploadBuffer(meshRef.MeshletTrianglesBuffer, meshletData.meshletTriangles.AsSpan());
}
TransitionBarrier(meshRef.MeshLetBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
TransitionBarrier(meshRef.MeshletVerticesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
TransitionBarrier(meshRef.MeshletTrianglesBuffer.AsResource(), false, BarrierLayout.Undefined, BarrierAccess.ShaderResource, BarrierSync.NonPixelShading | BarrierSync.PixelShading);
}
public void UpdateObjectData(Handle<Mesh> mesh, float4x4 localToWorld) public void UpdateObjectData(Handle<Mesh> mesh, float4x4 localToWorld)
{ {
var r = _resourceManager.GetMeshReference(mesh); var r = _resourceManager.GetMeshReference(mesh);
@@ -173,6 +235,9 @@ public readonly unsafe ref struct RenderingContext
worldBoundsMax = meshData.BoundingBox.Max, worldBoundsMax = meshData.BoundingBox.Max,
vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()), vertexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.VertexBuffer.AsResource()),
indexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.IndexBuffer.AsResource()), indexBuffer = _engine.ResourceDatabase.GetBindlessIndex(meshData.IndexBuffer.AsResource()),
meshletBuffer = meshData.MeshLetBuffer.IsInvalid ? 0 : _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshLetBuffer.AsResource()),
meshletVerticesBuffer = meshData.MeshletVerticesBuffer.IsInvalid ? 0 : _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshletVerticesBuffer.AsResource()),
meshletTrianglesBuffer = meshData.MeshletTrianglesBuffer.IsInvalid ? 0 : _engine.ResourceDatabase.GetBindlessIndex(meshData.MeshletTrianglesBuffer.AsResource()),
}; };
var bufferHandle = meshData.ObjectDataBuffer.AsResource(); var bufferHandle = meshData.ObjectDataBuffer.AsResource();

View File

@@ -1,10 +0,0 @@
using System.Numerics;
namespace Ghost.Graphics.Meshlet;
public struct ClodBounds
{
public Vector3 center;
public float radius;
public float error;
}

View File

@@ -1,48 +0,0 @@
using System;
using System.Numerics;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal static class ClodBoundsHelper
{
public static unsafe ClodBounds ComputeBounds(ClodMesh mesh, UnsafeList<uint> indices, float error)
{
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 unsafe ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group)
{
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.Count,
(nuint)sizeof(ClodBounds),
(float*)boundsList.GetUnsafePtr() + 3,
(nuint)sizeof(ClodBounds)
);
float maxError = 0.0f;
for (int j = 0; j < group.Count; j++)
maxError = Math.Max(maxError, clusters[group[j]].bounds.error);
boundsList.Dispose();
return new ClodBounds
{
center = new Vector3(merged.center[0], merged.center[1], merged.center[2]),
radius = merged.radius,
error = maxError
};
}
}

View File

@@ -1,176 +0,0 @@
using System;
using System.Diagnostics;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal struct Cluster
{
public nuint vertices;
public UnsafeList<uint> indices;
public int group;
public int refined;
public ClodBounds bounds;
}
public unsafe static class ClodBuilder
{
public static nuint Build(ClodConfig config, ClodMesh mesh, void* outputContext, ClodOutputDelegate outputCallback)
{
Debug.Assert(mesh.vertexAttributesStride % (nuint)sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)");
var locks = new UnsafeList<byte>((int)mesh.vertexCount, Allocator.Temp);
locks.AsSpan().Fill(0);
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);
if (mesh.attributeProtectMask != 0)
{
nuint maxAttributes = mesh.vertexAttributesStride / sizeof(float);
for (nuint i = 0; i < mesh.vertexCount; 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])
{
((byte*)locks.GetUnsafePtr())[(int)i] |= (byte)(Api.meshopt_SimplifyVertex_Protect & 0xFF);
}
}
}
}
}
var clusters = ClodInternal.Clusterize(config, mesh, mesh.indices, mesh.indexCount, Allocator.Persistent);
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.Count, Allocator.Temp);
for (int i = 0; i < clusters.Count; i++)
pending.Add(i);
int depth = 0;
while (pending.Count > 1)
{
var groups = ClodPartition.Partition(config, mesh, clusters, pending, remap, Allocator.Temp);
pending.Clear();
ClodBoundary.LockBoundary(locks, groups, clusters, remap, mesh.vertexLock);
for (int i = 0; i < groups.Count; i++)
{
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 < clusterIndices.Count; k++)
merged.Add(clusterIndices[k]);
}
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 ((nuint)simplified.Count > (nuint)(merged.Count * config.simplifyThreshold))
{
bounds.error = float.MaxValue;
OutputGroup(config, mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
merged.Dispose();
continue;
}
bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive;
int refined = OutputGroup(config, mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
for (int j = 0; j < groups[i].Count; j++)
clusters[groups[i][j]].indices.Dispose();
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(clusters.Count - 1);
}
split.Dispose();
merged.Dispose();
}
for (int i = 0; i < groups.Count; i++)
groups[i].Dispose();
groups.Dispose();
depth++;
}
if (pending.Count > 0)
{
var bounds = clusters[pending[0]].bounds;
bounds.error = float.MaxValue;
OutputGroup(config, mesh, clusters, pending, bounds, depth, outputContext, outputCallback);
}
nuint finalClusterCount = (nuint)clusters.Count;
for (int i = 0; i < clusters.Count; i++)
clusters[i].indices.Dispose();
clusters.Dispose();
locks.Dispose();
remap.Dispose();
pending.Dispose();
return finalClusterCount;
}
private static int OutputGroup(
ClodConfig config,
ClodMesh mesh,
UnsafeList<Cluster> clusters,
UnsafeList<int> group,
ClodBounds simplified,
int depth,
void* outputContext,
ClodOutputDelegate outputCallback
)
{
var groupClusters = new UnsafeList<ClodCluster>(group.Count, Allocator.Temp);
for (int i = 0; i < group.Count; i++)
{
ref var srcCluster = ref clusters[group[i]];
groupClusters.Add(new ClodCluster
{
refined = srcCluster.refined,
bounds = (config.optimizeBounds && srcCluster.refined != -1)
? ClodBoundsHelper.ComputeBounds(mesh, srcCluster.indices, srcCluster.bounds.error)
: srcCluster.bounds,
indices = (uint*)srcCluster.indices.GetUnsafePtr(),
indexCount = (nuint)srcCluster.indices.Count,
vertexCount = srcCluster.vertices
});
}
var clodGroup = new ClodGroup { depth = depth, simplified = simplified };
int result = outputCallback != null
? outputCallback(outputContext, clodGroup, (ClodCluster*)groupClusters.GetUnsafePtr(), (nuint)groupClusters.Count)
: -1;
groupClusters.Dispose();
return result;
}
}

View File

@@ -1,37 +0,0 @@
namespace Ghost.Graphics.Meshlet;
public struct ClodConfig
{
public nuint maxVertices;
public nuint minTriangles;
public nuint maxTriangles;
public bool partitionSpatial;
public bool partitionSort;
public nuint partitionSize;
public bool clusterSpatial;
public float clusterFillWeight;
public float clusterSplitFactor;
public float simplifyRatio;
public float simplifyThreshold;
public float simplifyErrorMergePrevious;
public float simplifyErrorMergeAdditive;
public float simplifyErrorFactorSloppy;
public float simplifyErrorEdgeLimit;
public bool simplifyPermissive;
public bool simplifyFallbackPermissive;
public bool simplifyFallbackSloppy;
public bool simplifyRegularize;
public bool optimizeBounds;
public bool optimizeClusters;
}

View File

@@ -1,83 +0,0 @@
using System;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal static class ClodInternal
{
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>((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();
nuint meshletCount;
if (config.clusterSpatial)
{
meshletCount = pMeshlets[0].BuildsSpatial(
pMeshletVertices, pMeshletTriangles,
indices, indexCount,
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
config.maxVertices, config.minTriangles, config.maxTriangles,
config.clusterFillWeight
);
}
else
{
meshletCount = pMeshlets[0].BuildsFlex(
pMeshletVertices, pMeshletTriangles,
indices, indexCount,
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
config.maxVertices, config.minTriangles, config.maxTriangles,
0.0f, config.clusterSplitFactor
);
}
var clusters = new UnsafeList<Cluster>((int)meshletCount, allocator);
for (nuint i = 0; i < meshletCount; i++)
{
ref var meshlet = ref pMeshlets[i];
if (config.optimizeClusters)
{
MeshOptApi.OptimizeMeshlet(
pMeshletVertices + meshlet.vertex_offset,
pMeshletTriangles + meshlet.triangle_offset,
meshlet.triangle_count,
meshlet.vertex_count
);
}
var cluster = new Cluster
{
vertices = meshlet.vertex_count,
indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), allocator),
group = -1,
refined = -1
};
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);
}
meshlets.Dispose();
meshletVertices.Dispose();
meshletTriangles.Dispose();
return clusters;
}
}

View File

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

View File

@@ -1,64 +0,0 @@
using System;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal static class ClodPartition
{
public static unsafe UnsafeList<UnsafeList<int>> Partition(ClodConfig config, ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> pending, UnsafeList<uint> remap, Allocator allocator)
{
if (pending.Count <= (int)config.partitionSize)
{
var single = new UnsafeList<UnsafeList<int>>(1, allocator);
single.Add(pending);
return single;
}
nuint totalIndexCount = 0;
for (int i = 0; i < pending.Count; i++)
totalIndexCount += (nuint)clusters[pending[i]].indices.Count;
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.Count; i++)
{
var cluster = clusters[pending[i]];
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>(pending.Count, Allocator.Temp);
clusterPart.Resize(pending.Count);
nuint partitionCount = MeshOptApi.PartitionClusters(
(uint*)clusterPart.GetUnsafePtr(),
(uint*)clusterIndices.GetUnsafePtr(),
totalIndexCount,
(uint*)clusterCounts.GetUnsafePtr(),
(nuint)pending.Count,
config.partitionSpatial ? mesh.vertexPositions : null,
(nuint)remap.Count,
mesh.vertexPositionsStride,
config.partitionSize
);
var partitions = new UnsafeList<UnsafeList<int>>((int)partitionCount, allocator);
for (nuint i = 0; i < partitionCount; i++)
partitions.Add(new UnsafeList<int>((int)(config.partitionSize + config.partitionSize / 3), allocator));
for (int i = 0; i < pending.Count; i++)
partitions[(int)((uint*)clusterPart.GetUnsafePtr())[i]].Add(pending[i]);
clusterIndices.Dispose();
clusterCounts.Dispose();
clusterPart.Dispose();
return partitions;
}
}

View File

@@ -1,43 +0,0 @@
using System;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
namespace Ghost.Graphics.Meshlet;
public unsafe struct ClodMesh
{
public float* vertexPositions;
public nuint vertexCount;
public nuint vertexPositionsStride;
public float* vertexAttributes;
public nuint vertexAttributesStride;
public float* attributeWeights;
public nuint attributeCount;
public uint* indices;
public nuint indexCount;
public byte* vertexLock;
public uint attributeProtectMask;
}
public struct ClodGroup
{
public int depth;
public ClodBounds simplified;
}
public unsafe struct ClodCluster
{
public int refined;
public ClodBounds bounds;
public uint* indices;
public nuint indexCount;
public nuint vertexCount;
}
public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, ClodCluster* clusters, nuint clusterCount);

View File

@@ -1,109 +0,0 @@
using System;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.Graphics.Meshlet;
internal static class ClodSimplify
{
public static unsafe UnsafeList<uint> Simplify(
ClodConfig config,
ClodMesh mesh,
UnsafeList<uint> indices,
UnsafeList<byte> locks,
nuint targetCount,
float* error
)
{
if (targetCount >= (nuint)indices.Count)
return indices;
var lod = new UnsafeList<uint>(indices.Count, Allocator.Temp);
lod.Resize(indices.Count);
uint options = (uint)(Api.meshopt_SimplifySparse | Api.meshopt_SimplifyErrorAbsolute);
if (config.simplifyPermissive)
options |= (uint)Api.meshopt_SimplifyPermissive;
if (config.simplifyRegularize)
options |= (uint)Api.meshopt_SimplifyRegularize;
nuint resultSize = MeshOptApi.SimplifyWithAttributes(
(uint*)lod.GetUnsafePtr(),
(uint*)indices.GetUnsafePtr(),
(nuint)indices.Count,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
mesh.vertexAttributes,
mesh.vertexAttributesStride,
mesh.attributeWeights,
mesh.attributeCount,
(byte*)locks.GetUnsafePtr(),
targetCount,
float.MaxValue,
options,
error
);
lod.Resize((int)resultSize);
if ((nuint)lod.Count > targetCount && config.simplifyFallbackPermissive && !config.simplifyPermissive)
{
options |= (uint)Api.meshopt_SimplifyPermissive;
resultSize = MeshOptApi.SimplifyWithAttributes(
(uint*)lod.GetUnsafePtr(),
(uint*)indices.GetUnsafePtr(),
(nuint)indices.Count,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
mesh.vertexAttributes,
mesh.vertexAttributesStride,
mesh.attributeWeights,
mesh.attributeCount,
(byte*)locks.GetUnsafePtr(),
targetCount,
float.MaxValue,
options,
error
);
lod.Resize((int)resultSize);
}
if ((nuint)lod.Count > targetCount && config.simplifyFallbackSloppy)
{
*error *= config.simplifyErrorFactorSloppy;
}
if (config.simplifyErrorEdgeLimit > 0)
{
float maxEdgeSq = 0;
uint* pIdx = (uint*)indices.GetUnsafePtr();
int posStride = (int)(mesh.vertexPositionsStride / sizeof(float));
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));
}
*error = Math.Min(*error, (float)Math.Sqrt(maxEdgeSq) * config.simplifyErrorEdgeLimit);
}
return lod;
}
}

View File

@@ -129,8 +129,10 @@ internal sealed class RenderGraphResourceRegistry
for (var i = 0; i < _resources.Count; i++) for (var i = 0; i < _resources.Count; i++)
{ {
if (_resources[i].type == RenderGraphResourceType.Texture) if (_resources[i].type == RenderGraphResourceType.Texture)
{
count++; count++;
} }
}
return count; return count;
} }
} }
@@ -142,8 +144,10 @@ internal sealed class RenderGraphResourceRegistry
for (var i = 0; i < _resources.Count; i++) for (var i = 0; i < _resources.Count; i++)
{ {
if (_resources[i].type == RenderGraphResourceType.Buffer) if (_resources[i].type == RenderGraphResourceType.Buffer)
{
count++; count++;
} }
}
return count; return count;
} }
} }
@@ -290,7 +294,9 @@ internal sealed class RenderGraphResourceRegistry
{ {
var res = _resources[i]; var res = _resources[i];
if (res.type != RenderGraphResourceType.Texture || res.isImported) if (res.type != RenderGraphResourceType.Texture || res.isImported)
{
continue; continue;
}
var desc = res.rgTextureDesc; var desc = res.rgTextureDesc;
if (desc.sizeMode == RGTextureSizeMode.Absolute) if (desc.sizeMode == RGTextureSizeMode.Absolute)

View File

@@ -194,6 +194,16 @@ internal class MeshRenderPass : IRenderPass
MeshBuilder.CreateCube(0.75f, default, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent, out var vertices, out var indices); MeshBuilder.CreateCube(0.75f, default, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent, out var vertices, out var indices);
_mesh = ctx.CreateMesh(vertices, indices, true); _mesh = ctx.CreateMesh(vertices, indices, true);
// Cook meshlets for the mesh
var meshRef = ctx.ResourceManager.GetMeshReference(_mesh);
if (meshRef.IsSuccess)
{
meshRef.Value.CookMeshlets();
}
ctx.UploadMeshlets(_mesh);
ctx.UpdateObjectData(_mesh, float4x4.identity); ctx.UpdateObjectData(_mesh, float4x4.identity);
_textures = new Handle<Texture>[_textureFiles.Length]; _textures = new Handle<Texture>[_textureFiles.Length];

View File

@@ -8,30 +8,80 @@ struct PixelInput
float4 uv : TEXCOORD0; float4 uv : TEXCOORD0;
}; };
[numthreads(3, 1, 1)] // 3 threads per triangle struct Meshlet
{
float4 boundingSphere;
float3 boundingBoxMin;
float3 boundingBoxMax;
uint vertexOffset;
uint triangleOffset;
uint groupIndex;
float parentError;
uint packedCounts; // byte vertexCount, byte triangleCount, byte localMaterialIndex, byte lodLevel
};
[numthreads(64, 1, 1)] // 64 threads for max 64 vertices and up to 124 triangles
[OUTPUT_TRIANGLE_TOPOLOGY] [OUTPUT_TRIANGLE_TOPOLOGY]
void MSMain( void MSMain(
uint3 groupThreadID : SV_GroupThreadID, uint3 groupThreadID : SV_GroupThreadID,
uint groupID : SV_GroupID, uint groupID : SV_GroupID,
out vertices PixelInput outVerts[3], out vertices PixelInput outVerts[64],
out indices uint3 outTris[1]) out indices uint3 outTris[124])
{ {
uint vertexId = groupThreadID.x;
PerObjectData perObjectData = LoadData<PerObjectData>(g_PushConstantData.perObjectBuffer, 0); PerObjectData perObjectData = LoadData<PerObjectData>(g_PushConstantData.perObjectBuffer, 0);
Vertex v = LoadVertexData(vertexId, groupID.x, perObjectData.vertexBuffer, perObjectData.indexBuffer);
SetMeshOutputCounts(3, 1); ByteAddressBuffer meshletBuffer = GET_BUFFER(perObjectData.meshletBuffer);
Meshlet m = meshletBuffer.Load<Meshlet>(groupID.x * sizeof(Meshlet));
uint vertexCount = m.packedCounts & 0xFF;
uint triangleCount = (m.packedCounts >> 8) & 0xFF;
SetMeshOutputCounts(vertexCount, triangleCount);
ByteAddressBuffer meshletVerticesBuffer = GET_BUFFER(perObjectData.meshletVerticesBuffer);
ByteAddressBuffer meshletTrianglesBuffer = GET_BUFFER(perObjectData.meshletTrianglesBuffer);
// Write vertex output // Write vertex output
outVerts[vertexId].position = v.position; if (groupThreadID.x < vertexCount)
outVerts[vertexId].color = v.color;
outVerts[vertexId].uv = v.uv;
// Thread 0 defines topology
if (vertexId == 0)
{ {
outTris[0] = uint3(0, 1, 2); uint vertexIndex = meshletVerticesBuffer.Load((m.vertexOffset + groupThreadID.x) * 4);
ByteAddressBuffer vertices = GET_BUFFER(perObjectData.vertexBuffer);
Vertex v = vertices.Load<Vertex>(vertexIndex * sizeof(Vertex));
// Basic MVP transform not needed if already in world space, but usually we need localToWorld and ViewProj
PerViewData perViewData = LoadData<PerViewData>(g_PushConstantData.perViewBuffer, 0);
float4 worldPos = mul(perObjectData.localToWorld, float4(v.position.xyz, 1.0f));
outVerts[groupThreadID.x].position = mul(perViewData.viewMatrix, worldPos);
outVerts[groupThreadID.x].position = mul(perViewData.projectionMatrix, outVerts[groupThreadID.x].position);
outVerts[groupThreadID.x].color = v.color;
outVerts[groupThreadID.x].uv = v.uv;
}
// Write triangle output (1 thread processes 1 triangle)
// We could pack 3 indices in a uint or just use byte offset
// In our CPU code, we packed it as individual bytes, so 3 bytes per triangle.
// For 124 triangles, we have 372 bytes.
if (groupThreadID.x < triangleCount)
{
uint triangleIndex = groupThreadID.x;
uint baseOffset = m.triangleOffset + triangleIndex * 3;
// Load 4 bytes to get the 3 index bytes
// Needs byte-aligned loading
uint wordOffset = baseOffset & ~3;
uint shift = (baseOffset & 3) * 8;
uint packedIndices1 = meshletTrianglesBuffer.Load(wordOffset);
uint packedIndices2 = meshletTrianglesBuffer.Load(wordOffset + 4);
uint64_t combined = ((uint64_t)packedIndices2 << 32) | packedIndices1;
uint packedIndices = (uint)(combined >> shift);
uint i0 = packedIndices & 0xFF;
uint i1 = (packedIndices >> 8) & 0xFF;
uint i2 = (packedIndices >> 16) & 0xFF;
outTris[triangleIndex] = uint3(i0, i1, i2);
} }
} }

View File

@@ -29,6 +29,9 @@ struct PerObjectData
BYTE_ADDRESS_BUFFER vertexBuffer; BYTE_ADDRESS_BUFFER vertexBuffer;
float3 worldBoundsMax; float3 worldBoundsMax;
BYTE_ADDRESS_BUFFER indexBuffer; BYTE_ADDRESS_BUFFER indexBuffer;
BYTE_ADDRESS_BUFFER meshletBuffer;
BYTE_ADDRESS_BUFFER meshletVerticesBuffer;
BYTE_ADDRESS_BUFFER meshletTrianglesBuffer;
}; };
PushConstantData g_PushConstantData : register(b0); PushConstantData g_PushConstantData : register(b0);

View File

@@ -0,0 +1,620 @@
using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics;
using System.Diagnostics;
namespace Ghost.Graphics.Utilities;
internal struct Cluster : IDisposable
{
public UnsafeList<uint> indices;
public ClodBounds bounds;
public nuint vertices;
public int group;
public int refined;
public void Dispose()
{
indices.Dispose();
}
}
/// <summary>
/// Represents the bounding sphere and simplification error for a LOD cluster.
/// </summary>
public struct ClodBounds
{
/// <summary> The center of the bounding sphere. </summary>
public float3 center;
/// <summary> The radius of the bounding sphere. </summary>
public float radius;
/// <summary> The simplification error associated with this LOD level. </summary>
public float error;
}
/// <summary>
/// Configuration parameters for the cluster LOD generation pipeline.
/// </summary>
public struct ClodConfig
{
/// <summary> The maximum number of vertices per meshlet. </summary>
public nuint maxVertices;
/// <summary> The minimum number of triangles per meshlet. </summary>
public nuint minTriangles;
/// <summary> The maximum number of triangles per meshlet. </summary>
public nuint maxTriangles;
/// <summary> Whether to use spatial partitioning during meshlet building. </summary>
public bool partitionSpatial;
/// <summary> Whether to sort clusters after partitioning. </summary>
public bool partitionSort;
/// <summary> The target size for partitions. </summary>
public nuint partitionSize;
/// <summary> Whether to cluster meshlets using spatial clustering. </summary>
public bool clusterSpatial;
/// <summary> Weight factor for cluster fill calculation. </summary>
public float clusterFillWeight;
/// <summary> Split factor for flexible clustering. </summary>
public float clusterSplitFactor;
/// <summary> The simplification ratio to achieve per LOD level. </summary>
public float simplifyRatio;
/// <summary> Threshold for stopping simplification. </summary>
public float simplifyThreshold;
/// <summary> Error factor used when merging previous LOD level errors. </summary>
public float simplifyErrorMergePrevious;
/// <summary> Additive error factor when merging LOD levels. </summary>
public float simplifyErrorMergeAdditive;
/// <summary> Error factor for sloppy simplification. </summary>
public float simplifyErrorFactorSloppy;
/// <summary> Edge length limit error factor. </summary>
public float simplifyErrorEdgeLimit;
/// <summary> Whether to allow permissive simplification. </summary>
public bool simplifyPermissive;
/// <summary> Whether to fallback to permissive simplification. </summary>
public bool simplifyFallbackPermissive;
/// <summary> Whether to fallback to sloppy simplification. </summary>
public bool simplifyFallbackSloppy;
/// <summary> Whether to regularize the mesh during simplification. </summary>
public bool simplifyRegularize;
/// <summary> Whether to optimize cluster bounds. </summary>
public bool optimizeBounds;
/// <summary> Whether to optimize clusters post-build. </summary>
public bool optimizeClusters;
}
/// <summary>
/// Contains input data for the Cluster LOD generation pipeline.
/// </summary>
public unsafe struct ClodMesh
{
/// <summary> Pointer to vertex position data (float array). </summary>
public float* vertexPositions;
/// <summary> Number of vertices in the mesh. </summary>
public nuint vertexCount;
/// <summary> Stride in bytes for vertex position data. </summary>
public nuint vertexPositionsStride;
/// <summary> Pointer to vertex attribute data (float array). </summary>
public float* vertexAttributes;
/// <summary> Stride in bytes for vertex attribute data. </summary>
public nuint vertexAttributesStride;
/// <summary> Pointer to attribute weights for simplification. </summary>
public float* attributeWeights;
/// <summary> Number of vertex attributes. </summary>
public nuint attributeCount;
/// <summary> Pointer to index data. </summary>
public uint* indices;
/// <summary> Number of indices in the mesh. </summary>
public nuint indexCount;
/// <summary> Pointer to per-vertex lock flags (1 byte per vertex). </summary>
public byte* vertexLock;
/// <summary> Mask indicating which attributes are protected during simplification. </summary>
public uint attributeProtectMask;
}
/// <summary>
/// Defines a group of clusters in the LOD hierarchy.
/// </summary>
public struct ClodGroup
{
/// <summary> LOD hierarchy depth of this group. </summary>
public int depth;
/// <summary> Bounding information for the simplified group. </summary>
public ClodBounds simplified;
}
/// <summary>
/// Represents a cluster of meshlets in the LOD hierarchy.
/// </summary>
public unsafe struct ClodCluster
{
/// <summary> Refinement level of the cluster. </summary>
public int refined;
/// <summary> Bounding info for the cluster. </summary>
public ClodBounds bounds;
/// <summary> Pointer to indices for this cluster. </summary>
public uint* indices;
/// <summary> Number of indices. </summary>
public nuint indexCount;
/// <summary> Number of vertices in the cluster. </summary>
public nuint vertexCount;
}
/// <summary>
/// Delegate type for processing generated LOD groups.
/// </summary>
public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, ClodCluster* clusters, nuint clusterCount);
// FIX: UnsafeList and UnsafeArray are not same as std::vector.
public static unsafe class MeshletUtility
{
private static ClodBounds ComputeBounds(ClodMesh mesh, UnsafeList<uint> indices, float error)
{
var bounds = MeshOptApi.ComputeClusterBounds((uint*)indices.GetUnsafePtr(), (nuint)indices.Count, mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
return new ClodBounds
{
center = new float3(bounds.center[0], bounds.center[1], bounds.center[2]),
radius = bounds.radius,
error = error
};
}
private static ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group)
{
using var boundsList = new UnsafeArray<ClodBounds>(group.Count, Allocator.FreeList);
for (var j = 0; j < group.Count; j++)
{
boundsList[j] = (clusters[group[j]].bounds);
}
var merged = MeshOptApi.ComputeSphereBounds(
(float*)boundsList.GetUnsafePtr(),
(nuint)group.Count,
(nuint)sizeof(ClodBounds),
(float*)boundsList.GetUnsafePtr() + 3,
(nuint)sizeof(ClodBounds)
);
var maxError = 0.0f;
for (var j = 0; j < group.Count; j++)
{
maxError = Math.Max(maxError, clusters[group[j]].bounds.error);
}
return new ClodBounds
{
center = new float3(merged.center[0], merged.center[1], merged.center[2]),
radius = merged.radius,
error = maxError
};
}
private static UnsafeList<Cluster> Clusterize(ClodConfig config, ClodMesh mesh, uint* indices, nuint indexCount, Allocator allocator)
{
var maxMeshlets = MeshOptApi.BuildMeshletsBound(indexCount, config.maxVertices, config.minTriangles);
using var meshlets = new UnsafeArray<meshopt_Meshlet>((int)maxMeshlets, Allocator.FreeList);
using var meshletVertices = new UnsafeArray<uint>((int)indexCount, Allocator.FreeList);
using var meshletTriangles = new UnsafeArray<byte>((int)indexCount, Allocator.FreeList);
var pMeshlets = (meshopt_Meshlet*)meshlets.GetUnsafePtr();
var pMeshletVertices = (uint*)meshletVertices.GetUnsafePtr();
var pMeshletTriangles = (byte*)meshletTriangles.GetUnsafePtr();
nuint meshletCount;
if (config.clusterSpatial)
{
meshletCount = pMeshlets[0].BuildsSpatial(
pMeshletVertices, pMeshletTriangles,
indices, indexCount,
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
config.maxVertices, config.minTriangles, config.maxTriangles,
config.clusterFillWeight
);
}
else
{
meshletCount = pMeshlets[0].BuildsFlex(
pMeshletVertices, pMeshletTriangles,
indices, indexCount,
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
config.maxVertices, config.minTriangles, config.maxTriangles,
0.0f, config.clusterSplitFactor
);
}
var clusters = new UnsafeList<Cluster>((int)meshletCount, allocator);
for (nuint i = 0; i < meshletCount; i++)
{
ref var meshlet = ref pMeshlets[i];
if (config.optimizeClusters)
{
MeshOptApi.OptimizeMeshlet(
pMeshletVertices + meshlet.vertex_offset,
pMeshletTriangles + meshlet.triangle_offset,
meshlet.triangle_count,
meshlet.vertex_count
);
}
var cluster = new Cluster
{
vertices = meshlet.vertex_count,
indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), Allocator.Persistent),
group = -1,
refined = -1
};
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);
}
return clusters;
}
internal static void LockBoundary(UnsafeArray<byte> locks, UnsafeList<UnsafeList<int>> groups, UnsafeList<Cluster> clusters, UnsafeArray<uint> remap, byte* vertexLock)
{
var pLocks = (byte*)locks.GetUnsafePtr();
var pRemap = (uint*)remap.GetUnsafePtr();
for (var i = 0; i < locks.Length; i++)
{
pLocks[i] = unchecked((byte)(pLocks[i] & ~((1 << 0) | (1 << 7))));
}
for (var i = 0; i < groups.Count; i++)
{
for (var j = 0; j < groups[i].Count; j++)
{
var cluster = clusters[groups[i][j]];
for (var k = 0; k < cluster.indices.Count; k++)
{
var r = pRemap[(int)cluster.indices[k]];
pLocks[r] |= (byte)(pLocks[r] >> 7);
}
}
for (var j = 0; j < groups[i].Count; j++)
{
var cluster = clusters[groups[i][j]];
for (var k = 0; k < cluster.indices.Count; k++)
{
var r = pRemap[(int)cluster.indices[k]];
pLocks[r] |= 1 << 7;
}
}
}
for (var i = 0; i < locks.Length; i++)
{
var r = pRemap[i];
pLocks[i] = (byte)((pLocks[r] & 1) | (pLocks[i] & (byte)SimplifyVertexOptions.Protect & 0xFF));
if (vertexLock != null)
{
pLocks[i] |= vertexLock[i];
}
}
}
private static UnsafeList<UnsafeList<int>> Partition(ClodConfig config, ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> pending, UnsafeArray<uint> remap, Allocator allocator)
{
if (pending.Count <= (int)config.partitionSize)
{
var single = new UnsafeList<UnsafeList<int>>(1, allocator);
single.Add(pending);
return single;
}
nuint totalIndexCount = 0;
for (var i = 0; i < pending.Count; i++)
{
totalIndexCount += (nuint)clusters[pending[i]].indices.Count;
}
using var clusterIndices = new UnsafeList<uint>((int)totalIndexCount, Allocator.FreeList);
using var clusterCounts = new UnsafeList<uint>(pending.Count, Allocator.FreeList);
nuint offset = 0;
for (var i = 0; i < pending.Count; i++)
{
var cluster = clusters[pending[i]];
clusterCounts.Add((uint)cluster.indices.Count);
for (var j = 0; j < cluster.indices.Count; j++)
{
clusterIndices.Add(((uint*)remap.GetUnsafePtr())[(int)cluster.indices[j]]);
}
offset += (nuint)cluster.indices.Count;
}
using var clusterPart = new UnsafeArray<uint>(pending.Count, Allocator.FreeList);
var partitionCount = MeshOptApi.PartitionClusters(
(uint*)clusterPart.GetUnsafePtr(),
(uint*)clusterIndices.GetUnsafePtr(),
totalIndexCount,
(uint*)clusterCounts.GetUnsafePtr(),
(nuint)pending.Count,
config.partitionSpatial ? mesh.vertexPositions : null,
(nuint)remap.Length,
mesh.vertexPositionsStride,
config.partitionSize
);
var partitions = new UnsafeList<UnsafeList<int>>((int)partitionCount, allocator);
for (nuint i = 0; i < partitionCount; i++)
{
partitions.Add(new UnsafeList<int>((int)(config.partitionSize + config.partitionSize / 3), allocator));
}
for (var i = 0; i < pending.Count; i++)
{
partitions[(int)((uint*)clusterPart.GetUnsafePtr())[i]].Add(pending[i]);
}
return partitions;
}
private static int OutputGroup(ClodConfig config, ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth, void* outputContext, ClodOutputDelegate? outputCallback)
{
using var groupClusters = new UnsafeList<ClodCluster>(group.Count, Allocator.FreeList);
for (var i = 0; i < group.Count; i++)
{
ref var srcCluster = ref clusters[group[i]];
groupClusters.Add(new ClodCluster
{
refined = srcCluster.refined,
bounds = (config.optimizeBounds && srcCluster.refined != -1)
? ComputeBounds(mesh, srcCluster.indices, srcCluster.bounds.error)
: srcCluster.bounds,
indices = (uint*)srcCluster.indices.GetUnsafePtr(),
indexCount = (nuint)srcCluster.indices.Count,
vertexCount = srcCluster.vertices
});
}
var clodGroup = new ClodGroup { depth = depth, simplified = simplified };
var result = outputCallback != null
? outputCallback(outputContext, clodGroup, (ClodCluster*)groupClusters.GetUnsafePtr(), (nuint)groupClusters.Count)
: -1;
return result;
}
public static UnsafeArray<uint> Simplify(ClodConfig config, ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint targetCount, float* error, Allocator allocator)
{
var lod = new UnsafeArray<uint>(indices.Count, allocator);
if (targetCount >= (nuint)indices.Count)
{
lod.CopyFrom(indices.AsSpan());
return lod;
}
var options = SimplifyOptions.Sparse | SimplifyOptions.ErrorAbsolute;
if (config.simplifyPermissive)
{
options |= SimplifyOptions.Permissive;
}
if (config.simplifyRegularize)
{
options |= SimplifyOptions.Regularize;
}
var resultSize = MeshOptApi.SimplifyWithAttributes(
(uint*)lod.GetUnsafePtr(),
(uint*)indices.GetUnsafePtr(),
(nuint)indices.Count,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
mesh.vertexAttributes,
mesh.vertexAttributesStride,
mesh.attributeWeights,
mesh.attributeCount,
(byte*)locks.GetUnsafePtr(),
targetCount,
float.MaxValue,
options,
error
);
lod.Resize((int)resultSize);
if ((nuint)lod.Length > targetCount && config.simplifyFallbackPermissive && !config.simplifyPermissive)
{
options |= SimplifyOptions.Permissive;
resultSize = MeshOptApi.SimplifyWithAttributes(
(uint*)lod.GetUnsafePtr(),
(uint*)indices.GetUnsafePtr(),
(nuint)indices.Count,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
mesh.vertexAttributes,
mesh.vertexAttributesStride,
mesh.attributeWeights,
mesh.attributeCount,
(byte*)locks.GetUnsafePtr(),
targetCount,
float.MaxValue,
options,
error
);
lod.Resize((int)resultSize);
}
if ((nuint)lod.Length > targetCount && config.simplifyFallbackSloppy)
{
*error *= config.simplifyErrorFactorSloppy;
}
if (config.simplifyErrorEdgeLimit > 0)
{
float maxEdgeSq = 0;
var pIdx = (uint*)indices.GetUnsafePtr();
var posStride = mesh.vertexPositionsStride / (nuint)sizeof(float);
for (var i = 0; i < indices.Count; i += 3)
{
uint a = pIdx[i], b = pIdx[i + 1], c = pIdx[i + 2];
var va = mesh.vertexPositions + (a * posStride);
var vb = mesh.vertexPositions + (b * posStride);
var vc = mesh.vertexPositions + (c * posStride);
float dx, dy, dz;
dx = va[0] - vb[0]; dy = va[1] - vb[1]; dz = va[2] - vb[2];
var eab = dx * dx + dy * dy + dz * dz;
dx = va[0] - vc[0]; dy = va[1] - vc[1]; dz = va[2] - vc[2];
var eac = dx * dx + dy * dy + dz * dz;
dx = vb[0] - vc[0]; dy = vb[1] - vc[1]; dz = vb[2] - vc[2];
var ebc = dx * dx + dy * dy + dz * dz;
var emax = Math.Max(Math.Max(eab, eac), ebc);
var emin = Math.Min(Math.Min(eab, eac), ebc);
maxEdgeSq = Math.Max(maxEdgeSq, Math.Max(emin, emax / 4));
}
*error = Math.Min(*error, (float)Math.Sqrt(maxEdgeSq) * config.simplifyErrorEdgeLimit);
}
return lod;
}
/// <summary>
/// Builds a cluster LOD hierarchy from the input mesh.
/// </summary>
/// <param name="config">The configuration parameters for the LOD building process.</param>
/// <param name="mesh">The input mesh data.</param>
/// <param name="outputContext">Optional context pointer passed to the output callback.</param>
/// <param name="outputCallback">Delegate invoked for each generated LOD group.</param>
/// <returns>The total count of generated clusters.</returns>
public static nuint Build(ClodConfig config, ClodMesh mesh, void* outputContext, ClodOutputDelegate? outputCallback)
{
Debug.Assert(mesh.vertexAttributesStride % sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)");
using var locks = new UnsafeArray<byte>((int)mesh.vertexCount, Allocator.FreeList, AllocationOption.Clear);
using var remap = new UnsafeArray<uint>((int)mesh.vertexCount, Allocator.FreeList);
MeshOptApi.GeneratePositionRemap((uint*)remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
if (mesh.attributeProtectMask != 0)
{
var maxAttributes = mesh.vertexAttributesStride / sizeof(float);
for (nuint i = 0; i < mesh.vertexCount; i++)
{
var 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])
{
((byte*)locks.GetUnsafePtr())[i] |= (byte)SimplifyVertexOptions.Protect & 0xFF;
}
}
}
}
}
using var clusters = Clusterize(config, mesh, mesh.indices, mesh.indexCount, Allocator.FreeList);
for (var i = 0; i < clusters.Count; i++)
{
clusters[i].bounds = ComputeBounds(mesh, clusters[i].indices, 0.0f);
}
using var pending = new UnsafeList<int>(clusters.Count, Allocator.FreeList);
for (var i = 0; i < clusters.Count; i++)
{
pending.Add(i);
}
var depth = 0;
while (pending.Count > 1)
{
using var groups = Partition(config, mesh, clusters, pending, remap, Allocator.FreeList);
pending.Clear();
LockBoundary(locks, groups, clusters, remap, mesh.vertexLock);
for (var i = 0; i < groups.Count; i++)
{
using var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, Allocator.FreeList);
for (var j = 0; j < groups[i].Count; j++)
{
var clusterIndices = clusters[groups[i][j]].indices;
for (var k = 0; k < clusterIndices.Count; k++)
{
merged.Add(clusterIndices[k]);
}
}
var targetSize = ((nuint)merged.Count / 3) * (nuint)config.simplifyRatio * 3;
var bounds = MergeBounds(clusters, groups[i]);
var error = 0.0f;
using var simplified = Simplify(config, mesh, merged.AsReadOnly(), locks.AsReadOnly(), targetSize, &error, Allocator.FreeList);
if ((nuint)simplified.Length > (nuint)(merged.Count * config.simplifyThreshold))
{
bounds.error = float.MaxValue;
OutputGroup(config, mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
continue;
}
bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive;
var refined = OutputGroup(config, mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
for (var j = 0; j < groups[i].Count; j++)
{
clusters[groups[i][j]].Dispose();
}
using var split = Clusterize(config, mesh, (uint*)simplified.GetUnsafePtr(), (nuint)simplified.Length, Allocator.FreeList);
for (var j = 0; j < split.Count; j++)
{
split[j].refined = refined;
split[j].bounds = bounds;
clusters.Add(split[j]);
pending.Add(clusters.Count - 1);
}
}
for (var i = 0; i < groups.Count; i++)
{
groups[i].Dispose();
}
depth++;
}
if (pending.Count > 0)
{
var bounds = clusters[pending[0]].bounds;
bounds.error = float.MaxValue;
OutputGroup(config, mesh, clusters, pending, bounds, depth, outputContext, outputCallback);
}
var finalClusterCount = (nuint)clusters.Count;
for (var i = 0; i < clusters.Count; i++)
{
clusters[i].Dispose();
}
return finalClusterCount;
}
}

View File

@@ -0,0 +1,90 @@
namespace Ghost.MeshOptimizer;
[Flags]
public enum SimplifyOptions : uint
{
LockBorder = 1 << 0,
Sparse = 1 << 1,
ErrorAbsolute = 1 << 2,
Prune = 1 << 3,
Regularize = 1 << 4,
Permissive = 1 << 5
}
[Flags]
public enum SimplifyVertexOptions : byte
{
Lock = 1 << 0,
Protect = 1 << 1
}
public unsafe partial struct MeshOptApi
{
public const int VERSION = Api.MESHOPTIMIZER_VERSION;
/// <summary>
/// From: <see cref="Api.meshopt_simplify(uint*, uint*, nuint, float*, nuint, nuint, nuint, float, uint, float*)" />
/// </summary>
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static nuint Simplify(uint* destination, uint* indices, nuint index_count, float* vertex_positions, nuint vertex_count, nuint vertex_positions_stride, nuint target_index_count, float target_error, SimplifyOptions options, float* result_error)
{
return Api.meshopt_simplify(
destination,
indices,
index_count,
vertex_positions,
vertex_count,
vertex_positions_stride,
target_index_count,
target_error,
(uint)options,
result_error);
}
/// <summary>
/// From: <see cref="Api.meshopt_simplifyWithAttributes(uint*, uint*, nuint, float*, nuint, nuint, float*, nuint, float*, nuint, byte*, nuint, float, uint, float*)" />
/// </summary>
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static nuint SimplifyWithAttributes(uint* destination, uint* indices, nuint index_count, float* vertex_positions, nuint vertex_count, nuint vertex_positions_stride, float* vertex_attributes, nuint vertex_attributes_stride, float* attribute_weights, nuint attribute_count, byte* vertex_lock, nuint target_index_count, float target_error, SimplifyOptions options, float* result_error)
{
return Api.meshopt_simplifyWithAttributes(
destination,
indices,
index_count,
vertex_positions,
vertex_count,
vertex_positions_stride,
vertex_attributes,
vertex_attributes_stride,
attribute_weights,
attribute_count,
vertex_lock,
target_index_count,
target_error,
(uint)options,
result_error);
}
/// <summary>
/// From: <see cref="Api.meshopt_simplifyWithUpdate(uint*, nuint, float*, nuint, nuint, float*, nuint, float*, nuint, byte*, nuint, float, uint, float*)" />
/// </summary>
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static nuint SimplifyWithUpdate(uint* indices, nuint index_count, float* vertex_positions, nuint vertex_count, nuint vertex_positions_stride, float* vertex_attributes, nuint vertex_attributes_stride, float* attribute_weights, nuint attribute_count, byte* vertex_lock, nuint target_index_count, float target_error, SimplifyOptions options, float* result_error)
{
return Api.meshopt_simplifyWithUpdate(
indices,
index_count,
vertex_positions,
vertex_count,
vertex_positions_stride,
vertex_attributes,
vertex_attributes_stride,
attribute_weights,
attribute_count,
vertex_lock,
target_index_count,
target_error,
(uint)options,
result_error);
}
}

View File

@@ -276,7 +276,7 @@ public sealed class WrapperGeneratorEmitter
{ {
var func = routed.Function; var func = routed.Function;
var nameOpts = routed.Apply.Opts?.name; var nameOpts = routed.Apply.Opts?.name;
var methodName = naming.GetMethodName(func.Name, nameOpts, routed.TargetStructName); var methodName = naming.GetName(func.Name, nameOpts, routed.TargetStructName);
// Build the parameter plan: for each native parameter, determine the public type // Build the parameter plan: for each native parameter, determine the public type
// and how to pass it to the Api call (applying remaps). // and how to pass it to the Api call (applying remaps).

View File

@@ -18,7 +18,6 @@ public sealed class NativeStruct
public required bool IsList { get; init; } public required bool IsList { get; init; }
public required bool IsPointerList { get; init; } public required bool IsPointerList { get; init; }
public string? ListElementType { get; init; } public string? ListElementType { get; init; }
public required bool IsElementLike { get; init; }
} }
public sealed class NativeEnum public sealed class NativeEnum
@@ -52,4 +51,5 @@ public enum NativeMemberKind
{ {
Field, Field,
Property, Property,
Constant,
} }

View File

@@ -10,6 +10,7 @@ public sealed class BindingParser
{ {
public NativeLibrary Parse(string inputDirectory, WrapperConfig config) public NativeLibrary Parse(string inputDirectory, WrapperConfig config)
{ {
var members = new List<NativeMember>();
var structs = new List<NativeStruct>(); var structs = new List<NativeStruct>();
var enums = new List<NativeEnum>(); var enums = new List<NativeEnum>();
var functions = new List<NativeFunction>(); var functions = new List<NativeFunction>();
@@ -33,18 +34,17 @@ public sealed class BindingParser
continue; continue;
} }
var members = ParseMembers(@struct); var structMembers = ParseMembers(@struct);
var listInfo = TryMatchList(members); var listInfo = TryMatchList(structMembers);
structs.Add(new NativeStruct structs.Add(new NativeStruct
{ {
Name = @struct.Identifier.ValueText, Name = @struct.Identifier.ValueText,
Namespace = namespaceName, Namespace = namespaceName,
Members = members, Members = structMembers,
IsList = listInfo.IsList, IsList = listInfo.IsList,
IsPointerList = listInfo.IsPointerList, IsPointerList = listInfo.IsPointerList,
ListElementType = listInfo.ListElementType, ListElementType = listInfo.ListElementType,
IsElementLike = members.Any(static m => m.Name == "element" && m.TypeName == "ufbx_element"),
}); });
} }

View File

@@ -17,13 +17,13 @@ public sealed class NamingConventions
/// ///
/// Supported remove tokens: /// Supported remove tokens:
/// "PREFIX" — strip the config's NativeTypePrefix from the start (e.g. "nvtt", "ufbx_") /// "PREFIX" — strip the config's NativeTypePrefix from the start (e.g. "nvtt", "ufbx_")
/// "NO_PREFIX($TSelf)" — strip the target struct name minus its type prefix from the start, /// "$TBare" — strip the target struct name minus its type prefix from the start,
/// case-insensitively (e.g. NvttSurface → "Surface" stripped from "SurfaceWidth") /// case-insensitively (e.g. NvttSurface → "Surface" stripped from "SurfaceWidth")
/// ///
/// nameOpts is the dynamic opts.name object from JSON (may be null). /// nameOpts is the dynamic opts.name object from JSON (may be null).
/// If no nameOpts are provided, the name is returned with only the library prefix stripped. /// If no nameOpts are provided, the name is returned with only the library prefix stripped.
/// </summary> /// </summary>
public string GetMethodName(string nativeFunctionName, dynamic? nameOpts, string targetStructName) public string GetName(string nativeFunctionName, dynamic? nameOpts, string targetStructName)
{ {
var name = nativeFunctionName; var name = nativeFunctionName;
@@ -47,13 +47,8 @@ public sealed class NamingConventions
{ {
name = StripPrefixIgnoreCase(name, _config.NativeTypePrefix); name = StripPrefixIgnoreCase(name, _config.NativeTypePrefix);
} }
else if (token.StartsWith("NO_PREFIX(", StringComparison.Ordinal) && token.EndsWith(')')) else if (string.Equals(token, "$TBare", StringComparison.Ordinal))
{ {
// Extract $TSelf — it's the literal token "NO_PREFIX($TSelf)", so the struct name
// is resolved from the targetStructName argument passed in.
// Strip the config prefix from the struct name to get the "bare" part.
// Try prefix first, then suffix (handles both nvtt "SurfaceWidth"→"Width"
// and ufbx "free_scene"→"free_" styles).
var bareStructName = StripPrefixIgnoreCase(targetStructName, _config.NativeTypePrefix); var bareStructName = StripPrefixIgnoreCase(targetStructName, _config.NativeTypePrefix);
// Remove directly, the name maybe nvttSetOutputOptionsOutputHeader, if we only remove prefix and suffix, OutputOptions in the middle will be ignored, so we remove the bare struct name directly, case-insensitively. // Remove directly, the name maybe nvttSetOutputOptionsOutputHeader, if we only remove prefix and suffix, OutputOptions in the middle will be ignored, so we remove the bare struct name directly, case-insensitively.
@@ -66,7 +61,7 @@ public sealed class NamingConventions
var style = nameOpts.style as string; var style = nameOpts.style as string;
if (!string.IsNullOrEmpty(style)) if (!string.IsNullOrEmpty(style))
{ {
if (string.Equals(style, "PascalCase", StringComparison.OrdinalIgnoreCase)) if (string.Equals(style, "PascalCase", StringComparison.Ordinal))
{ {
int counter = 0; int counter = 0;
Span<char> nameSpan = stackalloc char[name.Length]; Span<char> nameSpan = stackalloc char[name.Length];
@@ -83,10 +78,10 @@ public sealed class NamingConventions
if (name[i] == '_') if (name[i] == '_')
{ {
while (name[i] == '_' && i < name.Length) do
{ {
i++; i++;
} } while (i < name.Length && name[i] == '_');
nameSpan[counter] = char.ToUpperInvariant(name[i]); nameSpan[counter] = char.ToUpperInvariant(name[i]);
counter++; counter++;
@@ -98,6 +93,54 @@ public sealed class NamingConventions
counter++; counter++;
} }
name = nameSpan[..counter].ToString();
}
else if (string.Equals(style, "ALL_CAPS", StringComparison.Ordinal))
{
int counter = 0;
Span<char> nameSpan = stackalloc char[name.Length * 2]; // Worst case, every character is uppercase and followed by an underscore.
for (int i = 0; i < name.Length; i++)
{
// ___ to _
if (name[i] == '_')
{
while (i + 1 < name.Length && name[i + 1] == '_')
{
i++;
}
nameSpan[counter] = '_';
counter++;
continue;
}
// AbC to AB_C
if (i > 0 && char.IsUpper(name[i]) && char.IsLower(name[i - 1]))
{
nameSpan[counter] = '_';
counter++;
}
// ABC to ABC
while (i < name.Length && char.IsUpper(name[i]))
{
nameSpan[counter] = name[i];
counter++;
i++;
}
if (i == name.Length)
{
break;
}
nameSpan[counter] = char.ToUpperInvariant(name[i]);
counter++;
}
name = nameSpan[..counter].ToString(); name = nameSpan[..counter].ToString();
} }
} }

View File

@@ -24,7 +24,7 @@
"name": { "name": {
"remove": [ "remove": [
"PREFIX", "PREFIX",
"NO_PREFIX($TSelf)" // NO_PREFIX(NvttSurface) will change "NvttSurface" to "Surface", the prefix is determined by the "nativeTypePrefix" field at the top level of this config "$TBare" // NO_PREFIX(NvttSurface) will change "NvttSurface" to "Surface", the prefix is determined by the "nativeTypePrefix" field at the top level of this config
], ],
"style": "PascalCase" "style": "PascalCase"
} }
@@ -43,7 +43,7 @@
"name": { "name": {
"remove": [ "remove": [
"PREFIX", "PREFIX",
"NO_PREFIX($TSelf)" "$TBare"
], ],
"style": "PascalCase" "style": "PascalCase"
} }
@@ -64,6 +64,21 @@
} }
} }
} }
},
{
"filter": "CONST",
"targetType": "MeshOptApi",
"apply": {
"type": "CONST",
"opts": {
"name": {
"remove": [
"PREFIX"
],
"style": "ALL_CAPS"
}
}
}
} }
] ]
} }

View File

@@ -68,7 +68,7 @@
"name": { "name": {
"remove": [ "remove": [
"PREFIX", "PREFIX",
"NO_PREFIX($TSelf)" // NO_PREFIX(NvttSurface) will change "NvttSurface" to "Surface", the prefix is determined by the "nativeTypePrefix" field at the top level of this config "$TBare" // NO_PREFIX(NvttSurface) will change "NvttSurface" to "Surface", the prefix is determined by the "nativeTypePrefix" field at the top level of this config
] ]
} }
} }
@@ -86,21 +86,7 @@
"name": { "name": {
"remove": [ "remove": [
"PREFIX", "PREFIX",
"NO_PREFIX($TSelf)" "$TBare"
]
}
}
}
},
{
"filter": "EXTERN_API",
"targetType": "NvttApi",
"apply": {
"type": "STATIC_METHOD",
"opts": {
"name": {
"remove": [
"PREFIX"
] ]
} }
} }

View File

@@ -68,7 +68,7 @@
"name": { "name": {
"remove": [ "remove": [
"PREFIX", "PREFIX",
"NO_PREFIX($TSelf)" // NO_PREFIX(ufbx_scene) will output "scene" change "free_scene" to "free", the prefix is determined by the "nativeTypePrefix" field at the top level of this config "$TBare" // NO_PREFIX(ufbx_scene) will output "scene" change "free_scene" to "free", the prefix is determined by the "nativeTypePrefix" field at the top level of this config
], ],
"style": "PascalCase" "style": "PascalCase"
} }
@@ -87,7 +87,7 @@
"name": { "name": {
"remove": [ "remove": [
"PREFIX", "PREFIX",
"NO_PREFIX($TSelf)" "$TBare"
], ],
"style": "PascalCase" "style": "PascalCase"
} }