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:
@@ -6,9 +6,30 @@ namespace Ghost.Editor.Core.Assets;
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
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
|
||||
@@ -33,9 +54,6 @@ public interface IAssetExportOptions;
|
||||
|
||||
public interface IAssetHandler
|
||||
{
|
||||
AssetType RuntimeAssetType { get; }
|
||||
Guid EditorAssetTypeID { get; }
|
||||
|
||||
IAssetSettings? CreateDefaultSettings(string ext);
|
||||
|
||||
ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default);
|
||||
@@ -44,12 +62,10 @@ public interface 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> 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
|
||||
{
|
||||
|
||||
@@ -1,36 +1,54 @@
|
||||
using Ghost.Engine;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
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
|
||||
{
|
||||
private static readonly Dictionary<string, IAssetHandler> s_byExtension;
|
||||
private static readonly Dictionary<string, AssetType> s_typeByExtension;
|
||||
private static readonly Dictionary<Guid, IAssetHandler> s_byTypeId;
|
||||
private static readonly Dictionary<Guid, int> s_versionByTypeId;
|
||||
|
||||
private static readonly Dictionary<string, AssetHandlerInfo> s_byExtension;
|
||||
private static readonly Dictionary<Guid, AssetHandlerInfo> s_byTypeId;
|
||||
private static readonly List<(Type Type, string Name)> s_iAssetSettingsTypes;
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, IAssetHandler?> s_handlerCache;
|
||||
|
||||
static AssetHandlerRegistry()
|
||||
{
|
||||
s_byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase);
|
||||
s_typeByExtension = new Dictionary<string, AssetType>(StringComparer.OrdinalIgnoreCase);
|
||||
s_byTypeId = new Dictionary<Guid, IAssetHandler>();
|
||||
s_versionByTypeId = new Dictionary<Guid, int>();
|
||||
s_byExtension = new Dictionary<string, AssetHandlerInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
s_byTypeId = new Dictionary<Guid, AssetHandlerInfo>();
|
||||
|
||||
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;
|
||||
s_versionByTypeId[assetTypeId] = version;
|
||||
var info = new AssetHandlerInfo
|
||||
{
|
||||
HandlerType = handlerType,
|
||||
RuntimeAssetType = runtimeAssetType,
|
||||
EditorAssetTypeID = assetTypeId,
|
||||
Version = version
|
||||
};
|
||||
|
||||
s_byTypeId[assetTypeId] = info;
|
||||
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
var normalizedExt = ext.StartsWith('.') ? ext : "." + ext;
|
||||
s_byExtension[normalizedExt] = handler;
|
||||
s_typeByExtension[normalizedExt] = handler.RuntimeAssetType;
|
||||
s_byExtension[normalizedExt] = info;
|
||||
}
|
||||
|
||||
if (allowCaching)
|
||||
{
|
||||
s_handlerCache[handlerType] = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,36 +65,59 @@ public static class AssetHandlerRegistry
|
||||
}
|
||||
|
||||
var normalized = extension.StartsWith('.') ? extension : "." + extension;
|
||||
s_byExtension.TryGetValue(normalized, out var handler);
|
||||
return handler;
|
||||
if (!s_byExtension.TryGetValue(normalized, out var info))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return s_handlerCache.GetOrAdd(info.HandlerType, t =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return (IAssetHandler?)Activator.CreateInstance(t);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static IAssetHandler? GetByAssetTypeId(Guid typeId)
|
||||
{
|
||||
s_byTypeId.TryGetValue(typeId, out var handler);
|
||||
return handler;
|
||||
}
|
||||
|
||||
public static int GetVersionByAssetTypeId(Guid typeId)
|
||||
if (!s_byTypeId.TryGetValue(typeId, out var info))
|
||||
{
|
||||
s_versionByTypeId.TryGetValue(typeId, out var version);
|
||||
return version;
|
||||
return null;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
return AssetType.Unknown;
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -24,9 +24,9 @@ public sealed class AssetMeta
|
||||
public required Guid Guid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The Guid that identifies which IAssetHandler processes this asset.
|
||||
/// The Guid that identifies type id of asset.
|
||||
/// </summary>
|
||||
public Guid? HandlerTypeId { get; set; }
|
||||
public Guid? AssetTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the handler that last imported this asset.
|
||||
|
||||
@@ -4,6 +4,7 @@ using Ghost.Editor.Core.Services;
|
||||
using Ghost.Engine;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.Jobs;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using Misaki.HighPerformance.Mathematics;
|
||||
@@ -13,6 +14,7 @@ using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using TerraFX.Interop.Mimalloc;
|
||||
|
||||
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
|
||||
{
|
||||
private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions
|
||||
@@ -207,11 +209,7 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
public AssetType RuntimeAssetType => AssetType.Mesh;
|
||||
|
||||
public Guid EditorAssetTypeID => typeof(MeshAsset).GUID;
|
||||
|
||||
public bool CanExport => false;
|
||||
private readonly JobScheduler _jobScheduler = EditorApplication.GetService<EngineCore>().JobScheduler;
|
||||
|
||||
public IAssetSettings? CreateDefaultSettings(string ext)
|
||||
{
|
||||
@@ -268,8 +266,8 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
||||
using var root = new MeshNode();
|
||||
|
||||
var parseJob = new MeshParsingJob(root, sourcePath, AllocationHandle.Persistent, meshSettings);
|
||||
var context = default(Misaki.HighPerformance.Jobs.JobExecutionContext);
|
||||
parseJob.Execute(in context);
|
||||
var handle = _jobScheduler.Schedule(in parseJob);
|
||||
await _jobScheduler.WaitAsync(handle, token);
|
||||
|
||||
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)
|
||||
{
|
||||
return ValueTask.FromResult(Result.Failure("Packing model assets is not supported yet."));
|
||||
@@ -314,7 +307,7 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
||||
: new FbxAssetSettings();
|
||||
}
|
||||
|
||||
private static async ValueTask<ModelManifestNode> WriteNodeAsync(
|
||||
private async ValueTask<ModelManifestNode> WriteNodeAsync(
|
||||
Guid parentGuid,
|
||||
string sourcePath,
|
||||
MeshNode node,
|
||||
@@ -381,13 +374,10 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
||||
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();
|
||||
try
|
||||
{
|
||||
MeshProcessor.BuildMeshlets(ref meshletData, geometry.Vertices.AsReadOnly(), geometry.Indices.AsReadOnly(), geometry.MaterialParts.AsSpan());
|
||||
MeshProcessor.BuildClusterLodHierarchy(ref meshletData, AllocationHandle.Persistent);
|
||||
using var meshletData = await MeshProcessor.BuildMeshletsAsync(_jobScheduler, geometry.Vertices, geometry.Indices, geometry.MaterialParts, token).ConfigureAwait(false);
|
||||
await MeshProcessor.BuildClusterLodHierarchyAsync(_jobScheduler, meshletData.Share(), token).ConfigureAwait(false);
|
||||
|
||||
var bounds = ComputeBounds(geometry.Vertices);
|
||||
var header = new MeshContentHeader
|
||||
@@ -397,13 +387,13 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
||||
vertexCount = (uint)geometry.Vertices.Count,
|
||||
indexCount = (uint)geometry.Indices.Count,
|
||||
materialPartCount = (uint)geometry.MaterialParts.Length,
|
||||
meshletCount = (uint)meshletData.meshlets.Count,
|
||||
meshletGroupCount = (uint)meshletData.groups.Count,
|
||||
meshletHierarchyNodeCount = (uint)meshletData.hierarchyNodes.Count,
|
||||
meshletVertexCount = (uint)meshletData.meshletVertices.Count,
|
||||
meshletTriangleCount = (uint)meshletData.meshletTriangles.Count,
|
||||
materialSlotCount = (uint)meshletData.materialSlotCount,
|
||||
lodLevelCount = (uint)meshletData.lodLevelCount,
|
||||
meshletCount = (uint)meshletData.GetRef().meshlets.Count,
|
||||
meshletGroupCount = (uint)meshletData.GetRef().groups.Count,
|
||||
meshletHierarchyNodeCount = (uint)meshletData.GetRef().hierarchyNodes.Count,
|
||||
meshletVertexCount = (uint)meshletData.GetRef().meshletVertices.Count,
|
||||
meshletTriangleCount = (uint)meshletData.GetRef().meshletTriangles.Count,
|
||||
materialSlotCount = (uint)meshletData.GetRef().materialSlotCount,
|
||||
lodLevelCount = (uint)meshletData.GetRef().lodLevelCount,
|
||||
boundsMin = bounds.Min,
|
||||
boundsMax = bounds.Max,
|
||||
};
|
||||
@@ -421,30 +411,25 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
||||
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
|
||||
|
||||
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;
|
||||
await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.groups, token);
|
||||
await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.GetRef().groups, token);
|
||||
|
||||
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;
|
||||
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.meshletVertices, token);
|
||||
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletVertices, token);
|
||||
|
||||
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.Write(header);
|
||||
stream.Flush();
|
||||
|
||||
return (meshletData.materialSlotCount, meshletData.lodLevelCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
meshletData.Dispose();
|
||||
}
|
||||
return (meshletData.GetRef().materialSlotCount, meshletData.GetRef().lodLevelCount);
|
||||
}
|
||||
|
||||
private static AABB ComputeBounds(UnsafeList<Vertex> vertices)
|
||||
|
||||
@@ -13,7 +13,7 @@ using Misaki.HighPerformance.Mathematics;
|
||||
using System.Runtime.CompilerServices;
|
||||
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;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -92,7 +92,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
|
||||
|
||||
if (node->mesh != null)
|
||||
{
|
||||
var geoNode = ParseGeometry(node->mesh, ref pool);
|
||||
var geoNode = ParseGeometry(node->mesh, allocationHandle);
|
||||
if (geoNode != null)
|
||||
{
|
||||
children.Add(geoNode);
|
||||
@@ -104,14 +104,14 @@ internal readonly unsafe struct MeshParsingJob : IJob
|
||||
for (var i = 0u; i < node->children.count; i++)
|
||||
{
|
||||
var childNode = new MeshNode();
|
||||
ParseHierarchy(node->children.data[i], childNode, ref pool);
|
||||
ParseHierarchy(node->children.data[i], childNode, allocationHandle);
|
||||
childNode.Parent = self;
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -122,20 +122,18 @@ internal readonly unsafe struct MeshParsingJob : IJob
|
||||
|
||||
// Bucket faces by material
|
||||
|
||||
using var scope = pool.Allocator.CreateScope(pool.AllocationHandle);
|
||||
|
||||
using var materialBuckets = new UnsafeArray<UnsafeList<Vertex>>(numMaterials, scope.AllocationHandle);
|
||||
using var missingNormalsBucket = new UnsafeArray<bool>(numMaterials, scope.AllocationHandle);
|
||||
using var missingTangentsBucket = new UnsafeArray<bool>(numMaterials, scope.AllocationHandle);
|
||||
using var materialBuckets = new UnsafeArray<UnsafeList<Vertex>>(numMaterials, allocationHandle);
|
||||
using var missingNormalsBucket = new UnsafeArray<bool>(numMaterials, allocationHandle);
|
||||
using var missingTangentsBucket = new UnsafeArray<bool>(numMaterials, allocationHandle);
|
||||
|
||||
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);
|
||||
|
||||
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++)
|
||||
{
|
||||
@@ -196,7 +194,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
|
||||
|
||||
// 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++)
|
||||
{
|
||||
@@ -209,8 +207,8 @@ internal readonly unsafe struct MeshParsingJob : IJob
|
||||
|
||||
var numIndices = (uint)flatVertices.Count;
|
||||
|
||||
using var weldedIndices = new UnsafeArray<uint>((int)numIndices, scope.AllocationHandle);
|
||||
using var cachedIndices = new UnsafeArray<uint>((int)numIndices, scope.AllocationHandle);
|
||||
using var weldedIndices = new UnsafeArray<uint>((int)numIndices, allocationHandle);
|
||||
using var cachedIndices = new UnsafeArray<uint>((int)numIndices, allocationHandle);
|
||||
|
||||
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);
|
||||
|
||||
// Allocate temporary per-part buffers (will be merged then disposed)
|
||||
var partVertices = new UnsafeList<Vertex>((int)numUniqueVertices, scope.AllocationHandle);
|
||||
var partIndices = new UnsafeList<uint>((int)numIndices, scope.AllocationHandle);
|
||||
var partVertices = new UnsafeList<Vertex>((int)numUniqueVertices, 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));
|
||||
|
||||
@@ -349,16 +347,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
|
||||
load_Opts.obj_search_mtl_by_filename = true;
|
||||
}
|
||||
|
||||
var stack = new StackPool(new VirtualStack.CreationOptions
|
||||
{
|
||||
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);
|
||||
using var str = new UnsafeArray<byte>(Encoding.UTF8.GetByteCount(_filePath) + 1, AllocationHandle.TLSF);
|
||||
var count = Encoding.UTF8.GetBytes(_filePath, str.AsSpan());
|
||||
str[count] = 0;
|
||||
|
||||
@@ -369,12 +358,7 @@ internal readonly unsafe struct MeshParsingJob : IJob
|
||||
return;
|
||||
}
|
||||
|
||||
ParseHierarchy(scene.Get()->root_node, _rootNode, ref stack);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stack.Dispose();
|
||||
}
|
||||
ParseHierarchy(scene.Get()->root_node, _rootNode, AllocationHandle.TLSF);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ using Ghost.Core;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Ghost.MeshOptimizer;
|
||||
using Misaki.HighPerformance.Jobs;
|
||||
using Misaki.HighPerformance.LowLevel;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using Misaki.HighPerformance.Mathematics;
|
||||
using Misaki.HighPerformance.Mathematics.Geometry;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using TLSFPool = Misaki.HighPerformance.LowLevel.Buffer.MemoryPool<Misaki.HighPerformance.LowLevel.Buffer.TLSF, Misaki.HighPerformance.LowLevel.Buffer.TLSF.CreationOptions>;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Editor.Core.Assets;
|
||||
|
||||
@@ -162,7 +164,7 @@ public unsafe struct ClodCluster
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -392,7 +394,7 @@ internal static unsafe partial class MeshProcessor
|
||||
|
||||
private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh,
|
||||
UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth,
|
||||
ref MeshletContext outputContext, ClodOutputDelegate? outputCallback,
|
||||
MeshletContext outputContext, ClodOutputDelegate? outputCallback,
|
||||
AllocationHandle 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 result = outputCallback != null
|
||||
? outputCallback(ref outputContext, clodGroup, groupClusters.AsReadOnly())
|
||||
? outputCallback(outputContext, clodGroup, groupClusters.AsReadOnly())
|
||||
: -1;
|
||||
|
||||
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="outputCallback">Delegate invoked for each generated LOD group.</param>
|
||||
/// <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)");
|
||||
|
||||
using var pool = new TLSFPool(new TLSF.CreationOptions
|
||||
{
|
||||
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);
|
||||
using var locks = new UnsafeArray<byte>((int)mesh.vertexCount, AllocationHandle.TLSF, AllocationOption.Clear); ;
|
||||
using var remap = new UnsafeArray<uint>((int)mesh.vertexCount, AllocationHandle.TLSF);
|
||||
|
||||
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++)
|
||||
{
|
||||
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++)
|
||||
{
|
||||
pending.Add(i);
|
||||
@@ -630,14 +626,14 @@ internal static unsafe partial class MeshProcessor
|
||||
|
||||
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();
|
||||
|
||||
LockBoundary(locks, groups, clusters, remap, mesh.vertexLock);
|
||||
|
||||
for (var i = 0; i < groups.Count; i++)
|
||||
{
|
||||
using var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, 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++)
|
||||
{
|
||||
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 bounds = MergeBounds(clusters, groups[i], pool.AllocationHandle);
|
||||
var bounds = MergeBounds(clusters, groups[i], AllocationHandle.TLSF);
|
||||
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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++)
|
||||
{
|
||||
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++)
|
||||
{
|
||||
split[j].refined = refined;
|
||||
@@ -688,7 +684,7 @@ internal static unsafe partial class MeshProcessor
|
||||
{
|
||||
var bounds = clusters[pending[0]].bounds;
|
||||
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;
|
||||
@@ -701,33 +697,33 @@ internal static unsafe partial class MeshProcessor
|
||||
return finalClusterCount;
|
||||
}
|
||||
|
||||
private ref struct MeshletContext
|
||||
private struct MeshletContext
|
||||
{
|
||||
public ref MeshletMeshData data;
|
||||
public MeshletMeshData* data;
|
||||
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;
|
||||
|
||||
// Ensure lists are initialized
|
||||
if (!meshletData.groups.IsCreated) meshletData.groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent);
|
||||
if (!meshletData.meshlets.IsCreated) meshletData.meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent);
|
||||
if (!meshletData.meshletVertices.IsCreated) meshletData.meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent);
|
||||
if (!meshletData.meshletTriangles.IsCreated) meshletData.meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.Persistent);
|
||||
if (!meshletData->groups.IsCreated) meshletData->groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.TLSF);
|
||||
if (!meshletData->meshlets.IsCreated) meshletData->meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.TLSF);
|
||||
if (!meshletData->meshletVertices.IsCreated) meshletData->meshletVertices = new UnsafeList<uint>(128, AllocationHandle.TLSF);
|
||||
if (!meshletData->meshletTriangles.IsCreated) meshletData->meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.TLSF);
|
||||
|
||||
var meshletGroup = new MeshletGroup
|
||||
{
|
||||
boundingSphere = new SphereBounds(group.simplified.center, group.simplified.radius),
|
||||
boundingBox = new AABB(group.simplified.center - group.simplified.radius, group.simplified.center + group.simplified.radius),
|
||||
parentError = group.simplified.error,
|
||||
meshletStartIndex = (uint)meshletData.meshlets.Count,
|
||||
meshletStartIndex = (uint)meshletData->meshlets.Count,
|
||||
meshletCount = (uint)clusters.Count,
|
||||
lodLevel = (uint)group.depth
|
||||
};
|
||||
meshletData.groups.Add(meshletGroup);
|
||||
meshletData->groups.Add(meshletGroup);
|
||||
|
||||
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),
|
||||
vertexCount = (byte)cluster.vertexCount,
|
||||
triangleCount = (byte)(cluster.localIndexCount / 3),
|
||||
vertexOffset = (uint)meshletData.meshletVertices.Count,
|
||||
triangleOffset = (uint)meshletData.meshletTriangles.Count,
|
||||
groupIndex = (uint)meshletData.groups.Count - 1,
|
||||
vertexOffset = (uint)meshletData->meshletVertices.Count,
|
||||
triangleOffset = (uint)meshletData->meshletTriangles.Count,
|
||||
groupIndex = (uint)meshletData->groups.Count - 1,
|
||||
clusterError = cluster.bounds.error,
|
||||
parentError = group.simplified.error,
|
||||
localMaterialIndex = (byte)materialIndex,
|
||||
lodLevel = (byte)group.depth,
|
||||
};
|
||||
meshletData.meshlets.Add(meshlet);
|
||||
meshletData->meshlets.Add(meshlet);
|
||||
|
||||
// Add unique vertices
|
||||
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)
|
||||
var triangleCount = cluster.localIndexCount / 3;
|
||||
@@ -763,11 +759,29 @@ internal static unsafe partial class MeshProcessor
|
||||
uint i1 = cluster.localIndices[j * 3 + 1];
|
||||
uint i2 = cluster.localIndices[j * 3 + 2];
|
||||
var packedTriangle = i0 | (i1 << 8) | (i2 << 16);
|
||||
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>
|
||||
@@ -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.
|
||||
/// Meshlets are built per-part and tagged with the corresponding <c>localMaterialIndex</c>.
|
||||
/// </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(indices.Count > 0, "Mesh must have indices to build meshlets.");
|
||||
Logger.DebugAssert(parts.Length > 0, "Must have at least one material part.");
|
||||
@@ -806,10 +821,24 @@ internal static unsafe partial class MeshProcessor
|
||||
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++)
|
||||
{
|
||||
ref readonly var part = ref parts[i];
|
||||
|
||||
unsafe
|
||||
{
|
||||
// Each part references a slice of the global index buffer,
|
||||
// but vertex positions are the full unified buffer so global indices remain valid.
|
||||
var clodMesh = new ClodMesh
|
||||
@@ -826,24 +855,41 @@ internal static unsafe partial class MeshProcessor
|
||||
|
||||
var context = new MeshletContext
|
||||
{
|
||||
data = ref meshletData,
|
||||
data = (MeshletMeshData*)meshletData,
|
||||
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;
|
||||
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;
|
||||
@@ -852,7 +898,20 @@ internal static unsafe partial class MeshProcessor
|
||||
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
|
||||
@@ -992,7 +1051,8 @@ internal static unsafe partial class MeshProcessor
|
||||
return -1;
|
||||
}
|
||||
|
||||
var gathered = new UnsafeList<int>(4, AllocationHandle.Persistent);
|
||||
var scope = AllocationManager.CreateStackScope();
|
||||
var gathered = new UnsafeList<int>(4, scope.AllocationHandle);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1073,32 +1133,32 @@ internal static unsafe partial class MeshProcessor
|
||||
finally
|
||||
{
|
||||
gathered.Dispose();
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a cluster LOD hierarchy from the input meshlet data.
|
||||
/// </summary>
|
||||
/// <param name="meshletData">The meshlet data.</param>
|
||||
public static void BuildClusterLodHierarchy(ref MeshletMeshData meshletData, AllocationHandle allocationHandle)
|
||||
private unsafe struct BuildClusterLodHierarchyJob : IJob
|
||||
{
|
||||
if (meshletData.meshletCount == 0) return;
|
||||
public MeshletMeshData* meshletData;
|
||||
|
||||
using var meshletIndices = new UnsafeArray<int>(meshletData.meshletCount, AllocationHandle.Persistent);
|
||||
for (var i = 0; i < meshletData.meshletCount; i++)
|
||||
public readonly void Execute(ref readonly JobExecutionContext ctx)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
var binaryNodes = new UnsafeList<TempBinaryNode>(meshletData.meshletCount * 2, AllocationHandle.Persistent);
|
||||
var binaryNodes = new UnsafeList<TempBinaryNode>(meshletData->meshletCount * 2, scope.AllocationHandle);
|
||||
|
||||
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)
|
||||
@@ -1123,11 +1183,11 @@ internal static unsafe partial class MeshProcessor
|
||||
bvhNode.maxParentError.x = childNode.maxParentError;
|
||||
bvhNode.nodeData.x = (uint)childNode.meshletIndex;
|
||||
|
||||
meshletData.hierarchyNodes.Add(bvhNode);
|
||||
meshletData->hierarchyNodes.Add(bvhNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
CollapseTo4Ary(binaryNodes, rootIndex, meshletData.hierarchyNodes);
|
||||
CollapseTo4Ary(binaryNodes, rootIndex, meshletData->hierarchyNodes);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
[CustomAssetHandler(GraphicsShaderAsset.GUID, [".gshdr"], 1)]
|
||||
[CustomAssetHandler(AssetTypeId = GraphicsShaderAsset.GUID, RuntimeAssetType = AssetType.Shader, Extensions = new[] { ".gshdr" })]
|
||||
internal class GraphicsShaderAssetHandler : IPackableAssetHandler
|
||||
{
|
||||
public AssetType RuntimeAssetType => AssetType.Shader;
|
||||
public Guid EditorAssetTypeID => typeof(GraphicsShaderAsset).GUID;
|
||||
|
||||
public IAssetSettings? CreateDefaultSettings(string ext)
|
||||
{
|
||||
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
|
||||
{
|
||||
public AssetType RuntimeAssetType => AssetType.Shader;
|
||||
public Guid EditorAssetTypeID => typeof(ComputeShaderAsset).GUID;
|
||||
|
||||
public IAssetSettings? CreateDefaultSettings(string ext)
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -249,7 +249,7 @@ public class TextureAssetSettings : IAssetSettings
|
||||
} = 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 struct TextureInfo
|
||||
@@ -263,10 +263,6 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
|
||||
public bool isHDR;
|
||||
}
|
||||
|
||||
public bool CanExport => false;
|
||||
public AssetType RuntimeAssetType => AssetType.Texture;
|
||||
public Guid EditorAssetTypeID => typeof(TextureAsset).GUID;
|
||||
|
||||
public IAssetSettings? CreateDefaultSettings(string ext)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return ValueTask.FromResult(Result.Failure("Packing texture assets is not supported yet."));
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
|
||||
<ProjectReference Include="..\..\ThridParty\Ghost.Ufbx\Ghost.Ufbx.csproj" />
|
||||
<ProjectReference Include="..\..\ThridParty\Ghost.StbI\Ghost.StbI.csproj" />
|
||||
<ProjectReference Include="..\Ghost.DSL\Ghost.DSL.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,64 +5,24 @@ namespace Ghost.Editor.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
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 Lock _writeLock = new();
|
||||
private readonly string _connectionString;
|
||||
|
||||
// Prepared statements
|
||||
private readonly SqliteCommand _cmdGetGuid;
|
||||
private readonly SqliteCommand _cmdGetPath;
|
||||
private readonly SqliteCommand _cmdUpsert;
|
||||
private readonly SqliteCommand _cmdDelete;
|
||||
|
||||
private readonly SqliteCommand _cmdGetHandlerTypeId;
|
||||
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)
|
||||
private const string SqlGetGuid = "SELECT guid FROM assets WHERE source_path = @path";
|
||||
private const string SqlGetPath = "SELECT source_path FROM assets WHERE guid = @guid";
|
||||
private const string SqlGetAssetTypeId = "SELECT asset_type_id FROM assets WHERE guid = @guid";
|
||||
private const string SqlGetImportedAt = "SELECT imported_at_ms FROM assets WHERE guid = @guid";
|
||||
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)
|
||||
VALUES (@guid, @path, @asset_type_id, @version, @content_hash, @settings_hash, @imported_at_ms, @parent_guid, @subasset_kind, @display_name, @stable_path)
|
||||
ON CONFLICT(guid) DO UPDATE SET
|
||||
source_path = excluded.source_path,
|
||||
handler_type_id = excluded.handler_type_id,
|
||||
asset_type_id = excluded.asset_type_id,
|
||||
handler_version = excluded.handler_version,
|
||||
content_hash = excluded.content_hash,
|
||||
settings_hash = excluded.settings_hash,
|
||||
@@ -70,33 +30,54 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
parent_guid = excluded.parent_guid,
|
||||
subasset_kind = excluded.subasset_kind,
|
||||
display_name = excluded.display_name,
|
||||
stable_path = excluded.stable_path");
|
||||
_cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid");
|
||||
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid");
|
||||
_cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid");
|
||||
stable_path = excluded.stable_path";
|
||||
private const string SqlDelete = "DELETE FROM assets WHERE guid = @guid";
|
||||
private const string SqlGetReferencers = "SELECT from_guid FROM dependencies WHERE to_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)");
|
||||
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
|
||||
_cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets");
|
||||
_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");
|
||||
public AssetCatalog(string dbPath)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
||||
|
||||
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)
|
||||
{
|
||||
var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
return cmd;
|
||||
CreateSchemaInternal(connection);
|
||||
}
|
||||
|
||||
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 = @"
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
guid BLOB (16) PRIMARY KEY NOT NULL,
|
||||
source_path TEXT NOT NULL,
|
||||
handler_type_id BLOB(16),
|
||||
asset_type_id BLOB (16),
|
||||
handler_version INTEGER NOT NULL DEFAULT 0,
|
||||
content_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);";
|
||||
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)
|
||||
@@ -159,58 +118,47 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
|
||||
public Guid GetGuid(string sourcePath)
|
||||
{
|
||||
_cmdGetGuid.Parameters.Clear();
|
||||
_cmdGetGuid.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
|
||||
var result = _cmdGetGuid.ExecuteScalar();
|
||||
using var connection = OpenConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
|
||||
cmd.CommandText = SqlGetGuid;
|
||||
cmd.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
|
||||
var result = cmd.ExecuteScalar();
|
||||
return result is byte[] bytes ? new Guid(bytes) : Guid.Empty;
|
||||
}
|
||||
|
||||
public string? GetSourcePath(Guid guid)
|
||||
{
|
||||
_cmdGetPath.Parameters.Clear();
|
||||
_cmdGetPath.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
return _cmdGetPath.ExecuteScalar() as string;
|
||||
using var connection = OpenConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
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)
|
||||
{
|
||||
_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", DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@subasset_kind", DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@display_name", DBNull.Value);
|
||||
_cmdUpsert.Parameters.AddWithValue("@stable_path", DBNull.Value);
|
||||
_cmdUpsert.ExecuteNonQuery();
|
||||
}
|
||||
using var connection = OpenConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = SqlUpsert;
|
||||
cmd.Parameters.AddWithValue("@guid", meta.Guid.ToByteArray());
|
||||
cmd.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
|
||||
cmd.Parameters.AddWithValue("@asset_type_id", meta.AssetTypeId?.ToByteArray() ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@version", meta.HandlerVersion);
|
||||
cmd.Parameters.AddWithValue("@content_hash", meta.ContentHash ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@settings_hash", meta.SettingsHash ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@imported_at_ms", meta.LastImportedUtc?.Ticks ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@parent_guid", parentGuid?.ToByteArray() ?? (object)DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@subasset_kind", (object?)kind ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@display_name", (object?)displayName ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@stable_path", (object?)stablePath ?? DBNull.Value);
|
||||
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)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
=> UpsertInternal(meta, sourcePath, parentGuid, kind, displayName, stablePath);
|
||||
|
||||
public bool Remove(Guid guid)
|
||||
{
|
||||
@@ -220,65 +168,79 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
Remove(sub.Guid);
|
||||
}
|
||||
|
||||
lock (_writeLock)
|
||||
{
|
||||
_cmdDelete.Parameters.Clear();
|
||||
_cmdDelete.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
return _cmdDelete.ExecuteNonQuery() > 0;
|
||||
}
|
||||
using var connection = OpenConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
|
||||
cmd.CommandText = SqlDelete;
|
||||
cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
return cmd.ExecuteNonQuery() > 0;
|
||||
}
|
||||
|
||||
public Guid GetHandlerTypeId(Guid guid)
|
||||
public Guid GetAssetTypeId(Guid guid)
|
||||
{
|
||||
_cmdGetHandlerTypeId.Parameters.Clear();
|
||||
_cmdGetHandlerTypeId.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
var result = _cmdGetHandlerTypeId.ExecuteScalar();
|
||||
using var connection = OpenConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
|
||||
cmd.CommandText = SqlGetAssetTypeId;
|
||||
cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
|
||||
var result = cmd.ExecuteScalar();
|
||||
return result is byte[] bytes ? new Guid(bytes) : Guid.Empty;
|
||||
}
|
||||
|
||||
public DateTime? GetImportedAt(Guid guid)
|
||||
{
|
||||
_cmdGetImportedAt.Parameters.Clear();
|
||||
_cmdGetImportedAt.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
var result = _cmdGetImportedAt.ExecuteScalar();
|
||||
using var connection = OpenConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
|
||||
if (result is long ticks)
|
||||
{
|
||||
return new DateTime(ticks, DateTimeKind.Utc);
|
||||
}
|
||||
cmd.CommandText = SqlGetImportedAt;
|
||||
cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
|
||||
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)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
using var tx = _connection.BeginTransaction();
|
||||
_cmdClearDeps.Transaction = tx;
|
||||
_cmdClearDeps.Parameters.Clear();
|
||||
_cmdClearDeps.Parameters.AddWithValue("@guid", assetId.ToByteArray());
|
||||
_cmdClearDeps.ExecuteNonQuery();
|
||||
using var connection = OpenConnection();
|
||||
using var tx = connection.BeginTransaction();
|
||||
|
||||
using (var clearCmd = connection.CreateCommand())
|
||||
{
|
||||
clearCmd.Transaction = tx;
|
||||
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)
|
||||
{
|
||||
_cmdInsertDep.Parameters.Clear();
|
||||
_cmdInsertDep.Parameters.AddWithValue("@from", assetId.ToByteArray());
|
||||
_cmdInsertDep.Parameters.AddWithValue("@to", dep.ToByteArray());
|
||||
_cmdInsertDep.ExecuteNonQuery();
|
||||
toParam.Value = dep.ToByteArray();
|
||||
insertCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Guid> GetReferencers(Guid guid)
|
||||
{
|
||||
_cmdGetReferencers.Parameters.Clear();
|
||||
_cmdGetReferencers.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
using var connection = OpenConnection();
|
||||
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>();
|
||||
while (reader.Read())
|
||||
{
|
||||
@@ -290,10 +252,13 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
|
||||
public List<Guid> GetDependencies(Guid guid)
|
||||
{
|
||||
_cmdGetDependencies.Parameters.Clear();
|
||||
_cmdGetDependencies.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
using var connection = OpenConnection();
|
||||
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>();
|
||||
while (reader.Read())
|
||||
{
|
||||
@@ -305,7 +270,11 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
|
||||
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())
|
||||
{
|
||||
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)
|
||||
{
|
||||
_cmdEnumerateSubAssets.Parameters.Clear();
|
||||
_cmdEnumerateSubAssets.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
|
||||
using var connection = OpenConnection();
|
||||
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>();
|
||||
while (reader.Read())
|
||||
{
|
||||
@@ -335,49 +307,29 @@ public sealed partial class AssetCatalog : IDisposable
|
||||
}
|
||||
|
||||
public void RemoveSubAssetsExcept(Guid parentGuid, ReadOnlySpan<Guid> keepGuids)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
if (keepGuids.Length == 0)
|
||||
{
|
||||
_cmdDeleteSubAssetsForParent.Parameters.Clear();
|
||||
_cmdDeleteSubAssetsForParent.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
|
||||
_cmdDeleteSubAssetsForParent.ExecuteNonQuery();
|
||||
using var connection = OpenConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = SqlDeleteSubAssetsForParent;
|
||||
cmd.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
|
||||
cmd.ExecuteNonQuery();
|
||||
return;
|
||||
}
|
||||
|
||||
var keep = new HashSet<Guid>();
|
||||
for (var i = 0; i < keepGuids.Length; i++)
|
||||
var keep = new HashSet<Guid>(keepGuids.Length);
|
||||
foreach (var guid in keepGuids)
|
||||
{
|
||||
keep.Add(keepGuids[i]);
|
||||
keep.Add(guid);
|
||||
}
|
||||
|
||||
foreach (var subAsset in GetSubAssets(parentGuid))
|
||||
{
|
||||
if (!keep.Contains(subAsset.Guid))
|
||||
{
|
||||
_cmdDelete.Parameters.Clear();
|
||||
_cmdDelete.Parameters.AddWithValue("@guid", subAsset.Guid.ToByteArray());
|
||||
_cmdDelete.ExecuteNonQuery();
|
||||
Remove(subAsset.Guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,11 +226,16 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var handlerTypeId = handler?.EditorAssetTypeID;
|
||||
var assetTypeId = Guid.Empty;
|
||||
if (AssetHandlerRegistry.TryGetHandlerInfoByExtension(ext, out var handlerInfo))
|
||||
{
|
||||
assetTypeId = handlerInfo.EditorAssetTypeID;
|
||||
}
|
||||
|
||||
var meta = new AssetMeta
|
||||
{
|
||||
Guid = Guid.NewGuid(),
|
||||
HandlerTypeId = handlerTypeId,
|
||||
AssetTypeId = assetTypeId,
|
||||
HandlerVersion = 1,
|
||||
Settings = handler?.CreateDefaultSettings(ext)
|
||||
};
|
||||
@@ -420,7 +425,6 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
{
|
||||
_watcher.Dispose();
|
||||
_importCoordinator.Dispose();
|
||||
_catalog.Dispose();
|
||||
_loadLock.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,12 @@ internal class EditorContentProvider : IContentProvider
|
||||
|
||||
public AssetType GetAssetType(Guid guid)
|
||||
{
|
||||
var handlerID = _catalog.GetHandlerTypeId(guid);
|
||||
var handler = AssetHandlerRegistry.GetByAssetTypeId(handlerID);
|
||||
return handler?.RuntimeAssetType ?? AssetType.Unknown;
|
||||
var assetTypeID = _catalog.GetAssetTypeId(guid);
|
||||
if (AssetHandlerRegistry.TryGetHandlerInfoByAssetTypeId(assetTypeID, out var info))
|
||||
{
|
||||
return info.RuntimeAssetType;
|
||||
}
|
||||
|
||||
return AssetType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,18 +95,21 @@ internal sealed partial class ImportCoordinator : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var handler = meta.HandlerTypeId.HasValue
|
||||
? AssetHandlerRegistry.GetByAssetTypeId(meta.HandlerTypeId.Value)
|
||||
var handler = meta.AssetTypeId.HasValue
|
||||
? AssetHandlerRegistry.GetByAssetTypeId(meta.AssetTypeId.Value)
|
||||
: AssetHandlerRegistry.GetByExtension(Path.GetExtension(job.SourcePath));
|
||||
|
||||
var contentHash = await ComputeFileHashAsync(job.SourcePath, token);
|
||||
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)
|
||||
if (job.Reason != ImportReason.ManualReimport &&
|
||||
meta.ContentHash == contentHash &&
|
||||
meta.SettingsHash == settingsHash &&
|
||||
meta.HandlerVersion == AssetHandlerRegistry.GetVersionByAssetTypeId(meta.HandlerTypeId ?? Guid.Empty))
|
||||
meta.HandlerVersion == handlerVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -128,7 +131,7 @@ internal sealed partial class ImportCoordinator : IDisposable
|
||||
{
|
||||
meta.ContentHash = contentHash;
|
||||
meta.SettingsHash = settingsHash;
|
||||
meta.HandlerVersion = AssetHandlerRegistry.GetVersionByAssetTypeId(meta.HandlerTypeId ?? Guid.Empty);
|
||||
meta.HandlerVersion = handlerVersion;
|
||||
meta.LastImportedUtc = DateTime.UtcNow;
|
||||
|
||||
await AssetMetaIO.WriteAsync(job.MetaPath, meta, token);
|
||||
@@ -144,7 +147,7 @@ internal sealed partial class ImportCoordinator : IDisposable
|
||||
var subMeta = new AssetMeta
|
||||
{
|
||||
Guid = subAsset.Guid,
|
||||
HandlerTypeId = subAsset.HandlerTypeId,
|
||||
AssetTypeId = subAsset.AssetTypeId,
|
||||
HandlerVersion = meta.HandlerVersion,
|
||||
ContentHash = contentHash,
|
||||
SettingsHash = settingsHash,
|
||||
|
||||
@@ -58,9 +58,11 @@ internal static class ActivationHandler
|
||||
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.
|
||||
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.
|
||||
FreeListChunkSize = 64 * 1024 * 1024,
|
||||
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,
|
||||
FreeListDefaultAlignment = 8,
|
||||
TLSFInitialChunkSize = 64 * 1024,
|
||||
TLSFAlignment = 8,
|
||||
};
|
||||
|
||||
AllocationManager.Initialize(opts);
|
||||
|
||||
@@ -90,8 +90,12 @@ internal partial class ContentBrowserViewModel : ObservableObject
|
||||
if (!isDir)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -144,7 +148,7 @@ internal partial class ContentBrowserViewModel : ObservableObject
|
||||
}
|
||||
|
||||
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);
|
||||
Files.Add(fileItem);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
<IsTrimmable>True</IsTrimmable>
|
||||
</PropertyGroup>
|
||||
@@ -22,12 +22,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Misaki.HighPerformance" Version="1.0.9" />
|
||||
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.1.6" />
|
||||
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.20">
|
||||
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.24">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<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="TerraFX.Interop.Mimalloc" Version="1.6.7.2" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace Ghost.DSL;
|
||||
namespace Ghost.Core.Graphics;
|
||||
|
||||
#if DEBUG || GHOST_EDITOR
|
||||
public struct ShaderPropertyInfo
|
||||
{
|
||||
public string shaderName;
|
||||
@@ -21,3 +22,4 @@ public static class ShaderPropertiesRegistry
|
||||
return s_nameToCode.TryGetValue(name, out info);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,9 +1,20 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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
|
||||
{
|
||||
private static TempJobAllocator* _pAllocator;
|
||||
@@ -13,7 +24,8 @@ public unsafe partial struct TempJobAllocator
|
||||
internal static void Initialize(nuint capacity)
|
||||
{
|
||||
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()
|
||||
@@ -28,8 +40,8 @@ public unsafe partial struct TempJobAllocator
|
||||
_pAllocator->_pArena[i].Dispose();
|
||||
}
|
||||
|
||||
MemoryUtility.Free(_pAllocator->_pArena);
|
||||
MemoryUtility.Free(_pAllocator);
|
||||
NativeMemory.Free(_pAllocator->_pArena);
|
||||
NativeMemory.Free(_pAllocator);
|
||||
|
||||
_pAllocator = null;
|
||||
}
|
||||
@@ -38,40 +50,33 @@ public unsafe partial struct TempJobAllocator
|
||||
public unsafe partial struct TempJobAllocator : IAllocator
|
||||
{
|
||||
private const int _FRAME_LATENCY = 4;
|
||||
private const int _MAGIC_ID = -559038737;
|
||||
|
||||
private VirtualArena* _pArena;
|
||||
private int _currentFrameCount;
|
||||
private int _currentFrameIndex;
|
||||
#if MHP_ENABLE_SAFETY_CHECKS
|
||||
private fixed int _allocationsPerFrame[_FRAME_LATENCY];
|
||||
#endif
|
||||
|
||||
private MemoryHandle _memoryHandle;
|
||||
private AllocationHandle _handle;
|
||||
private readonly AllocationHandle _handle;
|
||||
|
||||
public readonly AllocationHandle Handle => _handle;
|
||||
|
||||
internal TempJobAllocator(void* pSelf, nuint capacity)
|
||||
{
|
||||
var memoryHandle = default(MemoryHandle);
|
||||
|
||||
_pArena = (VirtualArena*)Malloc((nuint)(sizeof(VirtualArena) * _FRAME_LATENCY));
|
||||
_pArena = (VirtualArena*)NativeMemory.Alloc((nuint)(sizeof(VirtualArena) * _FRAME_LATENCY));
|
||||
_currentFrameCount = 0;
|
||||
_currentFrameIndex = 0;
|
||||
_memoryHandle = memoryHandle;
|
||||
|
||||
for (var i = 0; i < _FRAME_LATENCY; i++)
|
||||
{
|
||||
_pArena[i] = new VirtualArena(capacity);
|
||||
#if MHP_ENABLE_SAFETY_CHECKS
|
||||
_allocationsPerFrame[i] = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
_handle = new AllocationHandle
|
||||
{
|
||||
State = Unsafe.AsPointer(ref this),
|
||||
Alloc = &Allocate,
|
||||
Realloc = &Reallocate,
|
||||
Free = &Free
|
||||
};
|
||||
_handle = new AllocationHandle(pSelf, &Allocate, &Reallocate, &Free);
|
||||
}
|
||||
|
||||
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
|
||||
@@ -84,7 +89,9 @@ public unsafe partial struct TempJobAllocator : IAllocator
|
||||
return null;
|
||||
}
|
||||
|
||||
#if MHP_ENABLE_SAFETY_CHECKS
|
||||
Interlocked.Increment(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]);
|
||||
#endif
|
||||
return ptr;
|
||||
}
|
||||
|
||||
@@ -103,7 +110,7 @@ public unsafe partial struct TempJobAllocator : IAllocator
|
||||
return null;
|
||||
}
|
||||
|
||||
MemCpy(ptr, newPtr, Math.Min(oldSize, newSize));
|
||||
MemoryUtility.MemCpy(ptr, newPtr, Math.Min(oldSize, newSize));
|
||||
|
||||
return newPtr;
|
||||
}
|
||||
@@ -111,7 +118,9 @@ public unsafe partial struct TempJobAllocator : IAllocator
|
||||
private static void Free(void* instance, void* ptr)
|
||||
{
|
||||
var pSelf = (TempJobAllocator*)instance;
|
||||
#if MHP_ENABLE_SAFETY_CHECKS
|
||||
Interlocked.Decrement(ref pSelf->_allocationsPerFrame[pSelf->_currentFrameIndex]);
|
||||
#endif
|
||||
}
|
||||
|
||||
public int AdvanceFrame()
|
||||
@@ -123,6 +132,13 @@ public unsafe partial struct TempJobAllocator : IAllocator
|
||||
|
||||
(_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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -22,6 +23,29 @@ internal class AssetHandlerRegistrationGenerator : IIncrementalGenerator
|
||||
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)
|
||||
{
|
||||
if (array.IsDefaultOrEmpty)
|
||||
@@ -39,12 +63,24 @@ internal class AssetHandlerRegistrationGenerator : IIncrementalGenerator
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = attribute.ConstructorArguments[0].Value as string;
|
||||
var extensionsTypesConstants = attribute.ConstructorArguments[1].Values;
|
||||
var extensions = $"new string[] {{ {string.Join(", ", extensionsTypesConstants.Select(v => v.ToCSharpString()))} }}";
|
||||
var version = (int)attribute.ConstructorArguments[2].Value;
|
||||
var properties = attribute.NamedArguments.ToDictionary(kv => kv.Key, kv => kv.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";
|
||||
|
||||
@@ -176,6 +176,7 @@ namespace {info.TypeSymbol.ContainingNamespace.ToDisplayString()}
|
||||
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)]
|
||||
{info.TypeSymbol.DeclaredAccessibility.ToString().ToLower()} partial struct {info.TypeSymbol.Name}
|
||||
{{
|
||||
#if DEBUG || GHOST_EDITOR
|
||||
public const string HLSL_SOURCE = @""
|
||||
# ifndef {definedSymbol}
|
||||
# define {definedSymbol}
|
||||
@@ -185,13 +186,14 @@ struct {info.Name}
|
||||
}};
|
||||
# endif // {definedSymbol}"";
|
||||
}}
|
||||
#endif
|
||||
}}";
|
||||
|
||||
context.AddSource($"{info.TypeSymbol.Name}_HLSL.gen.cs", code);
|
||||
codeBuilder.Clear();
|
||||
|
||||
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";
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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.RHI\Ghost.Graphics.RHI.csproj" />
|
||||
<ProjectReference Include="..\..\ThridParty\Ghost.MeshOptimizer\Ghost.MeshOptimizer.csproj" />
|
||||
|
||||
@@ -41,7 +41,7 @@ public class AssetCatalogTests
|
||||
[TestMethod]
|
||||
public void TestAssetCatalog_UpsertLookup()
|
||||
{
|
||||
using var catalog = new AssetCatalog(_dbPath);
|
||||
var catalog = new AssetCatalog(_dbPath);
|
||||
var guid = Guid.NewGuid();
|
||||
var meta = new AssetMeta { Guid = guid, HandlerVersion = 1 };
|
||||
var path = "Textures/hero.png";
|
||||
@@ -55,7 +55,7 @@ public class AssetCatalogTests
|
||||
[TestMethod]
|
||||
public void TestAssetCatalog_Dependencies()
|
||||
{
|
||||
using var catalog = new AssetCatalog(_dbPath);
|
||||
var catalog = new AssetCatalog(_dbPath);
|
||||
var asset1 = Guid.NewGuid();
|
||||
var asset2 = Guid.NewGuid();
|
||||
|
||||
@@ -72,14 +72,14 @@ public class AssetCatalogTests
|
||||
[TestMethod]
|
||||
public void TestAssetCatalog_VirtualSubAssets()
|
||||
{
|
||||
using var catalog = new AssetCatalog(_dbPath);
|
||||
var catalog = new AssetCatalog(_dbPath);
|
||||
var parent = Guid.NewGuid();
|
||||
var subMesh = 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,
|
||||
new AssetMeta { Guid = subMesh, HandlerTypeId = handlerTypeId, HandlerVersion = 1 },
|
||||
new AssetMeta { Guid = subMesh, AssetTypeId = handlerTypeId, HandlerVersion = 1 },
|
||||
"Props/kit.fbx#Mesh/Root/Crate",
|
||||
"Mesh",
|
||||
"Crate",
|
||||
|
||||
@@ -30,7 +30,7 @@ public class AssetMetaTests
|
||||
var originalMeta = new AssetMeta
|
||||
{
|
||||
Guid = Guid.NewGuid(),
|
||||
HandlerTypeId = Guid.NewGuid(),
|
||||
AssetTypeId = Guid.NewGuid(),
|
||||
HandlerVersion = 1,
|
||||
Labels = ["test", "hero"]
|
||||
};
|
||||
@@ -41,7 +41,7 @@ public class AssetMetaTests
|
||||
var loadedMeta = await AssetMetaIO.ReadAsync(metaPath);
|
||||
Assert.IsNotNull(loadedMeta);
|
||||
Assert.AreEqual(originalMeta.Guid, loadedMeta.Guid);
|
||||
Assert.AreEqual(originalMeta.HandlerTypeId, loadedMeta.HandlerTypeId);
|
||||
Assert.AreEqual(originalMeta.AssetTypeId, loadedMeta.AssetTypeId);
|
||||
Assert.AreEqual(originalMeta.HandlerVersion, loadedMeta.HandlerVersion);
|
||||
CollectionAssert.AreEqual(originalMeta.Labels, loadedMeta.Labels);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user