Refactor asset handler system and catalog for safety

- Introduced AssetHandlerInfo struct for handler registration and lookup, enabling handler caching and decoupling instantiation from extension/type.
- Changed CustomAssetHandlerAttribute to use required named properties; updated source generator.
- Replaced HandlerTypeId with AssetTypeId throughout metadata, catalog, and sub-asset records for clarity.
- Refactored asset catalog to use connection pooling and local command creation for thread safety.
- Updated asset handler interfaces and implementations to align with new registration system and removed redundant properties.
- Migrated mesh import and meshlet building to async JobScheduler jobs; switched to TLSF allocator and improved safety checks.
- Made meshlet/LOD hierarchy building async and job-based with better memory management.
- Updated usages and tests for new APIs; refreshed project references and package versions.
- Improved documentation and code comments for clarity.
This commit is contained in:
2026-05-08 11:50:06 +09:00
parent d052ca848f
commit b42398bbce
23 changed files with 690 additions and 568 deletions

View File

@@ -6,9 +6,30 @@ namespace Ghost.Editor.Core.Assets;
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public sealed class CustomAssetHandlerAttribute : Attribute public sealed class CustomAssetHandlerAttribute : Attribute
{ {
public CustomAssetHandlerAttribute(string assetTypeID, string[] supportedExtensions, int version = 1) public required string AssetTypeId
{ {
get; set;
} }
public required AssetType RuntimeAssetType
{
get; set;
}
public required string[] Extensions
{
get; set;
}
public int Version
{
get; set;
} = 1;
public bool AllowCaching
{
get; set;
} = true;
} }
public interface IAsset : IDisposable public interface IAsset : IDisposable
@@ -33,9 +54,6 @@ public interface IAssetExportOptions;
public interface IAssetHandler public interface IAssetHandler
{ {
AssetType RuntimeAssetType { get; }
Guid EditorAssetTypeID { get; }
IAssetSettings? CreateDefaultSettings(string ext); IAssetSettings? CreateDefaultSettings(string ext);
ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default); ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default);
@@ -44,12 +62,10 @@ public interface IAssetHandler
public interface IImportableAssetHandler : IAssetHandler public interface IImportableAssetHandler : IAssetHandler
{ {
bool CanExport { get; }
ValueTask<Result<ImportedSubAsset[]>> 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 readonly record struct ImportedSubAsset(Guid Guid, string Kind, string DisplayName, string StablePath, string VirtualSourcePath, Guid AssetTypeId);
public interface IPackableAssetHandler : IAssetHandler public interface IPackableAssetHandler : IAssetHandler
{ {

View File

@@ -1,36 +1,54 @@
using Ghost.Engine; using Ghost.Engine;
using System.Collections.Concurrent;
namespace Ghost.Editor.Core.Assets; namespace Ghost.Editor.Core.Assets;
public readonly struct AssetHandlerInfo
{
public Type HandlerType { get; init; }
public AssetType RuntimeAssetType { get; init; }
public Guid EditorAssetTypeID { get; init; }
public int Version { get; init; }
}
public static class AssetHandlerRegistry public static class AssetHandlerRegistry
{ {
private static readonly Dictionary<string, IAssetHandler> s_byExtension; private static readonly Dictionary<string, AssetHandlerInfo> s_byExtension;
private static readonly Dictionary<string, AssetType> s_typeByExtension; private static readonly Dictionary<Guid, AssetHandlerInfo> s_byTypeId;
private static readonly Dictionary<Guid, IAssetHandler> s_byTypeId;
private static readonly Dictionary<Guid, int> s_versionByTypeId;
private static readonly List<(Type Type, string Name)> s_iAssetSettingsTypes; private static readonly List<(Type Type, string Name)> s_iAssetSettingsTypes;
private static readonly ConcurrentDictionary<Type, IAssetHandler?> s_handlerCache;
static AssetHandlerRegistry() static AssetHandlerRegistry()
{ {
s_byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase); s_byExtension = new Dictionary<string, AssetHandlerInfo>(StringComparer.OrdinalIgnoreCase);
s_typeByExtension = new Dictionary<string, AssetType>(StringComparer.OrdinalIgnoreCase); s_byTypeId = new Dictionary<Guid, AssetHandlerInfo>();
s_byTypeId = new Dictionary<Guid, IAssetHandler>();
s_versionByTypeId = new Dictionary<Guid, int>();
s_iAssetSettingsTypes = new List<(Type Type, string Name)>(); s_iAssetSettingsTypes = new List<(Type Type, string Name)>();
s_handlerCache = new ConcurrentDictionary<Type, IAssetHandler?>();
} }
public static void RegisterHandler(IAssetHandler handler, Guid assetTypeId, ReadOnlySpan<string> extensions, int version) public static void RegisterHandler(Type handlerType, Guid assetTypeId, AssetType runtimeAssetType, int version, bool allowCaching, params ReadOnlySpan<string> extensions)
{ {
s_byTypeId[assetTypeId] = handler; var info = new AssetHandlerInfo
s_versionByTypeId[assetTypeId] = version; {
HandlerType = handlerType,
RuntimeAssetType = runtimeAssetType,
EditorAssetTypeID = assetTypeId,
Version = version
};
s_byTypeId[assetTypeId] = info;
foreach (var ext in extensions) foreach (var ext in extensions)
{ {
var normalizedExt = ext.StartsWith('.') ? ext : "." + ext; var normalizedExt = ext.StartsWith('.') ? ext : "." + ext;
s_byExtension[normalizedExt] = handler; s_byExtension[normalizedExt] = info;
s_typeByExtension[normalizedExt] = handler.RuntimeAssetType; }
if (allowCaching)
{
s_handlerCache[handlerType] = null;
} }
} }
@@ -47,36 +65,59 @@ public static class AssetHandlerRegistry
} }
var normalized = extension.StartsWith('.') ? extension : "." + extension; var normalized = extension.StartsWith('.') ? extension : "." + extension;
s_byExtension.TryGetValue(normalized, out var handler); if (!s_byExtension.TryGetValue(normalized, out var info))
return handler; {
return null;
}
return s_handlerCache.GetOrAdd(info.HandlerType, t =>
{
try
{
return (IAssetHandler?)Activator.CreateInstance(t);
}
catch
{
return null;
}
});
} }
public static IAssetHandler? GetByAssetTypeId(Guid typeId) public static IAssetHandler? GetByAssetTypeId(Guid typeId)
{ {
s_byTypeId.TryGetValue(typeId, out var handler); if (!s_byTypeId.TryGetValue(typeId, out var info))
return handler;
}
public static int GetVersionByAssetTypeId(Guid typeId)
{ {
s_versionByTypeId.TryGetValue(typeId, out var version); return null;
return version;
} }
public static IEnumerable<string> GetSupportedExtensions() return s_handlerCache.GetOrAdd(info.HandlerType, t =>
{ {
return s_byExtension.Keys; try
{
return (IAssetHandler?)Activator.CreateInstance(t);
}
catch
{
return null;
}
});
} }
public static AssetType GetRuntimeAssetTypeByExtension(string extension) public static bool TryGetHandlerInfoByAssetTypeId(Guid typeId, out AssetHandlerInfo info)
{
return s_byTypeId.TryGetValue(typeId, out info);
}
public static bool TryGetHandlerInfoByExtension(string extension, out AssetHandlerInfo info)
{ {
if (string.IsNullOrEmpty(extension)) if (string.IsNullOrEmpty(extension))
{ {
return AssetType.Unknown; info = default;
return false;
} }
var normalized = extension.StartsWith('.') ? extension : "." + extension; var normalized = extension.StartsWith('.') ? extension : "." + extension;
return s_typeByExtension.GetValueOrDefault(normalized, AssetType.Unknown); return s_byExtension.TryGetValue(normalized, out info);
} }
public static IReadOnlyCollection<(Type Type, string Name)> GetIAssetSettingsTypes() public static IReadOnlyCollection<(Type Type, string Name)> GetIAssetSettingsTypes()

View File

@@ -24,9 +24,9 @@ public sealed class AssetMeta
public required Guid Guid { get; init; } public required Guid Guid { get; init; }
/// <summary> /// <summary>
/// The Guid that identifies which IAssetHandler processes this asset. /// The Guid that identifies type id of asset.
/// </summary> /// </summary>
public Guid? HandlerTypeId { get; set; } public Guid? AssetTypeId { get; set; }
/// <summary> /// <summary>
/// Version of the handler that last imported this asset. /// Version of the handler that last imported this asset.

View File

@@ -4,6 +4,7 @@ using Ghost.Editor.Core.Services;
using Ghost.Engine; using Ghost.Engine;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
@@ -13,6 +14,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using TerraFX.Interop.Mimalloc;
namespace Ghost.Editor.Core.Assets; namespace Ghost.Editor.Core.Assets;
@@ -198,7 +200,7 @@ internal class FbxAssetSettings : MeshAssetSettings
{ {
} }
[CustomAssetHandler(MeshAsset.GUID, [".fbx", ".obj"], 1)] [CustomAssetHandler(AssetTypeId = MeshAsset.GUID, RuntimeAssetType = AssetType.Mesh, Extensions = new[] { ".fbx", ".obj" })]
internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
{ {
private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions
@@ -207,11 +209,7 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}; };
public AssetType RuntimeAssetType => AssetType.Mesh; private readonly JobScheduler _jobScheduler = EditorApplication.GetService<EngineCore>().JobScheduler;
public Guid EditorAssetTypeID => typeof(MeshAsset).GUID;
public bool CanExport => false;
public IAssetSettings? CreateDefaultSettings(string ext) public IAssetSettings? CreateDefaultSettings(string ext)
{ {
@@ -268,8 +266,8 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
using var root = new MeshNode(); using var root = new MeshNode();
var parseJob = new MeshParsingJob(root, sourcePath, AllocationHandle.Persistent, meshSettings); var parseJob = new MeshParsingJob(root, sourcePath, AllocationHandle.Persistent, meshSettings);
var context = default(Misaki.HighPerformance.Jobs.JobExecutionContext); var handle = _jobScheduler.Schedule(in parseJob);
parseJob.Execute(in context); await _jobScheduler.WaitAsync(handle, token);
var manifest = new ModelManifest var manifest = new ModelManifest
{ {
@@ -292,11 +290,6 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
} }
} }
public ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default)
{
return ValueTask.FromResult(Result.Failure("Exporting model assets is not supported yet."));
}
public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default) public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default)
{ {
return ValueTask.FromResult(Result.Failure("Packing model assets is not supported yet.")); return ValueTask.FromResult(Result.Failure("Packing model assets is not supported yet."));
@@ -314,7 +307,7 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
: new FbxAssetSettings(); : new FbxAssetSettings();
} }
private static async ValueTask<ModelManifestNode> WriteNodeAsync( private async ValueTask<ModelManifestNode> WriteNodeAsync(
Guid parentGuid, Guid parentGuid,
string sourcePath, string sourcePath,
MeshNode node, MeshNode node,
@@ -381,13 +374,10 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
return manifestNode; return manifestNode;
} }
private static async ValueTask<(int materialSlotCount, int lodLevelCount)> WriteMeshContentAsync(string targetPath, GeometryMeshNode geometry, CancellationToken token) private async ValueTask<(int materialSlotCount, int lodLevelCount)> WriteMeshContentAsync(string targetPath, GeometryMeshNode geometry, CancellationToken token)
{ {
var meshletData = new MeshletMeshData(); using var meshletData = await MeshProcessor.BuildMeshletsAsync(_jobScheduler, geometry.Vertices, geometry.Indices, geometry.MaterialParts, token).ConfigureAwait(false);
try await MeshProcessor.BuildClusterLodHierarchyAsync(_jobScheduler, meshletData.Share(), token).ConfigureAwait(false);
{
MeshProcessor.BuildMeshlets(ref meshletData, geometry.Vertices.AsReadOnly(), geometry.Indices.AsReadOnly(), geometry.MaterialParts.AsSpan());
MeshProcessor.BuildClusterLodHierarchy(ref meshletData, AllocationHandle.Persistent);
var bounds = ComputeBounds(geometry.Vertices); var bounds = ComputeBounds(geometry.Vertices);
var header = new MeshContentHeader var header = new MeshContentHeader
@@ -397,13 +387,13 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
vertexCount = (uint)geometry.Vertices.Count, vertexCount = (uint)geometry.Vertices.Count,
indexCount = (uint)geometry.Indices.Count, indexCount = (uint)geometry.Indices.Count,
materialPartCount = (uint)geometry.MaterialParts.Length, materialPartCount = (uint)geometry.MaterialParts.Length,
meshletCount = (uint)meshletData.meshlets.Count, meshletCount = (uint)meshletData.GetRef().meshlets.Count,
meshletGroupCount = (uint)meshletData.groups.Count, meshletGroupCount = (uint)meshletData.GetRef().groups.Count,
meshletHierarchyNodeCount = (uint)meshletData.hierarchyNodes.Count, meshletHierarchyNodeCount = (uint)meshletData.GetRef().hierarchyNodes.Count,
meshletVertexCount = (uint)meshletData.meshletVertices.Count, meshletVertexCount = (uint)meshletData.GetRef().meshletVertices.Count,
meshletTriangleCount = (uint)meshletData.meshletTriangles.Count, meshletTriangleCount = (uint)meshletData.GetRef().meshletTriangles.Count,
materialSlotCount = (uint)meshletData.materialSlotCount, materialSlotCount = (uint)meshletData.GetRef().materialSlotCount,
lodLevelCount = (uint)meshletData.lodLevelCount, lodLevelCount = (uint)meshletData.GetRef().lodLevelCount,
boundsMin = bounds.Min, boundsMin = bounds.Min,
boundsMax = bounds.Max, boundsMax = bounds.Max,
}; };
@@ -421,30 +411,25 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan()); WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
header.meshletOffset = (ulong)stream.Position; header.meshletOffset = (ulong)stream.Position;
await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.meshlets, token); await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.GetRef().meshlets, token);
header.meshletGroupOffset = (ulong)stream.Position; header.meshletGroupOffset = (ulong)stream.Position;
await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.groups, token); await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.GetRef().groups, token);
header.meshletHierarchyNodeOffset = (ulong)stream.Position; header.meshletHierarchyNodeOffset = (ulong)stream.Position;
await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.hierarchyNodes, token); await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.GetRef().hierarchyNodes, token);
header.meshletVertexOffset = (ulong)stream.Position; header.meshletVertexOffset = (ulong)stream.Position;
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.meshletVertices, token); await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletVertices, token);
header.meshletTriangleOffset = (ulong)stream.Position; header.meshletTriangleOffset = (ulong)stream.Position;
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.meshletTriangles, token); await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletTriangles, token);
stream.Position = 0; stream.Position = 0;
stream.Write(header); stream.Write(header);
stream.Flush(); stream.Flush();
return (meshletData.materialSlotCount, meshletData.lodLevelCount); return (meshletData.GetRef().materialSlotCount, meshletData.GetRef().lodLevelCount);
}
finally
{
meshletData.Dispose();
}
} }
private static AABB ComputeBounds(UnsafeList<Vertex> vertices) private static AABB ComputeBounds(UnsafeList<Vertex> vertices)

