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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
55
src/Runtime/Ghost.Core/Utilities/StreamUtility.cs
Normal file
55
src/Runtime/Ghost.Core/Utilities/StreamUtility.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,6 +188,9 @@ internal unsafe class ShaderLibrary : IDisposable
|
||||
kvp.Value.Dispose();
|
||||
}
|
||||
|
||||
_inMemoryCache.Dispose();
|
||||
_variantToCompiledHash.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user