Refactor asset import API and mesh streaming pipeline

- Standardize IImportableAssetHandler.ImportAsync to return sub-asset results
- Remove ISubAssetImportableAssetHandler, merge into main interface
- Update FBX/Texture handlers for new import contract
- Add StreamUtility for efficient (async) binary writes
- Refactor meshlet/LOD building to use ref structs and safe memory
- Use new streaming utilities in mesh import/export and tests
- AssetCatalog.Remove now recursively deletes sub-assets
- Improve asset registry file watcher for better change detection
- Log unhandled exceptions in App instead of breaking
- Add interpolated collection support to NativeMemoryManager
- Update project references and fix minor bugs
This commit is contained in:
2026-05-05 17:19:24 +09:00
parent 8d3e1c91d7
commit 5de480e231
15 changed files with 214 additions and 180 deletions

View File

@@ -45,17 +45,12 @@ public interface IAssetHandler
public interface IImportableAssetHandler : IAssetHandler public interface IImportableAssetHandler : IAssetHandler
{ {
bool CanExport { get; } bool CanExport { get; }
ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default); ValueTask<Result<ImportedSubAsset[]>> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default);
ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default); ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default);
} }
public readonly record struct ImportedSubAsset(Guid Guid, string Kind, string DisplayName, string StablePath, string VirtualSourcePath, Guid HandlerTypeId); public readonly record struct ImportedSubAsset(Guid Guid, string Kind, string DisplayName, string StablePath, string VirtualSourcePath, Guid HandlerTypeId);
public interface ISubAssetImportableAssetHandler : IImportableAssetHandler
{
ValueTask<Result<ImportedSubAsset[]>> ImportWithSubAssetsAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default);
}
public interface IPackableAssetHandler : IAssetHandler public interface IPackableAssetHandler : IAssetHandler
{ {
ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default); ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default);

View File