View File

@@ -13,7 +13,7 @@ using Misaki.HighPerformance.Mathematics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using StackPool = Misaki.HighPerformance.LowLevel.Buffer.MemoryPool<Misaki.HighPerformance.LowLevel.Buffer.VirtualStack, Misaki.HighPerformance.LowLevel.Buffer.VirtualStack.CreationOptions>; using TLSFPool = Misaki.HighPerformance.LowLevel.Buffer.MemoryPool<Misaki.HighPerformance.LowLevel.Buffer.TLSF, Misaki.HighPerformance.LowLevel.Buffer.TLSF.CreationOptions>;
namespace Ghost.Editor.Core.Assets; namespace Ghost.Editor.Core.Assets;
@@ -82,7 +82,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
); );
} }
private void ParseHierarchy(ufbx_node* node, MeshNode self, ref StackPool pool) private void ParseHierarchy(ufbx_node* node, MeshNode self, AllocationHandle allocationHandle)
{ {
var children = new List<MeshNode>(); var children = new List<MeshNode>();
@@ -92,7 +92,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
if (node->mesh != null) if (node->mesh != null)
{ {
var geoNode = ParseGeometry(node->mesh, ref pool); var geoNode = ParseGeometry(node->mesh, allocationHandle);
if (geoNode != null) if (geoNode != null)
{ {
children.Add(geoNode); children.Add(geoNode);
@@ -104,14 +104,14 @@ internal readonly unsafe struct MeshParsingJob : IJob
for (var i = 0u; i < node->children.count; i++) for (var i = 0u; i < node->children.count; i++)
{ {
var childNode = new MeshNode(); var childNode = new MeshNode();
ParseHierarchy(node->children.data[i], childNode, ref pool); ParseHierarchy(node->children.data[i], childNode, allocationHandle);
childNode.Parent = self; childNode.Parent = self;
children.Add(childNode); children.Add(childNode);
} }
} }
private GeometryMeshNode? ParseGeometry(ufbx_mesh* pMesh, ref StackPool pool) private GeometryMeshNode? ParseGeometry(ufbx_mesh* pMesh, AllocationHandle allocationHandle)
{ {
if (pMesh->num_faces == 0) if (pMesh->num_faces == 0)
{ {
@@ -122,20 +122,18 @@ internal readonly unsafe struct MeshParsingJob : IJob
// Bucket faces by material // Bucket faces by material
using var scope = pool.Allocator.CreateScope(pool.AllocationHandle); using var materialBuckets = new UnsafeArray<UnsafeList<Vertex>>(numMaterials, allocationHandle);
using var missingNormalsBucket = new UnsafeArray<bool>(numMaterials, allocationHandle);
using var materialBuckets = new UnsafeArray<UnsafeList<Vertex>>(numMaterials, scope.AllocationHandle); using var missingTangentsBucket = new UnsafeArray<bool>(numMaterials, allocationHandle);
using var missingNormalsBucket = new UnsafeArray<bool>(numMaterials, scope.AllocationHandle);
using var missingTangentsBucket = new UnsafeArray<bool>(numMaterials, scope.AllocationHandle);
for (var i = 0; i < numMaterials; i++) for (var i = 0; i < numMaterials; i++)
{ {
materialBuckets[i] = new UnsafeList<Vertex>(10240, scope.AllocationHandle); materialBuckets[i] = new UnsafeList<Vertex>(10240, allocationHandle);
} }
var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u); var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u);
using var triIndicesArray = new UnsafeArray<uint>(maxScratchIndices, scope.AllocationHandle); using var triIndicesArray = new UnsafeArray<uint>(maxScratchIndices, allocationHandle);
for (var j = 0u; j < pMesh->num_faces; j++) for (var j = 0u; j < pMesh->num_faces; j++)
{ {
@@ -196,7 +194,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
// Per-material weld + optimize, collect intermediate results // Per-material weld + optimize, collect intermediate results
using var partResults = new UnsafeList<GeometryPart>(numMaterials, scope.AllocationHandle); using var partResults = new UnsafeList<GeometryPart>(numMaterials, allocationHandle);
for (var m = 0; m < numMaterials; m++) for (var m = 0; m < numMaterials; m++)
{ {
@@ -209,8 +207,8 @@ internal readonly unsafe struct MeshParsingJob : IJob
var numIndices = (uint)flatVertices.Count; var numIndices = (uint)flatVertices.Count;
using var weldedIndices = new UnsafeArray<uint>((int)numIndices, scope.AllocationHandle); using var weldedIndices = new UnsafeArray<uint>((int)numIndices, allocationHandle);
using var cachedIndices = new UnsafeArray<uint>((int)numIndices, scope.AllocationHandle); using var cachedIndices = new UnsafeArray<uint>((int)numIndices, allocationHandle);
var stream = new ufbx_vertex_stream var stream = new ufbx_vertex_stream
{ {
@@ -230,8 +228,8 @@ internal readonly unsafe struct MeshParsingJob : IJob
MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices); MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices);
// Allocate temporary per-part buffers (will be merged then disposed) // Allocate temporary per-part buffers (will be merged then disposed)
var partVertices = new UnsafeList<Vertex>((int)numUniqueVertices, scope.AllocationHandle); var partVertices = new UnsafeList<Vertex>((int)numUniqueVertices, allocationHandle);
var partIndices = new UnsafeList<uint>((int)numIndices, scope.AllocationHandle); var partIndices = new UnsafeList<uint>((int)numIndices, allocationHandle);
var finalVertexCount = MeshOptApi.OptimizeVertexFetch(partVertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex)); var finalVertexCount = MeshOptApi.OptimizeVertexFetch(partVertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex));
@@ -349,16 +347,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
load_Opts.obj_search_mtl_by_filename = true; load_Opts.obj_search_mtl_by_filename = true;
} }
var stack = new StackPool(new VirtualStack.CreationOptions using var str = new UnsafeArray<byte>(Encoding.UTF8.GetByteCount(_filePath) + 1, AllocationHandle.TLSF);
{
reserveCapacity = 256 * 1024 * 1024 // 256 MB should be enough for most meshes, adjust as needed.
});
try
{
using var scope = stack.Allocator.CreateScope(stack.AllocationHandle);
using var str = new UnsafeArray<byte>(Encoding.UTF8.GetByteCount(_filePath) + 1, scope.AllocationHandle);
var count = Encoding.UTF8.GetBytes(_filePath, str.AsSpan()); var count = Encoding.UTF8.GetBytes(_filePath, str.AsSpan());
str[count] = 0; str[count] = 0;
@@ -369,12 +358,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
return; return;
} }
ParseHierarchy(scene.Get()->root_node, _rootNode, ref stack); ParseHierarchy(scene.Get()->root_node, _rootNode, AllocationHandle.TLSF);
}
finally
{
stack.Dispose();
}
} }
} }

View File

