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
{
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);
}
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
{
ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default);

View File

@@ -1,6 +1,7 @@
using Ghost.Core;
using Ghost.Engine;
using Ghost.Core.Utilities;
using Ghost.Editor.Core.Services;
using Ghost.Engine;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
@@ -382,7 +383,7 @@ internal class FbxAssetSettings : MeshAssetSettings
}
[CustomAssetHandler(FBXAsset.GUID, [".fbx", ".obj"], 1)]
internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAssetHandler
internal class FBXAssetHandler : IImportableAssetHandler, IPackableAssetHandler
{
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."));
}
public async ValueTask<Result> 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)
public async ValueTask<Result<ImportedSubAsset[]>> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
{
if (!File.Exists(sourcePath))
{
@@ -557,72 +553,69 @@ internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAsset
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();
try
MeshProcessor.BuildMeshlets(ref meshletData, geometry.Vertices.AsReadOnly(), geometry.Indices.AsReadOnly(), geometry.MaterialParts.AsSpan());
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());
MeshProcessor.BuildClusterLodHierarchy(&meshletData);
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,
};
var bounds = ComputeBounds(geometry.Vertices);
var header = new MeshContentHeader
{
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);
stream.Write(header);
using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
WriteStruct(stream, in header);
header.vertexOffset = (ulong)stream.Position;
await stream.WriteAsync<Vertex, UnsafeList<Vertex>>(geometry.Vertices, token);
header.vertexOffset = (ulong)stream.Position;
WriteSpan(stream, geometry.Vertices.AsSpan());
header.indexOffset = (ulong)stream.Position;
await stream.WriteAsync<uint, UnsafeList<uint>>(geometry.Indices, token);
header.indexOffset = (ulong)stream.Position;
WriteSpan(stream, geometry.Indices.AsSpan());
header.materialPartOffset = (ulong)stream.Position;
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
header.materialPartOffset = (ulong)stream.Position;
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
header.meshletOffset = (ulong)stream.Position;
await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.meshlets, token);
header.meshletOffset = (ulong)stream.Position;
WriteSpan(stream, meshletData.meshlets.AsSpan());
header.meshletGroupOffset = (ulong)stream.Position;
await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.groups, token);
header.meshletGroupOffset = (ulong)stream.Position;
WriteSpan(stream, meshletData.groups.AsSpan());
header.meshletHierarchyNodeOffset = (ulong)stream.Position;
await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.hierarchyNodes, token);
header.meshletHierarchyNodeOffset = (ulong)stream.Position;
WriteSpan(stream, meshletData.hierarchyNodes.AsSpan());
header.meshletVertexOffset = (ulong)stream.Position;
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.meshletVertices, token);
header.meshletVertexOffset = (ulong)stream.Position;
WriteSpan(stream, meshletData.meshletVertices.AsSpan());
header.meshletTriangleOffset = (ulong)stream.Position;
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.meshletTriangles, token);
header.meshletTriangleOffset = (ulong)stream.Position;
WriteSpan(stream, meshletData.meshletTriangles.AsSpan());
stream.Position = 0;
stream.Write(header);
stream.Flush();
stream.Position = 0;
WriteStruct(stream, in header);
stream.Flush();
return ValueTask.FromResult((meshletData.materialSlotCount, meshletData.lodLevelCount));
}
finally
{
meshletData.Dispose();
}
return (meshletData.materialSlotCount, meshletData.lodLevelCount);
}
finally
{
meshletData.Dispose();
}
}
@@ -660,7 +653,7 @@ internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAsset
}
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] == '#')
{
@@ -678,7 +671,7 @@ internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAsset
return;
}
Span<MeshContentMaterialPart> buffer = parts.Length <= 64
var buffer = parts.Length <= 64
? stackalloc MeshContentMaterialPart[parts.Length]
: new MeshContentMaterialPart[parts.Length];
@@ -694,24 +687,6 @@ internal class FBXAssetHandler : ISubAssetImportableAssetHandler, IPackableAsset
};
}
WriteSpan(stream, 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));
stream.Write(buffer);
}
}

View File

@@ -10,6 +10,7 @@ using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
@@ -126,7 +127,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
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);

View File