@@ -1,6 +1,7 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Engine; using Ghost.Core.Utilities;
using Ghost.Editor.Core.Services; using Ghost.Editor.Core.Services;
using Ghost.Engine;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
@@ -382,7 +383,7 @@ internal class FbxAssetSettings : MeshAssetSettings
} }
[CustomAssetHandler(FBXAsset.GUID, [".fbx", ".obj"], 1)] [CustomAssetHandler(FBXAsset.GUID, [".fbx", ".obj"], 1)]
internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAssetHandler internal class FBXAssetHandler : IImportableAssetHandler, IPackableAssetHandler
{ {
public AssetType RuntimeAssetType => AssetType.Mesh; public AssetType RuntimeAssetType => AssetType.Mesh;
@@ -422,12 +423,7 @@ internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAsset
return ValueTask.FromResult(Result.Failure("Saving model assets is not supported yet.")); return ValueTask.FromResult(Result.Failure("Saving model assets is not supported yet."));
} }
public async ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default) public async ValueTask<Result<ImportedSubAsset[]>> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
{
return await ImportWithSubAssetsAsync(sourcePath, targetPath, id, settings, token).ConfigureAwait(false);
}
public async ValueTask<Result<ImportedSubAsset[]>> ImportWithSubAssetsAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
{ {
if (!File.Exists(sourcePath)) if (!File.Exists(sourcePath))
{ {
@@ -557,72 +553,69 @@ internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAsset
return manifestNode; return manifestNode;
} }
private static ValueTask<(int materialSlotCount, int lodLevelCount)> WriteMeshContentAsync(string targetPath, GeometryMeshNode geometry, CancellationToken token) private static async ValueTask<(int materialSlotCount, int lodLevelCount)> WriteMeshContentAsync(string targetPath, GeometryMeshNode geometry, CancellationToken token)
{ {
unsafe var meshletData = new MeshletMeshData();
try
{ {
var meshletData = new MeshletMeshData(); MeshProcessor.BuildMeshlets(ref meshletData, geometry.Vertices.AsReadOnly(), geometry.Indices.AsReadOnly(), geometry.MaterialParts.AsSpan());
try MeshProcessor.BuildClusterLodHierarchy(ref meshletData);
var bounds = ComputeBounds(geometry.Vertices);
var header = new MeshContentHeader
{ {
MeshProcessor.BuildMeshlets(&meshletData, geometry.Vertices.AsReadOnly(), geometry.Indices.AsReadOnly(), geometry.MaterialParts.AsSpan()); magic = MeshContentHeader.MAGIC,
MeshProcessor.BuildClusterLodHierarchy(&meshletData); version = MeshContentHeader.VERSION,
vertexCount = (uint)geometry.Vertices.Count,
indexCount = (uint)geometry.Indices.Count,
materialPartCount = (uint)geometry.MaterialParts.Length,
meshletCount = (uint)meshletData.meshlets.Count,
meshletGroupCount = (uint)meshletData.groups.Count,
meshletHierarchyNodeCount = (uint)meshletData.hierarchyNodes.Count,
meshletVertexCount = (uint)meshletData.meshletVertices.Count,
meshletTriangleCount = (uint)meshletData.meshletTriangles.Count,
materialSlotCount = (uint)meshletData.materialSlotCount,
lodLevelCount = (uint)meshletData.lodLevelCount,
boundsMin = bounds.Min,
boundsMax = bounds.Max,
};
var bounds = ComputeBounds(geometry.Vertices); using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
var header = new MeshContentHeader stream.Write(header);
{
magic = MeshContentHeader.MAGIC,
version = MeshContentHeader.VERSION,
vertexCount = (uint)geometry.Vertices.Count,
indexCount = (uint)geometry.Indices.Count,
materialPartCount = (uint)geometry.MaterialParts.Length,
meshletCount = (uint)meshletData.meshlets.Count,
meshletGroupCount = (uint)meshletData.groups.Count,
meshletHierarchyNodeCount = (uint)meshletData.hierarchyNodes.Count,
meshletVertexCount = (uint)meshletData.meshletVertices.Count,
meshletTriangleCount = (uint)meshletData.meshletTriangles.Count,
materialSlotCount = (uint)meshletData.materialSlotCount,
lodLevelCount = (uint)meshletData.lodLevelCount,
boundsMin = bounds.Min,
boundsMax = bounds.Max,
};
using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None); header.vertexOffset = (ulong)stream.Position;
WriteStruct(stream, in header); await stream.WriteAsync<Vertex, UnsafeList<Vertex>>(geometry.Vertices, token);
header.vertexOffset = (ulong)stream.Position; header.indexOffset = (ulong)stream.Position;
WriteSpan(stream, geometry.Vertices.AsSpan()); await stream.WriteAsync<uint, UnsafeList<uint>>(geometry.Indices, token);
header.indexOffset = (ulong)stream.Position; header.materialPartOffset = (ulong)stream.Position;
WriteSpan(stream, geometry.Indices.AsSpan()); WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
header.materialPartOffset = (ulong)stream.Position; header.meshletOffset = (ulong)stream.Position;
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan()); await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.meshlets, token);
header.meshletOffset = (ulong)stream.Position; header.meshletGroupOffset = (ulong)stream.Position;
WriteSpan(stream, meshletData.meshlets.AsSpan()); await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.groups, token);
header.meshletGroupOffset = (ulong)stream.Position; header.meshletHierarchyNodeOffset = (ulong)stream.Position;
WriteSpan(stream, meshletData.groups.AsSpan()); await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.hierarchyNodes, token);
header.meshletHierarchyNodeOffset = (ulong)stream.Position; header.meshletVertexOffset = (ulong)stream.Position;
WriteSpan(stream, meshletData.hierarchyNodes.AsSpan()); await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.meshletVertices, token);
header.meshletVertexOffset = (ulong)stream.Position; header.meshletTriangleOffset = (ulong)stream.Position;
WriteSpan(stream, meshletData.meshletVertices.AsSpan()); await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.meshletTriangles, token);
header.meshletTriangleOffset = (ulong)stream.Position; stream.Position = 0;
WriteSpan(stream, meshletData.meshletTriangles.AsSpan()); stream.Write(header);
stream.Flush();
stream.Position = 0; return (meshletData.materialSlotCount, meshletData.lodLevelCount);
WriteStruct(stream, in header); }
stream.Flush(); finally
{
return ValueTask.FromResult((meshletData.materialSlotCount, meshletData.lodLevelCount)); meshletData.Dispose();
}
finally
{
meshletData.Dispose();
}
} }
} }
@@ -660,7 +653,7 @@ internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAsset
} }
var chars = value.ToCharArray(); var chars = value.ToCharArray();
for (var i = 0; i < chars.Length; i++) for (var i = 0; i < value.Length; i++)
{ {
if (chars[i] == '/' || chars[i] == '\\' || chars[i] == '#') if (chars[i] == '/' || chars[i] == '\\' || chars[i] == '#')
{ {
@@ -678,7 +671,7 @@ internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAsset
return; return;
} }
Span<MeshContentMaterialPart> buffer = parts.Length <= 64 var buffer = parts.Length <= 64
? stackalloc MeshContentMaterialPart[parts.Length] ? stackalloc MeshContentMaterialPart[parts.Length]
: new MeshContentMaterialPart[parts.Length]; : new MeshContentMaterialPart[parts.Length];
@@ -694,24 +687,6 @@ internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAsset
}; };
} }
WriteSpan(stream, buffer); stream.Write(buffer);
}
private static void WriteStruct<T>(Stream stream, ref readonly T value)
where T : unmanaged
{
var span = MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in value, 1));
stream.Write(span);
}
private static void WriteSpan<T>(Stream stream, ReadOnlySpan<T> value)
where T : unmanaged
{
if (value.IsEmpty)
{
return;
}
stream.Write(MemoryMarshal.AsBytes(value));
} }
} }