@@ -5,13 +5,15 @@ using Ghost.Core;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.MeshOptimizer; using Ghost.MeshOptimizer;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics; using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry; using Misaki.HighPerformance.Mathematics.Geometry;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using TLSFPool = Misaki.HighPerformance.LowLevel.Buffer.MemoryPool<Misaki.HighPerformance.LowLevel.Buffer.TLSF, Misaki.HighPerformance.LowLevel.Buffer.TLSF.CreationOptions>;
namespace Ghost.Editor.Core.Assets; namespace Ghost.Editor.Core.Assets;
@@ -162,7 +164,7 @@ public unsafe struct ClodCluster
internal static unsafe partial class MeshProcessor internal static unsafe partial class MeshProcessor
{ {
private delegate int ClodOutputDelegate(ref MeshletContext context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters); private delegate int ClodOutputDelegate(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)
{ {
@@ -392,7 +394,7 @@ internal static unsafe partial class MeshProcessor
private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh, private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh,
UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth, UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth,
ref MeshletContext outputContext, ClodOutputDelegate? outputCallback, MeshletContext outputContext, ClodOutputDelegate? outputCallback,
AllocationHandle allocationHandle) AllocationHandle allocationHandle)
{ {
using var groupClusters = new UnsafeList<ClodCluster>(group.Count, allocationHandle); using var groupClusters = new UnsafeList<ClodCluster>(group.Count, allocationHandle);
@@ -417,7 +419,7 @@ internal 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(ref outputContext, clodGroup, groupClusters.AsReadOnly()) ? outputCallback(outputContext, clodGroup, groupClusters.AsReadOnly())
: -1; : -1;
return result; return result;
@@ -579,18 +581,12 @@ internal 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>
private static nuint Build(ref readonly ClodConfig config, ref readonly ClodMesh mesh, ref MeshletContext outputContext, ClodOutputDelegate? outputCallback) private static nuint Build(ref readonly ClodConfig config, ref readonly ClodMesh mesh, 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)");
using var pool = new TLSFPool(new TLSF.CreationOptions using var locks = new UnsafeArray<byte>((int)mesh.vertexCount, AllocationHandle.TLSF, AllocationOption.Clear); ;
{ using var remap = new UnsafeArray<uint>((int)mesh.vertexCount, AllocationHandle.TLSF);
alignment = 8,
initialChunkSize = 64 * 1024,
});
using var locks = new UnsafeArray<byte>((int)mesh.vertexCount, pool.AllocationHandle, AllocationOption.Clear); ;
using var remap = new UnsafeArray<uint>((int)mesh.vertexCount, pool.AllocationHandle);
MeshOptApi.GeneratePositionRemap((uint*)remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride); MeshOptApi.GeneratePositionRemap((uint*)remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
@@ -613,14 +609,14 @@ internal static unsafe partial class MeshProcessor
} }
} }
using var clusters = Clusterize(in config, in mesh, mesh.indices, mesh.indexCount, pool.AllocationHandle); using var clusters = Clusterize(in config, in mesh, mesh.indices, mesh.indexCount, AllocationHandle.TLSF);
for (var i = 0; i < clusters.Count; i++) for (var i = 0; i < clusters.Count; i++)
{ {
clusters[i].bounds = ComputeBounds(in mesh, clusters[i].indices, 0.0f); clusters[i].bounds = ComputeBounds(in mesh, clusters[i].indices, 0.0f);
} }
using var pending = new UnsafeList<int>(clusters.Count, pool.AllocationHandle); using var pending = new UnsafeList<int>(clusters.Count, AllocationHandle.TLSF);
for (var i = 0; i < clusters.Count; i++) for (var i = 0; i < clusters.Count; i++)
{ {
pending.Add(i); pending.Add(i);
@@ -630,14 +626,14 @@ internal static unsafe partial class MeshProcessor
while (pending.Count > 1) while (pending.Count > 1)
{ {
using var groups = Partition(in config, in mesh, clusters, pending, remap, pool.AllocationHandle); using var groups = Partition(in config, in mesh, clusters, pending, remap, AllocationHandle.TLSF);
pending.Clear(); pending.Clear();
LockBoundary(locks, groups, clusters, remap, mesh.vertexLock); LockBoundary(locks, groups, clusters, remap, mesh.vertexLock);
for (var i = 0; i < groups.Count; i++) for (var i = 0; i < groups.Count; i++)
{ {
using var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, pool.AllocationHandle); using var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, AllocationHandle.TLSF);
for (var j = 0; j < groups[i].Count; j++) for (var j = 0; j < groups[i].Count; j++)
{ {
var clusterIndices = clusters[groups[i][j]].indices; var clusterIndices = clusters[groups[i][j]].indices;
@@ -645,28 +641,28 @@ internal static unsafe partial class MeshProcessor
} }
var targetSize = (nuint)(merged.Count / 3 * config.simplifyRatio * 3.0f); var targetSize = (nuint)(merged.Count / 3 * config.simplifyRatio * 3.0f);
var bounds = MergeBounds(clusters, groups[i], pool.AllocationHandle); var bounds = MergeBounds(clusters, groups[i], AllocationHandle.TLSF);
var error = 0.0f; var error = 0.0f;
using var simplified = Simplify(in config, in mesh, merged.AsReadOnly(), locks.AsReadOnly(), targetSize, &error, pool.AllocationHandle); using var simplified = Simplify(in config, in mesh, merged.AsReadOnly(), locks.AsReadOnly(), targetSize, &error, AllocationHandle.TLSF);
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, ref outputContext, outputCallback, pool.AllocationHandle); OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback, AllocationHandle.TLSF);
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, ref outputContext, outputCallback, pool.AllocationHandle); var refined = OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback, AllocationHandle.TLSF);
for (var j = 0; j < groups[i].Count; j++) for (var j = 0; j < groups[i].Count; j++)
{ {
clusters[groups[i][j]].Dispose(); clusters[groups[i][j]].Dispose();
} }
using var split = Clusterize(in config, in mesh, (uint*)simplified.GetUnsafePtr(), (nuint)simplified.Length, pool.AllocationHandle); using var split = Clusterize(in config, in mesh, (uint*)simplified.GetUnsafePtr(), (nuint)simplified.Length, AllocationHandle.TLSF);
for (var j = 0; j < split.Count; j++) for (var j = 0; j < split.Count; j++)
{ {
split[j].refined = refined; split[j].refined = refined;
@@ -688,7 +684,7 @@ internal 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, ref outputContext, outputCallback, pool.AllocationHandle); OutputGroup(in config, in mesh, clusters, pending, bounds, depth, outputContext, outputCallback, AllocationHandle.TLSF);
} }
var finalClusterCount = (nuint)clusters.Count; var finalClusterCount = (nuint)clusters.Count;
@@ -701,33 +697,33 @@ internal static unsafe partial class MeshProcessor
return finalClusterCount; return finalClusterCount;
} }
private ref struct MeshletContext private struct MeshletContext
{ {
public ref MeshletMeshData data; public MeshletMeshData* data;
public int materialIndex; public int materialIndex;
} }
private static int MeshletOutputCallback(ref MeshletContext context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters) private static int MeshletOutputCallback(MeshletContext context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters)
{ {
ref var meshletData = ref context.data; var meshletData = context.data;
var materialIndex = context.materialIndex; var materialIndex = context.materialIndex;
// Ensure lists are initialized // Ensure lists are initialized
if (!meshletData.groups.IsCreated) meshletData.groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent); if (!meshletData->groups.IsCreated) meshletData->groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.TLSF);
if (!meshletData.meshlets.IsCreated) meshletData.meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent); if (!meshletData->meshlets.IsCreated) meshletData->meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.TLSF);
if (!meshletData.meshletVertices.IsCreated) meshletData.meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent); if (!meshletData->meshletVertices.IsCreated) meshletData->meshletVertices = new UnsafeList<uint>(128, AllocationHandle.TLSF);
if (!meshletData.meshletTriangles.IsCreated) meshletData.meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.Persistent); if (!meshletData->meshletTriangles.IsCreated) meshletData->meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.TLSF);
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)meshletData.meshlets.Count, meshletStartIndex = (uint)meshletData->meshlets.Count,
meshletCount = (uint)clusters.Count, meshletCount = (uint)clusters.Count,
lodLevel = (uint)group.depth lodLevel = (uint)group.depth
}; };
meshletData.groups.Add(meshletGroup); meshletData->groups.Add(meshletGroup);
for (var i = 0; i < clusters.Count; i++) for (var i = 0; i < clusters.Count; i++)
{ {
@@ -740,20 +736,20 @@ internal 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)meshletData.meshletVertices.Count, vertexOffset = (uint)meshletData->meshletVertices.Count,
triangleOffset = (uint)meshletData.meshletTriangles.Count, triangleOffset = (uint)meshletData->meshletTriangles.Count,
groupIndex = (uint)meshletData.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,
}; };
meshletData.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++)
{ {
meshletData.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;
@@ -763,11 +759,29 @@ internal 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);
meshletData.meshletTriangles.Add(packedTriangle); meshletData->meshletTriangles.Add(packedTriangle);
} }
} }
return meshletData.groups.Count - 1; return meshletData->groups.Count - 1;
}
}
internal static partial class MeshProcessor
{
private struct MeshletBuildJob : IJob
{
public ClodConfig clodConfig;
public ClodMesh clodMesh;
public MeshletContext context;
public readonly void Execute(ref readonly JobExecutionContext ctx)
{
Build(in clodConfig, in clodMesh, context, MeshletOutputCallback);
}
} }
/// <summary> /// <summary>
@@ -775,9 +789,10 @@ internal 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(ref MeshletMeshData meshletData, ReadOnlyUnsafeCollection<Vertex> vertices, ReadOnlyUnsafeCollection<uint> indices, ReadOnlySpan<MaterialPartInfo> parts) public static async Task<DisposablePtr<MeshletMeshData>> BuildMeshletsAsync(JobScheduler jobScheduler,
ReadOnlyUnsafeCollection<Vertex> vertices, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<MaterialPartInfo> parts,
CancellationToken token)
{ {
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.");
@@ -806,10 +821,24 @@ internal static unsafe partial class MeshProcessor
simplifyFallbackSloppy = true, simplifyFallbackSloppy = true,
}; };
var jobs = new MeshletBuildJob[parts.Length];
IntPtr meshletData;
unsafe
{
// NOTE: We use NativeMemory here instead of MemoryUtility (use mimalloc internally) because this is a async method and may run a random thread pool thread which never dies.
// This will case mimalloc to allocate new heaps that hardly ever get freed, leading to memory bloat. Using NativeMemory ensures that we use the shared heap which doesn't have this issue.
meshletData = (IntPtr)NativeMemory.AllocZeroed(MemoryUtility.SizeOf<MeshletMeshData>());
}
try
{
for (var i = 0; i < parts.Length; i++) for (var i = 0; i < parts.Length; i++)
{ {
ref readonly var part = ref parts[i]; ref readonly var part = ref parts[i];
unsafe
{
// Each part references a slice of the global index buffer, // Each part references a slice of the global index buffer,
// but vertex positions are the full unified buffer so global indices remain valid. // but vertex positions are the full unified buffer so global indices remain valid.
var clodMesh = new ClodMesh var clodMesh = new ClodMesh
@@ -826,24 +855,41 @@ internal static unsafe partial class MeshProcessor
var context = new MeshletContext var context = new MeshletContext
{ {
data = ref meshletData, data = (MeshletMeshData*)meshletData,
materialIndex = part.materialIndex materialIndex = part.materialIndex
}; };
Build(in config, in clodMesh, ref context, MeshletOutputCallback); var job = new MeshletBuildJob
{
clodConfig = config,
clodMesh = clodMesh,
context = context
};
jobs[i] = job;
}
} }
meshletData.meshletCount = meshletData.meshlets.IsCreated ? meshletData.meshlets.Count : 0; foreach (var job in jobs)
{
var handle = jobScheduler.Schedule(in job);
await jobScheduler.WaitAsync(handle, token);
}
if (meshletData.groups.IsCreated && meshletData.groups.Count > 0) unsafe
{
var pMeshletData = (MeshletMeshData*)meshletData;
pMeshletData->meshletCount = pMeshletData->meshlets.IsCreated ? pMeshletData->meshlets.Count : 0;
if (pMeshletData->groups.IsCreated && pMeshletData->groups.Count > 0)
{ {
var maxLodLevel = 0u; var maxLodLevel = 0u;
for (var j = 0; j < meshletData.groups.Count; j++) for (var j = 0; j < pMeshletData->groups.Count; j++)
{ {
maxLodLevel = Math.Max(maxLodLevel, meshletData.groups[j].lodLevel); maxLodLevel = Math.Max(maxLodLevel, pMeshletData->groups[j].lodLevel);
} }
meshletData.lodLevelCount = (int)maxLodLevel + 1; pMeshletData->lodLevelCount = (int)maxLodLevel + 1;
} }
var maxMaterialSlot = 0; var maxMaterialSlot = 0;
@@ -852,7 +898,20 @@ internal static unsafe partial class MeshProcessor
maxMaterialSlot = Math.Max(maxMaterialSlot, parts[j].materialIndex); maxMaterialSlot = Math.Max(maxMaterialSlot, parts[j].materialIndex);
} }
meshletData.materialSlotCount = maxMaterialSlot + 1; pMeshletData->materialSlotCount = maxMaterialSlot + 1;
return new DisposablePtr<MeshletMeshData>(pMeshletData);
}
}
catch
{
unsafe
{
NativeMemory.Free((void*)meshletData);
}
throw;
}
} }
private struct TempBinaryNode private struct TempBinaryNode
@@ -992,7 +1051,8 @@ internal static unsafe partial class MeshProcessor
return -1; return -1;
} }
var gathered = new UnsafeList<int>(4, AllocationHandle.Persistent); var scope = AllocationManager.CreateStackScope();
var gathered = new UnsafeList<int>(4, scope.AllocationHandle);
try try
{ {
@@ -1073,32 +1133,32 @@ internal static unsafe partial class MeshProcessor
finally finally
{ {
gathered.Dispose(); gathered.Dispose();
scope.Dispose();
} }
} }
/// <summary> private unsafe struct BuildClusterLodHierarchyJob : IJob
/// 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, AllocationHandle allocationHandle)
{ {
if (meshletData.meshletCount == 0) return; public MeshletMeshData* meshletData;
using var meshletIndices = new UnsafeArray<int>(meshletData.meshletCount, AllocationHandle.Persistent); public readonly void Execute(ref readonly JobExecutionContext ctx)
for (var i = 0; i < meshletData.meshletCount; i++) {
using var scope = AllocationManager.CreateStackScope();
using var meshletIndices = new UnsafeArray<int>(meshletData->meshletCount, scope.AllocationHandle);
for (var i = 0; i < meshletData->meshletCount; i++)
{ {
meshletIndices[i] = i; meshletIndices[i] = i;
} }
var binaryNodes = new UnsafeList<TempBinaryNode>(meshletData.meshletCount * 2, AllocationHandle.Persistent); var binaryNodes = new UnsafeList<TempBinaryNode>(meshletData->meshletCount * 2, scope.AllocationHandle);
try try
{ {
var rootIndex = BuildBinaryTree(ref binaryNodes, meshletIndices, 0, meshletIndices.Length, meshletData.meshlets); var rootIndex = BuildBinaryTree(ref binaryNodes, meshletIndices, 0, meshletIndices.Length, meshletData->meshlets);
if (!meshletData.hierarchyNodes.IsCreated) if (!meshletData->hierarchyNodes.IsCreated)
{ {
meshletData.hierarchyNodes = new UnsafeList<MeshletHierarchyNode>(meshletData.meshletCount, allocationHandle); meshletData->hierarchyNodes = new UnsafeList<MeshletHierarchyNode>(meshletData->meshletCount, AllocationHandle.TLSF);
} }
if (binaryNodes[rootIndex].leftChild == -1) if (binaryNodes[rootIndex].leftChild == -1)
@@ -1123,11 +1183,11 @@ internal 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;
meshletData.hierarchyNodes.Add(bvhNode); meshletData->hierarchyNodes.Add(bvhNode);
} }
else else
{ {
CollapseTo4Ary(binaryNodes, rootIndex, meshletData.hierarchyNodes); CollapseTo4Ary(binaryNodes, rootIndex, meshletData->hierarchyNodes);
} }
} }
finally finally
@@ -1136,3 +1196,29 @@ internal static unsafe partial class MeshProcessor
} }
} }
} }
/// <summary>
/// Builds a cluster LOD hierarchy from the input meshlet data.
/// </summary>
/// <param name="meshletData">The meshlet data.</param>
public static async Task BuildClusterLodHierarchyAsync(JobScheduler jobScheduler, SharedPtr<MeshletMeshData> meshletData, CancellationToken token)
{
if (meshletData.GetRef().meshletCount == 0)
{
return;
}
JobHandle handle;
unsafe
{
var job = new BuildClusterLodHierarchyJob
{
meshletData = meshletData.Get()
};
handle = jobScheduler.Schedule(in job);
}
await jobScheduler.WaitAsync(handle, token);
}
}