@@ -158,13 +158,10 @@ public unsafe struct ClodCluster
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
{
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)
{
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;
}
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);
@@ -415,7 +412,7 @@ public static unsafe partial class MeshProcessor
var clodGroup = new ClodGroup { depth = depth, simplified = simplified };
var result = outputCallback != null
? outputCallback(outputContext, clodGroup, groupClusters.AsReadOnly())
? outputCallback(ref outputContext, clodGroup, groupClusters.AsReadOnly())
: -1;
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="outputCallback">Delegate invoked for each generated LOD group.</param>
/// <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)");
@@ -643,13 +640,13 @@ public static unsafe partial class MeshProcessor
if ((nuint)simplified.Length > (nuint)(merged.Count * config.simplifyThreshold))
{
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;
}
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++)
{
@@ -678,7 +675,7 @@ public static unsafe partial class MeshProcessor
{
var bounds = clusters[pending[0]].bounds;
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;
@@ -691,34 +688,33 @@ public static unsafe partial class MeshProcessor
return finalClusterCount;
}
private struct MeshletContext
private ref struct MeshletContext
{
public MeshletMeshData* data;
public ref MeshletMeshData data;
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;
var pMeshletData = context->data;
var materialIndex = context->materialIndex;
ref var meshletData = ref context.data;
var materialIndex = context.materialIndex;
// Ensure lists are initialized
if (!pMeshletData->groups.IsCreated) pMeshletData->groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent);
if (!pMeshletData->meshlets.IsCreated) pMeshletData->meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent);
if (!pMeshletData->meshletVertices.IsCreated) pMeshletData->meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent);
if (!pMeshletData->meshletTriangles.IsCreated) pMeshletData->meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.Persistent);
if (!meshletData.groups.IsCreated) meshletData.groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent);
if (!meshletData.meshlets.IsCreated) meshletData.meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent);
if (!meshletData.meshletVertices.IsCreated) meshletData.meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent);
if (!meshletData.meshletTriangles.IsCreated) meshletData.meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.Persistent);
var meshletGroup = new MeshletGroup
{
boundingSphere = new SphereBounds(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,
meshletStartIndex = (uint)pMeshletData->meshlets.Count,
meshletStartIndex = (uint)meshletData.meshlets.Count,
meshletCount = (uint)clusters.Count,
lodLevel = (uint)group.depth
};
pMeshletData->groups.Add(meshletGroup);
meshletData.groups.Add(meshletGroup);
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),
vertexCount = (byte)cluster.vertexCount,
triangleCount = (byte)(cluster.localIndexCount / 3),
vertexOffset = (uint)pMeshletData->meshletVertices.Count,
triangleOffset = (uint)pMeshletData->meshletTriangles.Count,
groupIndex = (uint)pMeshletData->groups.Count - 1,
vertexOffset = (uint)meshletData.meshletVertices.Count,
triangleOffset = (uint)meshletData.meshletTriangles.Count,
groupIndex = (uint)meshletData.groups.Count - 1,
clusterError = cluster.bounds.error,
parentError = group.simplified.error,
localMaterialIndex = (byte)materialIndex,
lodLevel = (byte)group.depth,
};
pMeshletData->meshlets.Add(meshlet);
meshletData.meshlets.Add(meshlet);
// Add unique vertices
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)
var triangleCount = cluster.localIndexCount / 3;
@@ -754,11 +750,11 @@ public static unsafe partial class MeshProcessor
uint i1 = cluster.localIndices[j * 3 + 1];
uint i2 = cluster.localIndices[j * 3 + 2];
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>
@@ -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.
/// Meshlets are built per-part and tagged with the corresponding <c>localMaterialIndex</c>.
/// </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(indices.Count > 0, "Mesh must have indices to build meshlets.");
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
{
data = pMeshletData,
data = ref meshletData,
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;
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;
@@ -843,7 +839,7 @@ public static unsafe partial class MeshProcessor
maxMaterialSlot = Math.Max(maxMaterialSlot, parts[j].materialIndex);
}
pMeshletData->materialSlotCount = maxMaterialSlot + 1;
meshletData.materialSlotCount = maxMaterialSlot + 1;
}
private struct TempBinaryNode
@@ -1059,24 +1055,28 @@ public static unsafe partial class MeshProcessor
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);
for (var i = 0; i < pMeshletData->meshletCount; i++)
using var meshletIndices = new UnsafeArray<int>(meshletData.meshletCount, AllocationHandle.FreeList);
for (var i = 0; i < meshletData.meshletCount; 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);
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)
@@ -1101,11 +1101,11 @@ public static unsafe partial class MeshProcessor
bvhNode.maxParentError.x = childNode.maxParentError;
bvhNode.nodeData.x = (uint)childNode.meshletIndex;
pMeshletData->hierarchyNodes.Add(bvhNode);
meshletData.hierarchyNodes.Add(bvhNode);
}
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);
}
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))
{
@@ -464,7 +464,7 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
if (result.IsFailure)
{
return result;
return Result.Failure(result.Message);
}
var (cachePath, mip) = result.Value;
@@ -486,7 +486,7 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
await ddsStream.CopyToAsync(targetStream, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false);
return Result.Success();
return Result.Success(Array.Empty<ImportedSubAsset>());
}
catch (Exception ex)
{

View File

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

View File

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

View File

@@ -112,22 +112,15 @@ internal sealed partial class ImportCoordinator : IDisposable
}
var importResult = Result.Success();
ImportedSubAsset[] subAssets = Array.Empty<ImportedSubAsset>();
var subAssets = Array.Empty<ImportedSubAsset>();
if (handler is IImportableAssetHandler importable)
{
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);
importResult = subAssetResult;
if (subAssetResult.IsSuccess)
{
subAssets = subAssetResult.Value;
}
}
else
{
importResult = await importable.ImportAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token);
subAssets = subAssetResult.Value;
}
}
@@ -164,7 +157,7 @@ internal sealed partial class ImportCoordinator : IDisposable
_catalog.RemoveSubAssetsExcept(job.AssetGuid, dependencies);
_catalog.SetDependencies(job.AssetGuid, dependencies);
}
else if (handler is ISubAssetImportableAssetHandler)
else
{
_catalog.RemoveSubAssetsExcept(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)
{
Logger.Error(ex);
Environment.Exit(ex.HResult);
}
}
@@ -169,7 +170,7 @@ public partial class App : Application
}
catch (Exception ex)
{
Debugger.BreakForUserUnhandledException(ex);
Logger.Error(ex);
}
finally
{

View File

@@ -22,7 +22,7 @@
<ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.9" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -20,7 +20,7 @@ public unsafe class NativeMemoryManager<T> : MemoryManager<T>
}
public static NativeMemoryManager<T> FromUnsafeCollection<C>(ref readonly C collection)
where C : unmanaged, IUnsafeCollection<T>
where C : IUnsafeCollection<T>
{
if (!collection.IsCreated)
{
@@ -30,6 +30,19 @@ public unsafe class NativeMemoryManager<T> : MemoryManager<T>
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)
{
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();
}
_inMemoryCache.Dispose();
_variantToCompiledHash.Dispose();
GC.SuppressFinalize(this);
}
}