View File

@@ -10,6 +10,7 @@ using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
@@ -126,7 +127,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
for (var i = 0; i < numMaterials; i++) for (var i = 0; i < numMaterials; i++)
{ {
materialBuckets[i] = new UnsafeList<Vertex>(1024, AllocationHandle.FreeList); materialBuckets[i] = new UnsafeList<Vertex>(40960, AllocationHandle.FreeList);
} }
var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u); var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u);

View File

@@ -158,13 +158,10 @@ public unsafe struct ClodCluster
public nuint localIndexCount; public nuint localIndexCount;
} }
/// <summary>
/// Delegate type for processing generated LOD groups.
/// </summary>
public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters);
public static unsafe partial class MeshProcessor public static unsafe partial class MeshProcessor
{ {
private delegate int ClodOutputDelegate(ref MeshletContext context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters);
private static ClodBounds ComputeBounds(ref readonly ClodMesh mesh, UnsafeList<uint> indices, float error) private static ClodBounds ComputeBounds(ref readonly ClodMesh mesh, UnsafeList<uint> indices, float error)
{ {
var bounds = MeshOptApi.ComputeClusterBounds((uint*)indices.GetUnsafePtr(), (nuint)indices.Count, mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride); var bounds = MeshOptApi.ComputeClusterBounds((uint*)indices.GetUnsafePtr(), (nuint)indices.Count, mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
@@ -391,7 +388,7 @@ public static unsafe partial class MeshProcessor
return partitions; return partitions;
} }
private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth, void* outputContext, ClodOutputDelegate? outputCallback) private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth, ref MeshletContext outputContext, ClodOutputDelegate? outputCallback)
{ {
using var groupClusters = new UnsafeList<ClodCluster>(group.Count, AllocationHandle.FreeList); using var groupClusters = new UnsafeList<ClodCluster>(group.Count, AllocationHandle.FreeList);
@@ -415,7 +412,7 @@ public static unsafe partial class MeshProcessor
var clodGroup = new ClodGroup { depth = depth, simplified = simplified }; var clodGroup = new ClodGroup { depth = depth, simplified = simplified };
var result = outputCallback != null var result = outputCallback != null
? outputCallback(outputContext, clodGroup, groupClusters.AsReadOnly()) ? outputCallback(ref outputContext, clodGroup, groupClusters.AsReadOnly())
: -1; : -1;
return result; return result;
@@ -575,7 +572,7 @@ public static unsafe partial class MeshProcessor
/// <param name="outputContext">Optional context pointer passed to the output callback.</param> /// <param name="outputContext">Optional context pointer passed to the output callback.</param>
/// <param name="outputCallback">Delegate invoked for each generated LOD group.</param> /// <param name="outputCallback">Delegate invoked for each generated LOD group.</param>
/// <returns>The total count of generated clusters.</returns> /// <returns>The total count of generated clusters.</returns>
public static nuint Build(ref readonly ClodConfig config, ref readonly ClodMesh mesh, void* outputContext, ClodOutputDelegate? outputCallback) private static nuint Build(ref readonly ClodConfig config, ref readonly ClodMesh mesh, ref MeshletContext outputContext, ClodOutputDelegate? outputCallback)
{ {
Logger.DebugAssert(mesh.vertexAttributesStride % sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)"); Logger.DebugAssert(mesh.vertexAttributesStride % sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)");
@@ -643,13 +640,13 @@ public static unsafe partial class MeshProcessor
if ((nuint)simplified.Length > (nuint)(merged.Count * config.simplifyThreshold)) if ((nuint)simplified.Length > (nuint)(merged.Count * config.simplifyThreshold))
{ {
bounds.error = float.MaxValue; bounds.error = float.MaxValue;
OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback); OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, ref outputContext, outputCallback);
continue; continue;
} }
bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive; bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive;
var refined = OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback); var refined = OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, ref outputContext, outputCallback);
for (var j = 0; j < groups[i].Count; j++) for (var j = 0; j < groups[i].Count; j++)
{ {
@@ -678,7 +675,7 @@ public static unsafe partial class MeshProcessor
{ {
var bounds = clusters[pending[0]].bounds; var bounds = clusters[pending[0]].bounds;
bounds.error = float.MaxValue; bounds.error = float.MaxValue;
OutputGroup(in config, in mesh, clusters, pending, bounds, depth, outputContext, outputCallback); OutputGroup(in config, in mesh, clusters, pending, bounds, depth, ref outputContext, outputCallback);
} }
var finalClusterCount = (nuint)clusters.Count; var finalClusterCount = (nuint)clusters.Count;
@@ -691,34 +688,33 @@ public static unsafe partial class MeshProcessor
return finalClusterCount; return finalClusterCount;
} }
private struct MeshletContext private ref struct MeshletContext
{ {
public MeshletMeshData* data; public ref MeshletMeshData data;
public int materialIndex; public int materialIndex;
} }
private static int MeshletOutputCallback(void* contextPtr, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters) private static int MeshletOutputCallback(ref MeshletContext context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters)
{ {
var context = (MeshletContext*)contextPtr; ref var meshletData = ref context.data;
var pMeshletData = context->data; var materialIndex = context.materialIndex;
var materialIndex = context->materialIndex;
// Ensure lists are initialized // Ensure lists are initialized
if (!pMeshletData->groups.IsCreated) pMeshletData->groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent); if (!meshletData.groups.IsCreated) meshletData.groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent);
if (!pMeshletData->meshlets.IsCreated) pMeshletData->meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent); if (!meshletData.meshlets.IsCreated) meshletData.meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent);
if (!pMeshletData->meshletVertices.IsCreated) pMeshletData->meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent); if (!meshletData.meshletVertices.IsCreated) meshletData.meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent);
if (!pMeshletData->meshletTriangles.IsCreated) pMeshletData->meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.Persistent); if (!meshletData.meshletTriangles.IsCreated) meshletData.meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.Persistent);
var meshletGroup = new MeshletGroup var meshletGroup = new MeshletGroup
{ {
boundingSphere = new SphereBounds(group.simplified.center, group.simplified.radius), boundingSphere = new SphereBounds(group.simplified.center, group.simplified.radius),
boundingBox = new AABB(group.simplified.center - group.simplified.radius, group.simplified.center + group.simplified.radius), boundingBox = new AABB(group.simplified.center - group.simplified.radius, group.simplified.center + group.simplified.radius),
parentError = group.simplified.error, parentError = group.simplified.error,
meshletStartIndex = (uint)pMeshletData->meshlets.Count, meshletStartIndex = (uint)meshletData.meshlets.Count,
meshletCount = (uint)clusters.Count, meshletCount = (uint)clusters.Count,
lodLevel = (uint)group.depth lodLevel = (uint)group.depth
}; };
pMeshletData->groups.Add(meshletGroup); meshletData.groups.Add(meshletGroup);
for (var i = 0; i < clusters.Count; i++) for (var i = 0; i < clusters.Count; i++)
{ {
@@ -731,20 +727,20 @@ public static unsafe partial class MeshProcessor
boundingBox = new AABB(cluster.bounds.center - cluster.bounds.radius, cluster.bounds.center + cluster.bounds.radius), boundingBox = new AABB(cluster.bounds.center - cluster.bounds.radius, cluster.bounds.center + cluster.bounds.radius),
vertexCount = (byte)cluster.vertexCount, vertexCount = (byte)cluster.vertexCount,
triangleCount = (byte)(cluster.localIndexCount / 3), triangleCount = (byte)(cluster.localIndexCount / 3),
vertexOffset = (uint)pMeshletData->meshletVertices.Count, vertexOffset = (uint)meshletData.meshletVertices.Count,
triangleOffset = (uint)pMeshletData->meshletTriangles.Count, triangleOffset = (uint)meshletData.meshletTriangles.Count,
groupIndex = (uint)pMeshletData->groups.Count - 1, groupIndex = (uint)meshletData.groups.Count - 1,
clusterError = cluster.bounds.error, clusterError = cluster.bounds.error,
parentError = group.simplified.error, parentError = group.simplified.error,
localMaterialIndex = (byte)materialIndex, localMaterialIndex = (byte)materialIndex,
lodLevel = (byte)group.depth, lodLevel = (byte)group.depth,
}; };
pMeshletData->meshlets.Add(meshlet); meshletData.meshlets.Add(meshlet);
// Add unique vertices // Add unique vertices
for (nuint j = 0; j < cluster.vertexCount; j++) for (nuint j = 0; j < cluster.vertexCount; j++)
{ {
pMeshletData->meshletVertices.Add(cluster.uniqueVertices[j]); meshletData.meshletVertices.Add(cluster.uniqueVertices[j]);
} }
// Add local triangles (packed into uints) // Add local triangles (packed into uints)
var triangleCount = cluster.localIndexCount / 3; var triangleCount = cluster.localIndexCount / 3;
@@ -754,11 +750,11 @@ public static unsafe partial class MeshProcessor
uint i1 = cluster.localIndices[j * 3 + 1]; uint i1 = cluster.localIndices[j * 3 + 1];
uint i2 = cluster.localIndices[j * 3 + 2]; uint i2 = cluster.localIndices[j * 3 + 2];
var packedTriangle = i0 | (i1 << 8) | (i2 << 16); var packedTriangle = i0 | (i1 << 8) | (i2 << 16);
pMeshletData->meshletTriangles.Add(packedTriangle); meshletData.meshletTriangles.Add(packedTriangle);
} }
} }
return pMeshletData->groups.Count - 1; return meshletData.groups.Count - 1;
} }
/// <summary> /// <summary>
@@ -766,9 +762,9 @@ public static unsafe partial class MeshProcessor
/// Each <see cref="MaterialPartInfo"/> describes a material partition's index range within the unified buffer. /// Each <see cref="MaterialPartInfo"/> describes a material partition's index range within the unified buffer.
/// Meshlets are built per-part and tagged with the corresponding <c>localMaterialIndex</c>. /// Meshlets are built per-part and tagged with the corresponding <c>localMaterialIndex</c>.
/// </summary> /// </summary>
public static void BuildMeshlets(MeshletMeshData* pMeshletData, ReadOnlyUnsafeCollection<Vertex> vertices, ReadOnlyUnsafeCollection<uint> indices, ReadOnlySpan<MaterialPartInfo> parts) public static void BuildMeshlets(ref MeshletMeshData meshletData, ReadOnlyUnsafeCollection<Vertex> vertices, ReadOnlyUnsafeCollection<uint> indices, ReadOnlySpan<MaterialPartInfo> parts)
{ {
Logger.DebugAssert(pMeshletData->meshletCount == 0, "Meshlet data is not empty."); Logger.DebugAssert(meshletData.meshletCount == 0, "Meshlet data is not empty.");
Logger.DebugAssert(vertices.Count > 0, "Mesh must have vertices to build meshlets."); Logger.DebugAssert(vertices.Count > 0, "Mesh must have vertices to build meshlets.");
Logger.DebugAssert(indices.Count > 0, "Mesh must have indices to build meshlets."); Logger.DebugAssert(indices.Count > 0, "Mesh must have indices to build meshlets.");
Logger.DebugAssert(parts.Length > 0, "Must have at least one material part."); Logger.DebugAssert(parts.Length > 0, "Must have at least one material part.");
@@ -817,24 +813,24 @@ public static unsafe partial class MeshProcessor
var context = new MeshletContext var context = new MeshletContext
{ {
data = pMeshletData, data = ref meshletData,
materialIndex = part.materialIndex materialIndex = part.materialIndex
}; };
Build(in config, in clodMesh, &context, MeshletOutputCallback); Build(in config, in clodMesh, ref context, MeshletOutputCallback);
} }
pMeshletData->meshletCount = pMeshletData->meshlets.IsCreated ? pMeshletData->meshlets.Count : 0; meshletData.meshletCount = meshletData.meshlets.IsCreated ? meshletData.meshlets.Count : 0;
if (pMeshletData->groups.IsCreated && pMeshletData->groups.Count > 0) if (meshletData.groups.IsCreated && meshletData.groups.Count > 0)
{ {
var maxLodLevel = 0u; var maxLodLevel = 0u;
for (var j = 0; j < pMeshletData->groups.Count; j++) for (var j = 0; j < meshletData.groups.Count; j++)
{ {
maxLodLevel = Math.Max(maxLodLevel, pMeshletData->groups[j].lodLevel); maxLodLevel = Math.Max(maxLodLevel, meshletData.groups[j].lodLevel);
} }
pMeshletData->lodLevelCount = (int)maxLodLevel + 1; meshletData.lodLevelCount = (int)maxLodLevel + 1;
} }
var maxMaterialSlot = 0; var maxMaterialSlot = 0;
@@ -843,7 +839,7 @@ public static unsafe partial class MeshProcessor
maxMaterialSlot = Math.Max(maxMaterialSlot, parts[j].materialIndex); maxMaterialSlot = Math.Max(maxMaterialSlot, parts[j].materialIndex);
} }
pMeshletData->materialSlotCount = maxMaterialSlot + 1; meshletData.materialSlotCount = maxMaterialSlot + 1;
} }
private struct TempBinaryNode private struct TempBinaryNode
@@ -1059,24 +1055,28 @@ public static unsafe partial class MeshProcessor
return outNodeIndex; return outNodeIndex;
} }
public static void BuildClusterLodHierarchy(MeshletMeshData* pMeshletData) /// <summary>
/// Builds a cluster LOD hierarchy from the input meshlet data.
/// </summary>
/// <param name="meshletData">The meshlet data.</param>
public static void BuildClusterLodHierarchy(ref MeshletMeshData meshletData)
{ {
if (pMeshletData->meshletCount == 0) return; if (meshletData.meshletCount == 0) return;
using var meshletIndices = new UnsafeArray<int>(pMeshletData->meshletCount, AllocationHandle.FreeList); using var meshletIndices = new UnsafeArray<int>(meshletData.meshletCount, AllocationHandle.FreeList);
for (var i = 0; i < pMeshletData->meshletCount; i++) for (var i = 0; i < meshletData.meshletCount; i++)
{ {
meshletIndices[i] = i; meshletIndices[i] = i;
} }
var meshletsSpan = new ReadOnlySpan<Meshlet>(pMeshletData->meshlets.GetUnsafePtr(), pMeshletData->meshlets.Count); var meshletsSpan = new ReadOnlySpan<Meshlet>(meshletData.meshlets.GetUnsafePtr(), meshletData.meshlets.Count);
using var binaryNodes = new UnsafeList<TempBinaryNode>(pMeshletData->meshletCount * 2, AllocationHandle.FreeList); using var binaryNodes = new UnsafeList<TempBinaryNode>(meshletData.meshletCount * 2, AllocationHandle.FreeList);
var rootIndex = BuildBinaryTree(binaryNodes, meshletIndices, 0, meshletIndices.Length, meshletsSpan); var rootIndex = BuildBinaryTree(binaryNodes, meshletIndices, 0, meshletIndices.Length, meshletsSpan);
if (!pMeshletData->hierarchyNodes.IsCreated) if (!meshletData.hierarchyNodes.IsCreated)
{ {
pMeshletData->hierarchyNodes = new UnsafeList<MeshletHierarchyNode>(pMeshletData->meshletCount, AllocationHandle.Persistent); meshletData.hierarchyNodes = new UnsafeList<MeshletHierarchyNode>(meshletData.meshletCount, AllocationHandle.Persistent);
} }
if (binaryNodes[rootIndex].leftChild == -1) if (binaryNodes[rootIndex].leftChild == -1)
@@ -1101,11 +1101,11 @@ public static unsafe partial class MeshProcessor
bvhNode.maxParentError.x = childNode.maxParentError; bvhNode.maxParentError.x = childNode.maxParentError;
bvhNode.nodeData.x = (uint)childNode.meshletIndex; bvhNode.nodeData.x = (uint)childNode.meshletIndex;
pMeshletData->hierarchyNodes.Add(bvhNode); meshletData.hierarchyNodes.Add(bvhNode);
} }
else else
{ {
CollapseTo4Ary(binaryNodes, rootIndex, pMeshletData->hierarchyNodes); CollapseTo4Ary(binaryNodes, rootIndex, meshletData.hierarchyNodes);
} }
} }
} }