View File

@@ -73,12 +73,9 @@ public sealed partial class ComputeShaderAsset : IAsset
} }
// Shader does not handle import/export via asset registry, it will handled by the hot reload system. // Shader does not handle import/export via asset registry, it will handled by the hot reload system.
[CustomAssetHandler(GraphicsShaderAsset.GUID, [".gshdr"], 1)] [CustomAssetHandler(AssetTypeId = GraphicsShaderAsset.GUID, RuntimeAssetType = AssetType.Shader, Extensions = new[] { ".gshdr" })]
internal class GraphicsShaderAssetHandler : IPackableAssetHandler internal class GraphicsShaderAssetHandler : IPackableAssetHandler
{ {
public AssetType RuntimeAssetType => AssetType.Shader;
public Guid EditorAssetTypeID => typeof(GraphicsShaderAsset).GUID;
public IAssetSettings? CreateDefaultSettings(string ext) public IAssetSettings? CreateDefaultSettings(string ext)
{ {
return null; return null;
@@ -113,12 +110,9 @@ internal class GraphicsShaderAssetHandler : IPackableAssetHandler
} }
} }
[CustomAssetHandler(ComputeShaderAsset.GUID, [".gcomp"], 1)] [CustomAssetHandler(AssetTypeId = ComputeShaderAsset.GUID, RuntimeAssetType = AssetType.Shader, Extensions = new[] { ".gcomp" })]
internal class ComputeShaderAssetHandler : IPackableAssetHandler internal class ComputeShaderAssetHandler : IPackableAssetHandler
{ {
public AssetType RuntimeAssetType => AssetType.Shader;
public Guid EditorAssetTypeID => typeof(ComputeShaderAsset).GUID;
public IAssetSettings? CreateDefaultSettings(string ext) public IAssetSettings? CreateDefaultSettings(string ext)
{ {
return null; return null;

View File

@@ -249,7 +249,7 @@ public class TextureAssetSettings : IAssetSettings
} = new SamplerSettings(); } = new SamplerSettings();
} }
[CustomAssetHandler(TextureAsset.GUID, [".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr"], 1)] [CustomAssetHandler(AssetTypeId = TextureAsset.GUID, RuntimeAssetType = AssetType.Texture, Extensions = new[] { ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr" })]
internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHandler internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHandler
{ {
internal struct TextureInfo internal struct TextureInfo
@@ -263,10 +263,6 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
public bool isHDR; public bool isHDR;
} }
public bool CanExport => false;
public AssetType RuntimeAssetType => AssetType.Texture;
public Guid EditorAssetTypeID => typeof(TextureAsset).GUID;
public IAssetSettings? CreateDefaultSettings(string ext) public IAssetSettings? CreateDefaultSettings(string ext)
{ {
return new TextureAssetSettings(); return new TextureAssetSettings();
@@ -494,11 +490,6 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
} }
} }
public ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default)
{
return ValueTask.FromResult(Result.Failure("Exporting texture assets is not supported yet."));
}
public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default) public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default)
{ {
return ValueTask.FromResult(Result.Failure("Packing texture assets is not supported yet.")); return ValueTask.FromResult(Result.Failure("Packing texture assets is not supported yet."));

View File

@@ -33,6 +33,7 @@
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.Ufbx\Ghost.Ufbx.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.Ufbx\Ghost.Ufbx.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.StbI\Ghost.StbI.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.StbI\Ghost.StbI.csproj" />
<ProjectReference Include="..\Ghost.DSL\Ghost.DSL.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -5,64 +5,24 @@ namespace Ghost.Editor.Core.Services;
/// <summary> /// <summary>
/// Thread-safe SQLite-backed asset catalog. /// Thread-safe SQLite-backed asset catalog.
/// Replaces the in-memory dictionary approach with persistent storage. /// Uses connection pooling and local command creation for safe multi-threaded access.
/// </summary> /// </summary>
public sealed partial class AssetCatalog : IDisposable public sealed partial class AssetCatalog
{ {
public readonly record struct SubAssetInfo(Guid Guid, Guid ParentGuid, string Kind, string DisplayName, string StablePath, string SourcePath, Guid HandlerTypeId); public readonly record struct SubAssetInfo(Guid Guid, Guid ParentGuid, string Kind, string DisplayName, string StablePath, string SourcePath, Guid AssetTypeId);
private readonly SqliteConnection _connection; private readonly string _connectionString;
private readonly Lock _writeLock = new();
// Prepared statements private const string SqlGetGuid = "SELECT guid FROM assets WHERE source_path = @path";
private readonly SqliteCommand _cmdGetGuid; private const string SqlGetPath = "SELECT source_path FROM assets WHERE guid = @guid";
private readonly SqliteCommand _cmdGetPath; private const string SqlGetAssetTypeId = "SELECT asset_type_id FROM assets WHERE guid = @guid";
private readonly SqliteCommand _cmdUpsert; private const string SqlGetImportedAt = "SELECT imported_at_ms FROM assets WHERE guid = @guid";
private readonly SqliteCommand _cmdDelete; private const string SqlUpsert = @"
INSERT INTO assets (guid, source_path, asset_type_id, handler_version, content_hash, settings_hash, imported_at_ms, parent_guid, subasset_kind, display_name, stable_path)
private readonly SqliteCommand _cmdGetHandlerTypeId; VALUES (@guid, @path, @asset_type_id, @version, @content_hash, @settings_hash, @imported_at_ms, @parent_guid, @subasset_kind, @display_name, @stable_path)
private readonly SqliteCommand _cmdGetReferencers;
private readonly SqliteCommand _cmdGetDependencies;
private readonly SqliteCommand _cmdGetImportedAt;
private readonly SqliteCommand _cmdInsertDep;
private readonly SqliteCommand _cmdClearDeps;
private readonly SqliteCommand _cmdEnumerate;
private readonly SqliteCommand _cmdEnumerateSubAssets;
private readonly SqliteCommand _cmdDeleteSubAssetsForParent;
public AssetCatalog(string dbPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
var connString = new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Cache = SqliteCacheMode.Shared,
}.ToString();
_connection = new SqliteConnection(connString);
_connection.Open();
using (var pragma = _connection.CreateCommand())
{
pragma.CommandText = "PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;";
pragma.ExecuteNonQuery();
}
CreateSchema();
_cmdGetGuid = CreateCommand("SELECT guid FROM assets WHERE source_path = @path");
_cmdGetPath = CreateCommand("SELECT source_path FROM assets WHERE guid = @guid");
_cmdGetHandlerTypeId = CreateCommand("SELECT handler_type_id FROM assets WHERE guid = @guid");
_cmdGetImportedAt = CreateCommand("SELECT imported_at_ms FROM assets WHERE guid = @guid");
_cmdUpsert = CreateCommand(@"
INSERT INTO assets (guid, source_path, handler_type_id, handler_version, content_hash, settings_hash, imported_at_ms, parent_guid, subasset_kind, display_name, stable_path)
VALUES (@guid, @path, @handler_id, @version, @content_hash, @settings_hash, @imported_at_ms, @parent_guid, @subasset_kind, @display_name, @stable_path)
ON CONFLICT(guid) DO UPDATE SET ON CONFLICT(guid) DO UPDATE SET
source_path = excluded.source_path, source_path = excluded.source_path,
handler_type_id = excluded.handler_type_id, asset_type_id = excluded.asset_type_id,
handler_version = excluded.handler_version, handler_version = excluded.handler_version,
content_hash = excluded.content_hash, content_hash = excluded.content_hash,
settings_hash = excluded.settings_hash, settings_hash = excluded.settings_hash,
@@ -70,33 +30,54 @@ public sealed partial class AssetCatalog : IDisposable
parent_guid = excluded.parent_guid, parent_guid = excluded.parent_guid,
subasset_kind = excluded.subasset_kind, subasset_kind = excluded.subasset_kind,
display_name = excluded.display_name, display_name = excluded.display_name,
stable_path = excluded.stable_path"); stable_path = excluded.stable_path";
_cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid"); private const string SqlDelete = "DELETE FROM assets WHERE guid = @guid";
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid"); private const string SqlGetReferencers = "SELECT from_guid FROM dependencies WHERE to_guid = @guid";
_cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid"); private const string SqlGetDependencies = "SELECT to_guid FROM dependencies WHERE from_guid = @guid";
private const string SqlInsertDep = "INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)";
private const string SqlClearDeps = "DELETE FROM dependencies WHERE from_guid = @guid";
private const string SqlEnumerate = "SELECT guid, source_path FROM assets";
private const string SqlEnumerateSubAssets = "SELECT guid, parent_guid, subasset_kind, display_name, stable_path, source_path, asset_type_id FROM assets WHERE parent_guid = @parent_guid ORDER BY stable_path";
private const string SqlDeleteSubAssetsForParent = "DELETE FROM assets WHERE parent_guid = @parent_guid";
_cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)"); public AssetCatalog(string dbPath)
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid"); {
_cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets"); Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
_cmdEnumerateSubAssets = CreateCommand("SELECT guid, parent_guid, subasset_kind, display_name, stable_path, source_path, handler_type_id FROM assets WHERE parent_guid = @parent_guid ORDER BY stable_path");
_cmdDeleteSubAssetsForParent = CreateCommand("DELETE FROM assets WHERE parent_guid = @parent_guid"); var builder = new SqliteConnectionStringBuilder
{
DataSource = dbPath,
ForeignKeys = true,
Pooling = true,
};
_connectionString = builder.ToString();
// Initial setup
using var connection = OpenConnection();
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "PRAGMA journal_mode = WAL;";
cmd.ExecuteNonQuery();
} }
private SqliteCommand CreateCommand(string sql) CreateSchemaInternal(connection);
{
var cmd = _connection.CreateCommand();
cmd.CommandText = sql;
return cmd;
} }
private void CreateSchema() private SqliteConnection OpenConnection()
{ {
using var cmd = _connection.CreateCommand(); var connection = new SqliteConnection(_connectionString);
connection.Open();
return connection;
}
private static void CreateSchemaInternal(SqliteConnection connection)
{
using var cmd = connection.CreateCommand();
cmd.CommandText = @" cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS assets ( CREATE TABLE IF NOT EXISTS assets (
guid BLOB (16) PRIMARY KEY NOT NULL, guid BLOB (16) PRIMARY KEY NOT NULL,
source_path TEXT NOT NULL, source_path TEXT NOT NULL,
handler_type_id BLOB(16), asset_type_id BLOB (16),
handler_version INTEGER NOT NULL DEFAULT 0, handler_version INTEGER NOT NULL DEFAULT 0,
content_hash TEXT, content_hash TEXT,
settings_hash TEXT, settings_hash TEXT,
@@ -123,28 +104,6 @@ public sealed partial class AssetCatalog : IDisposable
); );
CREATE INDEX IF NOT EXISTS idx_labels_label ON labels(label);"; CREATE INDEX IF NOT EXISTS idx_labels_label ON labels(label);";
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
TryAddColumn("assets", "parent_guid", "BLOB(16)");
TryAddColumn("assets", "subasset_kind", "TEXT");
TryAddColumn("assets", "display_name", "TEXT");
TryAddColumn("assets", "stable_path", "TEXT");
using var indexCmd = _connection.CreateCommand();
indexCmd.CommandText = "CREATE INDEX IF NOT EXISTS idx_assets_parent ON assets(parent_guid);";
indexCmd.ExecuteNonQuery();
}
private void TryAddColumn(string tableName, string columnName, string columnType)
{
try
{
using var cmd = _connection.CreateCommand();
cmd.CommandText = $"ALTER TABLE {tableName} ADD COLUMN {columnName} {columnType};";
cmd.ExecuteNonQuery();
}
catch (SqliteException)
{
}
} }
private static string ToUniversalPath(string path) private static string ToUniversalPath(string path)
@@ -159,58 +118,47 @@ public sealed partial class AssetCatalog : IDisposable
public Guid GetGuid(string sourcePath) public Guid GetGuid(string sourcePath)
{ {
_cmdGetGuid.Parameters.Clear(); using var connection = OpenConnection();
_cmdGetGuid.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath)); using var cmd = connection.CreateCommand();
var result = _cmdGetGuid.ExecuteScalar();
cmd.CommandText = SqlGetGuid;
cmd.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
var result = cmd.ExecuteScalar();
return result is byte[] bytes ? new Guid(bytes) : Guid.Empty; return result is byte[] bytes ? new Guid(bytes) : Guid.Empty;
} }
public string? GetSourcePath(Guid guid) public string? GetSourcePath(Guid guid)
{ {
_cmdGetPath.Parameters.Clear(); using var connection = OpenConnection();
_cmdGetPath.Parameters.AddWithValue("@guid", guid.ToByteArray()); using var cmd = connection.CreateCommand();
return _cmdGetPath.ExecuteScalar() as string; cmd.CommandText = SqlGetPath;
cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
return cmd.ExecuteScalar() as string;
} }
public void Upsert(AssetMeta meta, string sourcePath) private void UpsertInternal(AssetMeta meta, string sourcePath, Guid? parentGuid, string? kind, string? displayName, string? stablePath)
{ {
lock (_writeLock) using var connection = OpenConnection();
{ using var cmd = connection.CreateCommand();
_cmdUpsert.Parameters.Clear(); cmd.CommandText = SqlUpsert;
_cmdUpsert.Parameters.AddWithValue("@guid", meta.Guid.ToByteArray()); cmd.Parameters.AddWithValue("@guid", meta.Guid.ToByteArray());
_cmdUpsert.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath)); cmd.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
_cmdUpsert.Parameters.AddWithValue("@handler_id", meta.HandlerTypeId?.ToByteArray() ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@asset_type_id", meta.AssetTypeId?.ToByteArray() ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@version", meta.HandlerVersion); cmd.Parameters.AddWithValue("@version", meta.HandlerVersion);
_cmdUpsert.Parameters.AddWithValue("@content_hash", meta.ContentHash ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@content_hash", meta.ContentHash ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@settings_hash", meta.SettingsHash ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@settings_hash", meta.SettingsHash ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@imported_at_ms", meta.LastImportedUtc?.Ticks ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@imported_at_ms", meta.LastImportedUtc?.Ticks ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@parent_guid", DBNull.Value); cmd.Parameters.AddWithValue("@parent_guid", parentGuid?.ToByteArray() ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@subasset_kind", DBNull.Value); cmd.Parameters.AddWithValue("@subasset_kind", (object?)kind ?? DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@display_name", DBNull.Value); cmd.Parameters.AddWithValue("@display_name", (object?)displayName ?? DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@stable_path", DBNull.Value); cmd.Parameters.AddWithValue("@stable_path", (object?)stablePath ?? DBNull.Value);
_cmdUpsert.ExecuteNonQuery(); cmd.ExecuteNonQuery();
}
} }
public void Upsert(AssetMeta meta, string sourcePath) => UpsertInternal(meta, sourcePath, null, null, null, null);
public void UpsertSubAsset(Guid parentGuid, AssetMeta meta, string sourcePath, string kind, string displayName, string stablePath) public void UpsertSubAsset(Guid parentGuid, AssetMeta meta, string sourcePath, string kind, string displayName, string stablePath)
{ => UpsertInternal(meta, sourcePath, parentGuid, kind, displayName, stablePath);
lock (_writeLock)
{
_cmdUpsert.Parameters.Clear();
_cmdUpsert.Parameters.AddWithValue("@guid", meta.Guid.ToByteArray());
_cmdUpsert.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
_cmdUpsert.Parameters.AddWithValue("@handler_id", meta.HandlerTypeId?.ToByteArray() ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@version", meta.HandlerVersion);
_cmdUpsert.Parameters.AddWithValue("@content_hash", meta.ContentHash ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@settings_hash", meta.SettingsHash ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@imported_at_ms", meta.LastImportedUtc?.Ticks ?? (object)DBNull.Value);
_cmdUpsert.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
_cmdUpsert.Parameters.AddWithValue("@subasset_kind", kind);
_cmdUpsert.Parameters.AddWithValue("@display_name", displayName);
_cmdUpsert.Parameters.AddWithValue("@stable_path", stablePath);
_cmdUpsert.ExecuteNonQuery();
}
}
public bool Remove(Guid guid) public bool Remove(Guid guid)
{ {
@@ -220,65 +168,79 @@ public sealed partial class AssetCatalog : IDisposable
Remove(sub.Guid); Remove(sub.Guid);
} }
lock (_writeLock) using var connection = OpenConnection();
{ using var cmd = connection.CreateCommand();
_cmdDelete.Parameters.Clear();
_cmdDelete.Parameters.AddWithValue("@guid", guid.ToByteArray()); cmd.CommandText = SqlDelete;
return _cmdDelete.ExecuteNonQuery() > 0; cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
} return cmd.ExecuteNonQuery() > 0;
} }
public Guid GetHandlerTypeId(Guid guid) public Guid GetAssetTypeId(Guid guid)
{ {
_cmdGetHandlerTypeId.Parameters.Clear(); using var connection = OpenConnection();
_cmdGetHandlerTypeId.Parameters.AddWithValue("@guid", guid.ToByteArray()); using var cmd = connection.CreateCommand();
var result = _cmdGetHandlerTypeId.ExecuteScalar();
cmd.CommandText = SqlGetAssetTypeId;
cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
var result = cmd.ExecuteScalar();
return result is byte[] bytes ? new Guid(bytes) : Guid.Empty; return result is byte[] bytes ? new Guid(bytes) : Guid.Empty;
} }
public DateTime? GetImportedAt(Guid guid) public DateTime? GetImportedAt(Guid guid)
{ {
_cmdGetImportedAt.Parameters.Clear(); using var connection = OpenConnection();
_cmdGetImportedAt.Parameters.AddWithValue("@guid", guid.ToByteArray()); using var cmd = connection.CreateCommand();
var result = _cmdGetImportedAt.ExecuteScalar();
if (result is long ticks) cmd.CommandText = SqlGetImportedAt;
{ cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
return new DateTime(ticks, DateTimeKind.Utc);
}
return null; var result = cmd.ExecuteScalar();
return result is long ticks ? new DateTime(ticks, DateTimeKind.Utc) : null;
} }
public void SetDependencies(Guid assetId, ReadOnlySpan<Guid> dependencies) public void SetDependencies(Guid assetId, ReadOnlySpan<Guid> dependencies)
{ {
lock (_writeLock) using var connection = OpenConnection();
{ using var tx = connection.BeginTransaction();
using var tx = _connection.BeginTransaction();
_cmdClearDeps.Transaction = tx; using (var clearCmd = connection.CreateCommand())
_cmdClearDeps.Parameters.Clear(); {
_cmdClearDeps.Parameters.AddWithValue("@guid", assetId.ToByteArray()); clearCmd.Transaction = tx;
_cmdClearDeps.ExecuteNonQuery(); clearCmd.CommandText = SqlClearDeps;
clearCmd.Parameters.AddWithValue("@guid", assetId.ToByteArray());
clearCmd.ExecuteNonQuery();
}
if (dependencies.Length > 0)
{
using var insertCmd = connection.CreateCommand();
insertCmd.Transaction = tx;
insertCmd.CommandText = SqlInsertDep;
var fromParam = insertCmd.Parameters.Add("@from", SqliteType.Blob);
var toParam = insertCmd.Parameters.Add("@to", SqliteType.Blob);
fromParam.Value = assetId.ToByteArray();
_cmdInsertDep.Transaction = tx;
foreach (var dep in dependencies) foreach (var dep in dependencies)
{ {
_cmdInsertDep.Parameters.Clear(); toParam.Value = dep.ToByteArray();
_cmdInsertDep.Parameters.AddWithValue("@from", assetId.ToByteArray()); insertCmd.ExecuteNonQuery();
_cmdInsertDep.Parameters.AddWithValue("@to", dep.ToByteArray()); }
_cmdInsertDep.ExecuteNonQuery();
} }
tx.Commit(); tx.Commit();
} }
}
public List<Guid> GetReferencers(Guid guid) public List<Guid> GetReferencers(Guid guid)
{ {
_cmdGetReferencers.Parameters.Clear(); using var connection = OpenConnection();
_cmdGetReferencers.Parameters.AddWithValue("@guid", guid.ToByteArray()); using var cmd = connection.CreateCommand();
using var reader = _cmdGetReferencers.ExecuteReader(); cmd.CommandText = SqlGetReferencers;
cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
using var reader = cmd.ExecuteReader();
var list = new List<Guid>(); var list = new List<Guid>();
while (reader.Read()) while (reader.Read())
{ {
@@ -290,10 +252,13 @@ public sealed partial class AssetCatalog : IDisposable
public List<Guid> GetDependencies(Guid guid) public List<Guid> GetDependencies(Guid guid)
{ {
_cmdGetDependencies.Parameters.Clear(); using var connection = OpenConnection();
_cmdGetDependencies.Parameters.AddWithValue("@guid", guid.ToByteArray()); using var cmd = connection.CreateCommand();
using var reader = _cmdGetDependencies.ExecuteReader(); cmd.CommandText = SqlGetDependencies;
cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
using var reader = cmd.ExecuteReader();
var list = new List<Guid>(); var list = new List<Guid>();
while (reader.Read()) while (reader.Read())
{ {
@@ -305,7 +270,11 @@ public sealed partial class AssetCatalog : IDisposable
public IEnumerable<(Guid guid, string sourcePath)> EnumerateAll() public IEnumerable<(Guid guid, string sourcePath)> EnumerateAll()
{ {
using var reader = _cmdEnumerate.ExecuteReader(); using var connection = OpenConnection();
using var cmd = connection.CreateCommand();
cmd.CommandText = SqlEnumerate;
using var reader = cmd.ExecuteReader();
while (reader.Read()) while (reader.Read())
{ {
yield return (new Guid((byte[])reader[0]), reader.GetString(1)); yield return (new Guid((byte[])reader[0]), reader.GetString(1));
@@ -314,10 +283,13 @@ public sealed partial class AssetCatalog : IDisposable
public List<SubAssetInfo> GetSubAssets(Guid parentGuid) public List<SubAssetInfo> GetSubAssets(Guid parentGuid)
{ {
_cmdEnumerateSubAssets.Parameters.Clear(); using var connection = OpenConnection();
_cmdEnumerateSubAssets.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray()); using var cmd = connection.CreateCommand();
using var reader = _cmdEnumerateSubAssets.ExecuteReader(); cmd.CommandText = SqlEnumerateSubAssets;
cmd.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
using var reader = cmd.ExecuteReader();
var list = new List<SubAssetInfo>(); var list = new List<SubAssetInfo>();
while (reader.Read()) while (reader.Read())
{ {
@@ -335,49 +307,29 @@ public sealed partial class AssetCatalog : IDisposable
} }
public void RemoveSubAssetsExcept(Guid parentGuid, ReadOnlySpan<Guid> keepGuids) public void RemoveSubAssetsExcept(Guid parentGuid, ReadOnlySpan<Guid> keepGuids)
{
lock (_writeLock)
{ {
if (keepGuids.Length == 0) if (keepGuids.Length == 0)
{ {
_cmdDeleteSubAssetsForParent.Parameters.Clear(); using var connection = OpenConnection();
_cmdDeleteSubAssetsForParent.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray()); using var cmd = connection.CreateCommand();
_cmdDeleteSubAssetsForParent.ExecuteNonQuery(); cmd.CommandText = SqlDeleteSubAssetsForParent;
cmd.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
cmd.ExecuteNonQuery();
return; return;
} }
var keep = new HashSet<Guid>(); var keep = new HashSet<Guid>(keepGuids.Length);
for (var i = 0; i < keepGuids.Length; i++) foreach (var guid in keepGuids)
{ {
keep.Add(keepGuids[i]); keep.Add(guid);
} }
foreach (var subAsset in GetSubAssets(parentGuid)) foreach (var subAsset in GetSubAssets(parentGuid))
{ {
if (!keep.Contains(subAsset.Guid)) if (!keep.Contains(subAsset.Guid))
{ {
_cmdDelete.Parameters.Clear(); Remove(subAsset.Guid);
_cmdDelete.Parameters.AddWithValue("@guid", subAsset.Guid.ToByteArray());
_cmdDelete.ExecuteNonQuery();
} }
} }
} }
} }
public void Dispose()
{
_cmdGetGuid.Dispose();
_cmdGetPath.Dispose();
_cmdUpsert.Dispose();
_cmdDelete.Dispose();
_cmdGetHandlerTypeId.Dispose();
_cmdGetReferencers.Dispose();
_cmdGetDependencies.Dispose();
_cmdInsertDep.Dispose();
_cmdClearDeps.Dispose();
_cmdEnumerate.Dispose();
_cmdEnumerateSubAssets.Dispose();
_cmdDeleteSubAssetsForParent.Dispose();
_connection.Dispose();
}
}

View File

@@ -226,11 +226,16 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
return; return;
} }
var handlerTypeId = handler?.EditorAssetTypeID; var assetTypeId = Guid.Empty;
if (AssetHandlerRegistry.TryGetHandlerInfoByExtension(ext, out var handlerInfo))
{
assetTypeId = handlerInfo.EditorAssetTypeID;
}
var meta = new AssetMeta var meta = new AssetMeta
{ {
Guid = Guid.NewGuid(), Guid = Guid.NewGuid(),
HandlerTypeId = handlerTypeId, AssetTypeId = assetTypeId,
HandlerVersion = 1, HandlerVersion = 1,
Settings = handler?.CreateDefaultSettings(ext) Settings = handler?.CreateDefaultSettings(ext)
}; };
@@ -420,7 +425,6 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
{ {
_watcher.Dispose(); _watcher.Dispose();
_importCoordinator.Dispose(); _importCoordinator.Dispose();
_catalog.Dispose();
_loadLock.Dispose(); _loadLock.Dispose();
} }
} }

View File

@@ -37,8 +37,12 @@ internal class EditorContentProvider : IContentProvider
public AssetType GetAssetType(Guid guid) public AssetType GetAssetType(Guid guid)
{ {
var handlerID = _catalog.GetHandlerTypeId(guid); var assetTypeID = _catalog.GetAssetTypeId(guid);
var handler = AssetHandlerRegistry.GetByAssetTypeId(handlerID); if (AssetHandlerRegistry.TryGetHandlerInfoByAssetTypeId(assetTypeID, out var info))
return handler?.RuntimeAssetType ?? AssetType.Unknown; {
return info.RuntimeAssetType;
}
return AssetType.Unknown;
} }
} }

View File

@@ -95,18 +95,21 @@ internal sealed partial class ImportCoordinator : IDisposable
return; return;
} }
var handler = meta.HandlerTypeId.HasValue var handler = meta.AssetTypeId.HasValue
? AssetHandlerRegistry.GetByAssetTypeId(meta.HandlerTypeId.Value) ? AssetHandlerRegistry.GetByAssetTypeId(meta.AssetTypeId.Value)
: AssetHandlerRegistry.GetByExtension(Path.GetExtension(job.SourcePath)); : AssetHandlerRegistry.GetByExtension(Path.GetExtension(job.SourcePath));
var contentHash = await ComputeFileHashAsync(job.SourcePath, token); var contentHash = await ComputeFileHashAsync(job.SourcePath, token);
var settingsHash = ComputeSettingsHash(meta.Settings); var settingsHash = ComputeSettingsHash(meta.Settings);
var handlerVersion = AssetHandlerRegistry.TryGetHandlerInfoByAssetTypeId(meta.AssetTypeId ?? Guid.Empty, out var info)
? info.Version
: 0;
// Check if we can skip (if not a manual reimport) // Check if we can skip (if not a manual reimport)
if (job.Reason != ImportReason.ManualReimport && if (job.Reason != ImportReason.ManualReimport &&
meta.ContentHash == contentHash && meta.ContentHash == contentHash &&
meta.SettingsHash == settingsHash && meta.SettingsHash == settingsHash &&
meta.HandlerVersion == AssetHandlerRegistry.GetVersionByAssetTypeId(meta.HandlerTypeId ?? Guid.Empty)) meta.HandlerVersion == handlerVersion)
{ {
return; return;
} }
@@ -128,7 +131,7 @@ internal sealed partial class ImportCoordinator : IDisposable
{ {
meta.ContentHash = contentHash; meta.ContentHash = contentHash;
meta.SettingsHash = settingsHash; meta.SettingsHash = settingsHash;
meta.HandlerVersion = AssetHandlerRegistry.GetVersionByAssetTypeId(meta.HandlerTypeId ?? Guid.Empty); meta.HandlerVersion = handlerVersion;
meta.LastImportedUtc = DateTime.UtcNow; meta.LastImportedUtc = DateTime.UtcNow;
await AssetMetaIO.WriteAsync(job.MetaPath, meta, token); await AssetMetaIO.WriteAsync(job.MetaPath, meta, token);
@@ -144,7 +147,7 @@ internal sealed partial class ImportCoordinator : IDisposable
var subMeta = new AssetMeta var subMeta = new AssetMeta
{ {
Guid = subAsset.Guid, Guid = subAsset.Guid,
HandlerTypeId = subAsset.HandlerTypeId, AssetTypeId = subAsset.AssetTypeId,
HandlerVersion = meta.HandlerVersion, HandlerVersion = meta.HandlerVersion,
ContentHash = contentHash, ContentHash = contentHash,
SettingsHash = settingsHash, SettingsHash = settingsHash,

View File

@@ -58,9 +58,11 @@ internal static class ActivationHandler
var opts = new AllocationManagerDesc var opts = new AllocationManagerDesc
{ {
ArenaCapacity = 1024 * 1024 * 1024, // 1 GB. Arena using virtual memory, so this is just a reservation and won't actually consume physical memory until used. ArenaCapacity = 1024 * 1024 * 1024, // 1 GB. Arena using virtual memory, so this is just a reservation and won't actually consume physical memory until used.
StackCapacity = 1024 * 1024 * 32, // 32 MB. Stack using virtual memory, so this is just a reservation and won't actually consume physical memory until used. StackCapacity = 1024 * 1024 * 64, // 64 MB. Stack using virtual memory, so this is just a reservation and won't actually consume physical memory until used.
FreeListChunkSize = 64 * 1024 * 1024, FreeListChunkSize = 64 * 1024,
FreeListDefaultAlignment = 8, FreeListDefaultAlignment = 8,
TLSFInitialChunkSize = 64 * 1024,
TLSFAlignment = 8,
}; };
AllocationManager.Initialize(opts); AllocationManager.Initialize(opts);

View File

@@ -90,8 +90,12 @@ internal partial class ContentBrowserViewModel : ObservableObject
if (!isDir) if (!isDir)
{ {
var ext = Path.GetExtension(fullPath); var ext = Path.GetExtension(fullPath);
assetType = AssetHandlerRegistry.GetRuntimeAssetTypeByExtension(ext); if (AssetHandlerRegistry.TryGetHandlerInfoByExtension(ext, out var info))
{
assetType = info.RuntimeAssetType;
} }
}
Files.Add(new ExplorerItem(Path.GetFileName(fullPath), fullPath, isDir, assetType)); Files.Add(new ExplorerItem(Path.GetFileName(fullPath), fullPath, isDir, assetType));
} }
} }
@@ -144,7 +148,7 @@ internal partial class ContentBrowserViewModel : ObservableObject
} }
var ext = Path.GetExtension(file); var ext = Path.GetExtension(file);
var assetType = AssetHandlerRegistry.GetRuntimeAssetTypeByExtension(ext); var assetType = AssetHandlerRegistry.TryGetHandlerInfoByExtension(ext, out var handlerInfo) ? handlerInfo.RuntimeAssetType : AssetType.Unknown;
var fileItem = new ExplorerItem(Path.GetFileName(file), file, false, assetType); var fileItem = new ExplorerItem(Path.GetFileName(file), file, false, assetType);
Files.Add(fileItem); Files.Add(fileItem);