View File

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

View File

@@ -1,4 +1,5 @@
using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Engine;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
@@ -143,18 +144,18 @@ internal class MockingContentProvider : IContentProvider
boundsMax = new float3(1, 1, 0),
};
WriteStruct(stream, in header);
header.vertexOffset = (ulong)stream.Position; WriteSpan(stream, vertices);
header.indexOffset = (ulong)stream.Position; WriteSpan(stream, indices);
header.materialPartOffset = (ulong)stream.Position; WriteSpan(stream, materialParts);
header.meshletOffset = (ulong)stream.Position; WriteSpan(stream, meshlets);
header.meshletGroupOffset = (ulong)stream.Position; WriteSpan(stream, groups);
header.meshletHierarchyNodeOffset = (ulong)stream.Position; WriteSpan(stream, hierarchy);
header.meshletVertexOffset = (ulong)stream.Position; WriteSpan(stream, meshletVertices);
header.meshletTriangleOffset = (ulong)stream.Position; WriteSpan(stream, meshletTriangles);
stream.Write(header);
header.vertexOffset = (ulong)stream.Position; stream.Write(vertices);
header.indexOffset = (ulong)stream.Position; stream.Write(indices);
header.materialPartOffset = (ulong)stream.Position; stream.Write(materialParts);
header.meshletOffset = (ulong)stream.Position; stream.Write(meshlets);
header.meshletGroupOffset = (ulong)stream.Position; stream.Write(groups);
header.meshletHierarchyNodeOffset = (ulong)stream.Position; stream.Write(hierarchy);
header.meshletVertexOffset = (ulong)stream.Position; stream.Write(meshletVertices);
header.meshletTriangleOffset = (ulong)stream.Position; stream.Write(meshletTriangles);
stream.Position = 0;
WriteStruct(stream, in header);
stream.Write(header);
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)
{
return _assets.TryGetValue(guid, out var asset) ? asset.type : AssetType.Unknown;