View File

@@ -440,7 +440,7 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
}, token).ConfigureAwait(false); }, token).ConfigureAwait(false);
} }
public async ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default) public async ValueTask<Result<ImportedSubAsset[]>> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
{ {
if (!File.Exists(sourcePath)) if (!File.Exists(sourcePath))
{ {
@@ -464,7 +464,7 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
if (result.IsFailure) if (result.IsFailure)
{ {
return result; return Result.Failure(result.Message);
} }
var (cachePath, mip) = result.Value; var (cachePath, mip) = result.Value;
@@ -486,7 +486,7 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
await ddsStream.CopyToAsync(targetStream, token).ConfigureAwait(false); await ddsStream.CopyToAsync(targetStream, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false); await targetStream.FlushAsync(token).ConfigureAwait(false);
return Result.Success(); return Result.Success(Array.Empty<ImportedSubAsset>());
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -214,6 +214,12 @@ public sealed partial class AssetCatalog : IDisposable
public bool Remove(Guid guid) public bool Remove(Guid guid)
{ {
var subAssets = GetSubAssets(guid);
foreach (var sub in subAssets)
{
Remove(sub.Guid);
}
lock (_writeLock) lock (_writeLock)
{ {
_cmdDelete.Parameters.Clear(); _cmdDelete.Parameters.Clear();

View File

@@ -49,7 +49,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
{ {
IncludeSubdirectories = true, IncludeSubdirectories = true,
EnableRaisingEvents = true, EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.LastWrite NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName,
}; };
_watcher.Created += OnFileSystemEvent; _watcher.Created += OnFileSystemEvent;
@@ -165,6 +165,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
_catalog.Remove(guid); _catalog.Remove(guid);
changeType = AssetChangeType.Deleted; changeType = AssetChangeType.Deleted;
} }
Logger.DebugAssert(e.ChangeType == WatcherChangeTypes.Deleted);
} }
else if (guid == Guid.Empty) else if (guid == Guid.Empty)
{ {

View File

@@ -112,22 +112,15 @@ internal sealed partial class ImportCoordinator : IDisposable
} }
var importResult = Result.Success(); var importResult = Result.Success();
ImportedSubAsset[] subAssets = Array.Empty<ImportedSubAsset>(); var subAssets = Array.Empty<ImportedSubAsset>();
if (handler is IImportableAssetHandler importable) if (handler is IImportableAssetHandler importable)
{ {
var targetPath = GetImportedAssetPath(job.AssetGuid); var targetPath = GetImportedAssetPath(job.AssetGuid);
if (importable is ISubAssetImportableAssetHandler subAssetImportable) var subAssetResult = await importable.ImportAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token);
importResult = subAssetResult;
if (subAssetResult.IsSuccess)
{ {
var subAssetResult = await subAssetImportable.ImportWithSubAssetsAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token); subAssets = subAssetResult.Value;
importResult = subAssetResult;
if (subAssetResult.IsSuccess)
{
subAssets = subAssetResult.Value;
}
}
else
{
importResult = await importable.ImportAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token);
} }
} }
@@ -164,7 +157,7 @@ internal sealed partial class ImportCoordinator : IDisposable
_catalog.RemoveSubAssetsExcept(job.AssetGuid, dependencies); _catalog.RemoveSubAssetsExcept(job.AssetGuid, dependencies);
_catalog.SetDependencies(job.AssetGuid, dependencies); _catalog.SetDependencies(job.AssetGuid, dependencies);
} }
else if (handler is ISubAssetImportableAssetHandler) else
{ {
_catalog.RemoveSubAssetsExcept(job.AssetGuid, ReadOnlySpan<Guid>.Empty); _catalog.RemoveSubAssetsExcept(job.AssetGuid, ReadOnlySpan<Guid>.Empty);
_catalog.SetDependencies(job.AssetGuid, ReadOnlySpan<Guid>.Empty); _catalog.SetDependencies(job.AssetGuid, ReadOnlySpan<Guid>.Empty);

View File

@@ -153,6 +153,7 @@ public partial class App : Application
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error(ex);
Environment.Exit(ex.HResult); Environment.Exit(ex.HResult);
} }
} }
@@ -169,7 +170,7 @@ public partial class App : Application
} }
catch (Exception ex) catch (Exception ex)
{ {
Debugger.BreakForUserUnhandledException(ex); Logger.Error(ex);
} }
finally finally
{ {

View File

@@ -22,7 +22,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.9" /> <PackageReference Include="Misaki.HighPerformance" Version="1.0.9" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.1.6" /> <PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.1.6" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.18"> <PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.20">
<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

@@ -20,7 +20,7 @@ public unsafe class NativeMemoryManager<T> : MemoryManager<T>
} }
public static NativeMemoryManager<T> FromUnsafeCollection<C>(ref readonly C collection) public static NativeMemoryManager<T> FromUnsafeCollection<C>(ref readonly C collection)
where C : unmanaged, IUnsafeCollection<T> where C : IUnsafeCollection<T>
{ {
if (!collection.IsCreated) if (!collection.IsCreated)
{ {
@@ -30,6 +30,19 @@ public unsafe class NativeMemoryManager<T> : MemoryManager<T>
return new NativeMemoryManager<T>((T*)collection.GetUnsafePtr(), collection.Count); return new NativeMemoryManager<T>((T*)collection.GetUnsafePtr(), collection.Count);
} }
public static NativeMemoryManager<T> FromUnsafeCollectionInterpolated<C, U>(ref readonly C collection)
where U : unmanaged
where C : IUnsafeCollection<U>
{
if (!collection.IsCreated)
{
throw new InvalidOperationException("The collection is not created.");
}
var length = collection.Count * Unsafe.SizeOf<U>() / Unsafe.SizeOf<T>();
return new NativeMemoryManager<T>((T*)collection.GetUnsafePtr(), length);
}
public static NativeMemoryManager<T> FromMemoryBlock(MemoryBlock memoryBlock, int start, int length) public static NativeMemoryManager<T> FromMemoryBlock(MemoryBlock memoryBlock, int start, int length)
{ {
return new NativeMemoryManager<T>((T*)memoryBlock.GetUnsafePtr() + start, length); return new NativeMemoryManager<T>((T*)memoryBlock.GetUnsafePtr() + start, length);

View File

@@ -0,0 +1,55 @@
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Core.Utilities;
public static class StreamUtility
{
public static void Write<T>(this Stream stream, in T value)
where T : struct
{
stream.Write(MemoryMarshal.AsBytes(new ReadOnlySpan<T>(in value)));
}
public static void Write<T>(this Stream stream, ReadOnlySpan<T> values)
where T : struct
{
if (values.IsEmpty)
{
return;
}
stream.Write(MemoryMarshal.AsBytes(values));
}
public static async ValueTask WriteAsync<T, C>(this Stream stream, C collection, CancellationToken cancellationToken = default)
where T : unmanaged
where C : IUnsafeCollection<T>
{
if (!collection.IsCreated || collection.Count == 0)
{
return;
}
using var manager = NativeMemoryManager<byte>.FromUnsafeCollectionInterpolated<C, T>(in collection);
await stream.WriteAsync(manager.Memory, cancellationToken).ConfigureAwait(false);
}
public static async ValueTask WriteAsync<T>(this Stream stream, T value, CancellationToken cancellationToken = default)
where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
Unsafe.WriteUnaligned(ref buffer[0], value);
await stream.WriteAsync(buffer.AsMemory(0, size), cancellationToken);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}

View File

@@ -188,6 +188,9 @@ internal unsafe class ShaderLibrary : IDisposable
kvp.Value.Dispose(); kvp.Value.Dispose();
} }
_inMemoryCache.Dispose();
_variantToCompiledHash.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }

View File

@@ -14,6 +14,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" /> <ProjectReference Include="..\..\Test\Ghost.Test.Core\Ghost.Test.Core.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.StbI\Ghost.StbI.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.StbI\Ghost.StbI.csproj" />

View File

@@ -1,4 +1,5 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Engine; using Ghost.Engine;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
@@ -143,18 +144,18 @@ internal class MockingContentProvider : IContentProvider
boundsMax = new float3(1, 1, 0), boundsMax = new float3(1, 1, 0),
}; };
WriteStruct(stream, in header); stream.Write(header);
header.vertexOffset = (ulong)stream.Position; WriteSpan(stream, vertices); header.vertexOffset = (ulong)stream.Position; stream.Write(vertices);
header.indexOffset = (ulong)stream.Position; WriteSpan(stream, indices); header.indexOffset = (ulong)stream.Position; stream.Write(indices);
header.materialPartOffset = (ulong)stream.Position; WriteSpan(stream, materialParts); header.materialPartOffset = (ulong)stream.Position; stream.Write(materialParts);
header.meshletOffset = (ulong)stream.Position; WriteSpan(stream, meshlets); header.meshletOffset = (ulong)stream.Position; stream.Write(meshlets);
header.meshletGroupOffset = (ulong)stream.Position; WriteSpan(stream, groups); header.meshletGroupOffset = (ulong)stream.Position; stream.Write(groups);
header.meshletHierarchyNodeOffset = (ulong)stream.Position; WriteSpan(stream, hierarchy); header.meshletHierarchyNodeOffset = (ulong)stream.Position; stream.Write(hierarchy);
header.meshletVertexOffset = (ulong)stream.Position; WriteSpan(stream, meshletVertices); header.meshletVertexOffset = (ulong)stream.Position; stream.Write(meshletVertices);
header.meshletTriangleOffset = (ulong)stream.Position; WriteSpan(stream, meshletTriangles); header.meshletTriangleOffset = (ulong)stream.Position; stream.Write(meshletTriangles);
stream.Position = 0; stream.Position = 0;
WriteStruct(stream, in header); stream.Write(header);
AddMockAsset(guid, new MockAssetData AddMockAsset(guid, new MockAssetData
{ {
@@ -164,18 +165,6 @@ internal class MockingContentProvider : IContentProvider
}); });
} }
private static void WriteStruct<T>(Stream stream, ref readonly T value)
where T : unmanaged
{
stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in value, 1)));
}
private static void WriteSpan<T>(Stream stream, ReadOnlySpan<T> value)
where T : unmanaged
{
stream.Write(MemoryMarshal.AsBytes(value));
}
public AssetType GetAssetType(Guid guid) public AssetType GetAssetType(Guid guid)
{ {
return _assets.TryGetValue(guid, out var asset) ? asset.type : AssetType.Unknown; return _assets.TryGetValue(guid, out var asset) ? asset.type : AssetType.Unknown;