View File

@@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>$(DefineConstants);MHP_ENABLE_MIMALLOC;MHP_FASTMATH</DefineConstants> <DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS;MHP_ENABLE_MIMALLOC;MHP_FASTMATH</DefineConstants>
<IsAotCompatible>True</IsAotCompatible> <IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable> <IsTrimmable>True</IsTrimmable>
</PropertyGroup> </PropertyGroup>
@@ -22,12 +22,12 @@
<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.20"> <PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.24">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" /> <PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" />
<PackageReference Include="Misaki.HighPerformance.Mathematics.SPMD" Version="1.3.7" /> <PackageReference Include="Misaki.HighPerformance.Mathematics.SPMD" Version="1.3.8" />
<PackageReference Include="System.IO.Hashing" Version="10.0.7" /> <PackageReference Include="System.IO.Hashing" Version="10.0.7" />
<PackageReference Include="TerraFX.Interop.Mimalloc" Version="1.6.7.2" /> <PackageReference Include="TerraFX.Interop.Mimalloc" Version="1.6.7.2" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" /> <PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />

View File

@@ -1,5 +1,6 @@
namespace Ghost.DSL; namespace Ghost.Core.Graphics;
#if DEBUG || GHOST_EDITOR
public struct ShaderPropertyInfo public struct ShaderPropertyInfo
{ {
public string shaderName; public string shaderName;
@@ -21,3 +22,4 @@ public static class ShaderPropertiesRegistry
return s_nameToCode.TryGetValue(name, out info); return s_nameToCode.TryGetValue(name, out info);
} }
} }
#endif

View File

@@ -1,9 +1,20 @@
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using System.Runtime.CompilerServices; using System.Runtime.InteropServices;
namespace Ghost.Core; namespace Ghost.Core;
public static class TempJobAllocatorHandle
{
extension(AllocationHandle)
{
/// <summary>
/// Gets the allocation handle for the TempJobAllocator, which is designed for temporary allocations within jobs. This allocator provides a simple interface for allocating and freeing memory that is automatically reset after a certain number of frames, making it ideal for use in job systems where temporary data is needed.
/// </summary>
public static AllocationHandle TempJob => TempJobAllocator.AllocationHandle;
}
}
public unsafe partial struct TempJobAllocator public unsafe partial struct TempJobAllocator
{ {
private static TempJobAllocator* _pAllocator; private static TempJobAllocator* _pAllocator;
@@ -13,7 +24,8 @@ public unsafe partial struct TempJobAllocator
internal static void Initialize(nuint capacity) internal static void Initialize(nuint capacity)
{ {
Logger.DebugAssert(_pAllocator == null, "TempJobAllocator is already initialized."); Logger.DebugAssert(_pAllocator == null, "TempJobAllocator is already initialized.");
_pAllocator = (TempJobAllocator*)Malloc((nuint)sizeof(TempJobAllocator)); _pAllocator = (TempJobAllocator*)NativeMemory.Alloc((nuint)sizeof(TempJobAllocator));
*_pAllocator = new TempJobAllocator(_pAllocator, capacity);
} }
internal static void Dispose() internal static void Dispose()
@@ -28,8 +40,8 @@ public unsafe partial struct TempJobAllocator
_pAllocator->_pArena[i].Dispose(); _pAllocator->_pArena[i].Dispose();
} }
MemoryUtility.Free(_pAllocator->_pArena); NativeMemory.Free(_pAllocator->_pArena);
MemoryUtility.Free(_pAllocator); NativeMemory.Free(_pAllocator);
_pAllocator = null; _pAllocator = null;
} }
@@ -38,40 +50,33 @@ public unsafe partial struct TempJobAllocator
public unsafe partial struct TempJobAllocator : IAllocator public unsafe partial struct TempJobAllocator : IAllocator
{ {
private const int _FRAME_LATENCY = 4; private const int _FRAME_LATENCY = 4;
private const int _MAGIC_ID = -559038737;
private VirtualArena* _pArena; private VirtualArena* _pArena;
private int _currentFrameCount; private int _currentFrameCount;
private int _currentFrameIndex; private int _currentFrameIndex;
#if MHP_ENABLE_SAFETY_CHECKS
private fixed int _allocationsPerFrame[_FRAME_LATENCY]; private fixed int _allocationsPerFrame[_FRAME_LATENCY];
#endif
private MemoryHandle _memoryHandle; private readonly AllocationHandle _handle;
private AllocationHandle _handle;
public readonly AllocationHandle Handle => _handle; public readonly AllocationHandle Handle => _handle;
internal TempJobAllocator(void* pSelf, nuint capacity) internal TempJobAllocator(void* pSelf, nuint capacity)
{ {
var memoryHandle = default(MemoryHandle); _pArena = (VirtualArena*)NativeMemory.Alloc((nuint)(sizeof(VirtualArena) * _FRAME_LATENCY));
_pArena = (VirtualArena*)Malloc((nuint)(sizeof(VirtualArena) * _FRAME_LATENCY));
_currentFrameCount = 0; _currentFrameCount = 0;
_currentFrameIndex = 0; _currentFrameIndex = 0;
_memoryHandle = memoryHandle;
for (var i = 0; i < _FRAME_LATENCY; i++) for (var i = 0; i < _FRAME_LATENCY; i++)
{ {
_pArena[i] = new VirtualArena(capacity); _pArena[i] = new VirtualArena(capacity);
#if MHP_ENABLE_SAFETY_CHECKS
_allocationsPerFrame[i] = 0; _allocationsPerFrame[i] = 0;
#endif
} }
_handle = new AllocationHandle _handle = new AllocationHandle(pSelf, &Allocate, &Reallocate, &Free);
{
State = Unsafe.AsPointer(ref this),
Alloc = &Allocate,
Realloc = &Reallocate,
Free = &Free
};
} }
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption) private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
@@ -84,7 +89,9 @@ public unsafe partial struct TempJobAllocator : IAllocator
return null; return null;
} }
#if MHP_ENABLE_SAFETY_CHECKS
Interlocked.Increment(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]); Interlocked.Increment(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]);
#endif
return ptr; return ptr;
} }
@@ -103,7 +110,7 @@ public unsafe partial struct TempJobAllocator : IAllocator
return null; return null;
} }
MemCpy(ptr, newPtr, Math.Min(oldSize, newSize)); MemoryUtility.MemCpy(ptr, newPtr, Math.Min(oldSize, newSize));
return newPtr; return newPtr;
} }
@@ -111,7 +118,9 @@ public unsafe partial struct TempJobAllocator : IAllocator
private static void Free(void* instance, void* ptr) private static void Free(void* instance, void* ptr)
{ {
var pSelf = (TempJobAllocator*)instance; var pSelf = (TempJobAllocator*)instance;
#if MHP_ENABLE_SAFETY_CHECKS
Interlocked.Decrement(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]); Interlocked.Decrement(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]);
#endif
} }
public int AdvanceFrame() public int AdvanceFrame()
@@ -123,6 +132,13 @@ public unsafe partial struct TempJobAllocator : IAllocator
(_pArena + _currentFrameIndex)->Reset(); (_pArena + _currentFrameIndex)->Reset();
#if MHP_ENABLE_SAFETY_CHECKS
if (_allocationsPerFrame[_currentFrameIndex] != 0)
{
Logger.Error($"TempJobAllocator: Detected {_allocationsPerFrame[_currentFrameIndex]} leaked allocations from frame {_currentFrameCount - _FRAME_LATENCY}.");
}
#endif
return allocations; return allocations;
} }
} }

View File

@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -22,6 +23,29 @@ internal class AssetHandlerRegistrationGenerator : IIncrementalGenerator
context.RegisterSourceOutput(handerCandidates, GenerateRegistrationCode); context.RegisterSourceOutput(handerCandidates, GenerateRegistrationCode);
} }
private static T GetValueOrDefault<T>(IDictionary<string, TypedConstant> dictionary, string key, T defaultValue = default)
{
if (dictionary.TryGetValue(key, out var value))
{
if (value.Value is T typedValue)
{
return typedValue;
}
}
return defaultValue;
}
private static ImmutableArray<TypedConstant> GetValuesOrDefault(IDictionary<string, TypedConstant> dictionary, string key)
{
if (dictionary.TryGetValue(key, out var value))
{
return value.Values;
}
return default;
}
private void GenerateRegistrationCode(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> array) private void GenerateRegistrationCode(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> array)
{ {
if (array.IsDefaultOrEmpty) if (array.IsDefaultOrEmpty)
@@ -39,12 +63,24 @@ internal class AssetHandlerRegistrationGenerator : IIncrementalGenerator
continue; continue;
} }
var id = attribute.ConstructorArguments[0].Value as string; var properties = attribute.NamedArguments.ToDictionary(kv => kv.Key, kv => kv.Value);
var extensionsTypesConstants = attribute.ConstructorArguments[1].Values;
var extensions = $"new string[] {{ {string.Join(", ", extensionsTypesConstants.Select(v => v.ToCSharpString()))} }}";
var version = (int)attribute.ConstructorArguments[2].Value;
sb.AppendLine($" global::Ghost.Editor.Core.Assets.AssetHandlerRegistry.RegisterHandler(new {symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}(), Guid.Parse(\"{id}\"), {extensions}, {version});"); var id = GetValueOrDefault(properties, "AssetTypeId", string.Empty);
var runtimeType = GetValueOrDefault(properties, "RuntimeAssetType", 0);
var version = GetValueOrDefault(properties, "Version", 1);
var allowCaching = GetValueOrDefault(properties, "AllowCaching", false) ? "true" : "false";
var extensionsTypesConstants = GetValuesOrDefault(properties, "Extensions");
var extensions = string.Join(", ", extensionsTypesConstants.Select(v => v.ToCSharpString()));
sb.AppendLine(" global::Ghost.Editor.Core.Assets.AssetHandlerRegistry.RegisterHandler(");
sb.AppendLine($" typeof({symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}),");
sb.AppendLine($" System.Guid.Parse(\"{id}\"),");
sb.AppendLine($" (Ghost.Engine.AssetType){runtimeType},");
sb.AppendLine($" {version},");
sb.AppendLine($" {allowCaching},");
sb.AppendLine($" new string[] {{ {extensions} }});");
sb.AppendLine();
} }
var registerTypeName = "g_assethandler_registeration"; var registerTypeName = "g_assethandler_registeration";

View File

@@ -176,6 +176,7 @@ namespace {info.TypeSymbol.ContainingNamespace.ToDisplayString()}
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)] [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)]
{info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name} {info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name}
{{ {{
#if DEBUG || GHOST_EDITOR
public const string HLSL_SOURCE = @"" public const string HLSL_SOURCE = @""
# ifndef {definedSymbol} # ifndef {definedSymbol}
# define {definedSymbol} # define {definedSymbol}
@@ -185,13 +186,14 @@ struct {info.Name}
}}; }};
# endif // {definedSymbol}""; # endif // {definedSymbol}"";
}} }}
#endif
}}"; }}";
context.AddSource($"{info.TypeSymbol.Name}_HLSL.gen.cs", code); context.AddSource($"{info.TypeSymbol.Name}_HLSL.gen.cs", code);
codeBuilder.Clear(); codeBuilder.Clear();
var typeFullName = info.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var typeFullName = info.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
registerBuilder.AppendLine($@" global::Ghost.DSL.ShaderPropertiesRegistry.Register(""{info.ShaderName}"", {typeFullName}.HLSL_SOURCE, (uint)sizeof({typeFullName}));"); registerBuilder.AppendLine($@" global::Ghost.Core.Graphics.ShaderPropertiesRegistry.Register(""{info.ShaderName}"", {typeFullName}.HLSL_SOURCE, (uint)sizeof({typeFullName}));");
} }
var registerTypeName = "g_shaderproperty_registeration"; var registerTypeName = "g_shaderproperty_registeration";

View File

@@ -26,7 +26,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" /> <ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\..\Editor\Ghost.DSL\Ghost.DSL.csproj" />
<ProjectReference Include="..\Ghost.Graphics.D3D12\Ghost.Graphics.D3D12.csproj" /> <ProjectReference Include="..\Ghost.Graphics.D3D12\Ghost.Graphics.D3D12.csproj" />
<ProjectReference Include="..\Ghost.Graphics.RHI\Ghost.Graphics.RHI.csproj" /> <ProjectReference Include="..\Ghost.Graphics.RHI\Ghost.Graphics.RHI.csproj" />
<ProjectReference Include="..\..\ThridParty\Ghost.MeshOptimizer\Ghost.MeshOptimizer.csproj" /> <ProjectReference Include="..\..\ThridParty\Ghost.MeshOptimizer\Ghost.MeshOptimizer.csproj" />

View File

@@ -41,7 +41,7 @@ public class AssetCatalogTests
[TestMethod] [TestMethod]
public void TestAssetCatalog_UpsertLookup() public void TestAssetCatalog_UpsertLookup()
{ {
using var catalog = new AssetCatalog(_dbPath); var catalog = new AssetCatalog(_dbPath);
var guid = Guid.NewGuid(); var guid = Guid.NewGuid();
var meta = new AssetMeta { Guid = guid, HandlerVersion = 1 }; var meta = new AssetMeta { Guid = guid, HandlerVersion = 1 };
var path = "Textures/hero.png"; var path = "Textures/hero.png";
@@ -55,7 +55,7 @@ public class AssetCatalogTests
[TestMethod] [TestMethod]
public void TestAssetCatalog_Dependencies() public void TestAssetCatalog_Dependencies()
{ {
using var catalog = new AssetCatalog(_dbPath); var catalog = new AssetCatalog(_dbPath);
var asset1 = Guid.NewGuid(); var asset1 = Guid.NewGuid();
var asset2 = Guid.NewGuid(); var asset2 = Guid.NewGuid();
@@ -72,14 +72,14 @@ public class AssetCatalogTests
[TestMethod] [TestMethod]
public void TestAssetCatalog_VirtualSubAssets() public void TestAssetCatalog_VirtualSubAssets()
{ {
using var catalog = new AssetCatalog(_dbPath); var catalog = new AssetCatalog(_dbPath);
var parent = Guid.NewGuid(); var parent = Guid.NewGuid();
var subMesh = Guid.NewGuid(); var subMesh = Guid.NewGuid();
var handlerTypeId = Guid.NewGuid(); var handlerTypeId = Guid.NewGuid();
catalog.Upsert(new AssetMeta { Guid = parent, HandlerTypeId = handlerTypeId, HandlerVersion = 1 }, "Props/kit.fbx"); catalog.Upsert(new AssetMeta { Guid = parent, AssetTypeId = handlerTypeId, HandlerVersion = 1 }, "Props/kit.fbx");
catalog.UpsertSubAsset(parent, catalog.UpsertSubAsset(parent,
new AssetMeta { Guid = subMesh, HandlerTypeId = handlerTypeId, HandlerVersion = 1 }, new AssetMeta { Guid = subMesh, AssetTypeId = handlerTypeId, HandlerVersion = 1 },
"Props/kit.fbx#Mesh/Root/Crate", "Props/kit.fbx#Mesh/Root/Crate",
"Mesh", "Mesh",
"Crate", "Crate",

View File

@@ -30,7 +30,7 @@ public class AssetMetaTests
var originalMeta = new AssetMeta var originalMeta = new AssetMeta
{ {
Guid = Guid.NewGuid(), Guid = Guid.NewGuid(),
HandlerTypeId = Guid.NewGuid(), AssetTypeId = Guid.NewGuid(),
HandlerVersion = 1, HandlerVersion = 1,
Labels = ["test", "hero"] Labels = ["test", "hero"]
}; };
@@ -41,7 +41,7 @@ public class AssetMetaTests
var loadedMeta = await AssetMetaIO.ReadAsync(metaPath); var loadedMeta = await AssetMetaIO.ReadAsync(metaPath);
Assert.IsNotNull(loadedMeta); Assert.IsNotNull(loadedMeta);
Assert.AreEqual(originalMeta.Guid, loadedMeta.Guid); Assert.AreEqual(originalMeta.Guid, loadedMeta.Guid);
Assert.AreEqual(originalMeta.HandlerTypeId, loadedMeta.HandlerTypeId); Assert.AreEqual(originalMeta.AssetTypeId, loadedMeta.AssetTypeId);
Assert.AreEqual(originalMeta.HandlerVersion, loadedMeta.HandlerVersion); Assert.AreEqual(originalMeta.HandlerVersion, loadedMeta.HandlerVersion);
CollectionAssert.AreEqual(originalMeta.Labels, loadedMeta.Labels); CollectionAssert.AreEqual(originalMeta.Labels, loadedMeta.Labels);
} }