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)]
|
[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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s_handlerCache.GetOrAdd(info.HandlerType, t =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (IAssetHandler?)Activator.CreateInstance(t);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int GetVersionByAssetTypeId(Guid typeId)
|
public static bool TryGetHandlerInfoByAssetTypeId(Guid typeId, out AssetHandlerInfo info)
|
||||||
{
|
{
|
||||||
s_versionByTypeId.TryGetValue(typeId, out var version);
|
return s_byTypeId.TryGetValue(typeId, out info);
|
||||||
return version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<string> GetSupportedExtensions()
|
public static bool TryGetHandlerInfoByExtension(string extension, out AssetHandlerInfo info)
|
||||||
{
|
|
||||||
return s_byExtension.Keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AssetType GetRuntimeAssetTypeByExtension(string extension)
|
|
||||||
{
|
{
|
||||||
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()
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -264,12 +262,12 @@ internal class MeshAssetHandler : IImportableAssetHandler, IPackableAssetHandler
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var meshSettings = ResolveSettings(sourcePath, settings);
|
var meshSettings = ResolveSettings(sourcePath, settings);
|
||||||
|
|
||||||
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,70 +374,62 @@ 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);
|
||||||
|
|
||||||
|
var bounds = ComputeBounds(geometry.Vertices);
|
||||||
|
var header = new MeshContentHeader
|
||||||
{
|
{
|
||||||
MeshProcessor.BuildMeshlets(ref meshletData, geometry.Vertices.AsReadOnly(), geometry.Indices.AsReadOnly(), geometry.MaterialParts.AsSpan());
|
magic = MeshContentHeader.MAGIC,
|
||||||
MeshProcessor.BuildClusterLodHierarchy(ref meshletData, AllocationHandle.Persistent);
|
version = MeshContentHeader.VERSION,
|
||||||
|
vertexCount = (uint)geometry.Vertices.Count,
|
||||||
|
indexCount = (uint)geometry.Indices.Count,
|
||||||
|
materialPartCount = (uint)geometry.MaterialParts.Length,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
var bounds = ComputeBounds(geometry.Vertices);
|
using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
var header = new MeshContentHeader
|
stream.Write(header);
|
||||||
{
|
|
||||||
magic = MeshContentHeader.MAGIC,
|
|
||||||
version = MeshContentHeader.VERSION,
|
|
||||||
vertexCount = (uint)geometry.Vertices.Count,
|
|
||||||
indexCount = (uint)geometry.Indices.Count,
|
|
||||||
materialPartCount = (uint)geometry.MaterialParts.Length,
|
|
||||||
meshletCount = (uint)meshletData.meshlets.Count,
|
|
||||||
meshletGroupCount = (uint)meshletData.groups.Count,
|
|
||||||
meshletHierarchyNodeCount = (uint)meshletData.hierarchyNodes.Count,
|
|
||||||
meshletVertexCount = (uint)meshletData.meshletVertices.Count,
|
|
||||||
meshletTriangleCount = (uint)meshletData.meshletTriangles.Count,
|
|
||||||
materialSlotCount = (uint)meshletData.materialSlotCount,
|
|
||||||
lodLevelCount = (uint)meshletData.lodLevelCount,
|
|
||||||
boundsMin = bounds.Min,
|
|
||||||
boundsMax = bounds.Max,
|
|
||||||
};
|
|
||||||
|
|
||||||
using var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
header.vertexOffset = (ulong)stream.Position;
|
||||||
stream.Write(header);
|
await stream.WriteAsync<Vertex, UnsafeList<Vertex>>(geometry.Vertices, token);
|
||||||
|
|
||||||
header.vertexOffset = (ulong)stream.Position;
|
header.indexOffset = (ulong)stream.Position;
|
||||||
await stream.WriteAsync<Vertex, UnsafeList<Vertex>>(geometry.Vertices, token);
|
await stream.WriteAsync<uint, UnsafeList<uint>>(geometry.Indices, token);
|
||||||
|
|
||||||
header.indexOffset = (ulong)stream.Position;
|
header.materialPartOffset = (ulong)stream.Position;
|
||||||
await stream.WriteAsync<uint, UnsafeList<uint>>(geometry.Indices, token);
|
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
|
||||||
|
|
||||||
header.materialPartOffset = (ulong)stream.Position;
|
header.meshletOffset = (ulong)stream.Position;
|
||||||
WriteMaterialParts(stream, geometry.MaterialParts.AsSpan());
|
await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.GetRef().meshlets, token);
|
||||||
|
|
||||||
header.meshletOffset = (ulong)stream.Position;
|
header.meshletGroupOffset = (ulong)stream.Position;
|
||||||
await stream.WriteAsync<Meshlet, UnsafeList<Meshlet>>(meshletData.meshlets, token);
|
await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.GetRef().groups, token);
|
||||||
|
|
||||||
header.meshletGroupOffset = (ulong)stream.Position;
|
header.meshletHierarchyNodeOffset = (ulong)stream.Position;
|
||||||
await stream.WriteAsync<MeshletGroup, UnsafeList<MeshletGroup>>(meshletData.groups, token);
|
await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.GetRef().hierarchyNodes, token);
|
||||||
|
|
||||||
header.meshletHierarchyNodeOffset = (ulong)stream.Position;
|
header.meshletVertexOffset = (ulong)stream.Position;
|
||||||
await stream.WriteAsync<MeshletHierarchyNode, UnsafeList<MeshletHierarchyNode>>(meshletData.hierarchyNodes, token);
|
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletVertices, token);
|
||||||
|
|
||||||
header.meshletVertexOffset = (ulong)stream.Position;
|
header.meshletTriangleOffset = (ulong)stream.Position;
|
||||||
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.meshletVertices, token);
|
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.GetRef().meshletTriangles, token);
|
||||||
|
|
||||||
header.meshletTriangleOffset = (ulong)stream.Position;
|
stream.Position = 0;
|
||||||
await stream.WriteAsync<uint, UnsafeList<uint>>(meshletData.meshletTriangles, token);
|
stream.Write(header);
|
||||||
|
stream.Flush();
|
||||||
|
|
||||||
stream.Position = 0;
|
return (meshletData.GetRef().materialSlotCount, meshletData.GetRef().lodLevelCount);
|
||||||
stream.Write(header);
|
|
||||||
stream.Flush();
|
|
||||||
|
|
||||||
return (meshletData.materialSlotCount, meshletData.lodLevelCount);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
meshletData.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AABB ComputeBounds(UnsafeList<Vertex> vertices)
|
private static AABB ComputeBounds(UnsafeList<Vertex> vertices)
|
||||||
|
|||||||
@@ -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,32 +347,18 @@ 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);
|
||||||
|
var count = Encoding.UTF8.GetBytes(_filePath, str.AsSpan());
|
||||||
|
str[count] = 0;
|
||||||
|
|
||||||
|
using var scene = new DisposablePtr<ufbx_scene>(ufbx_scene.LoadFile((sbyte*)str.GetUnsafePtr(), &load_Opts, &error));
|
||||||
|
if (scene.Get() == null)
|
||||||
{
|
{
|
||||||
reserveCapacity = 256 * 1024 * 1024 // 256 MB should be enough for most meshes, adjust as needed.
|
Logger.Error(error.description.ToString());
|
||||||
});
|
return;
|
||||||
|
|
||||||
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());
|
|
||||||
str[count] = 0;
|
|
||||||
|
|
||||||
using var scene = new DisposablePtr<ufbx_scene>(ufbx_scene.LoadFile((sbyte*)str.GetUnsafePtr(), &load_Opts, &error));
|
|
||||||
if (scene.Get() == null)
|
|
||||||
{
|
|
||||||
Logger.Error(error.description.ToString());
|
|
||||||
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.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,53 +821,97 @@ internal static unsafe partial class MeshProcessor
|
|||||||
simplifyFallbackSloppy = true,
|
simplifyFallbackSloppy = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var i = 0; i < parts.Length; i++)
|
var jobs = new MeshletBuildJob[parts.Length];
|
||||||
|
|
||||||
|
IntPtr meshletData;
|
||||||
|
unsafe
|
||||||
{
|
{
|
||||||
ref readonly var part = ref parts[i];
|
// 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.
|
||||||
// Each part references a slice of the global index buffer,
|
meshletData = (IntPtr)NativeMemory.AllocZeroed(MemoryUtility.SizeOf<MeshletMeshData>());
|
||||||
// but vertex positions are the full unified buffer so global indices remain valid.
|
|
||||||
var clodMesh = new ClodMesh
|
|
||||||
{
|
|
||||||
vertexPositions = (float*)Unsafe.AsPointer(in vertices[0].position),
|
|
||||||
vertexCount = (nuint)vertices.Count,
|
|
||||||
vertexPositionsStride = (nuint)sizeof(Vertex),
|
|
||||||
vertexAttributes = (float*)Unsafe.AsPointer(in vertices[0].normal),
|
|
||||||
vertexAttributesStride = (nuint)sizeof(Vertex),
|
|
||||||
indices = (uint*)indices.GetUnsafePtr() + part.indexStart,
|
|
||||||
indexCount = (nuint)part.indexCount,
|
|
||||||
attributeProtectMask = 0, // TODO: Protect UVs at material boundaries.
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = new MeshletContext
|
|
||||||
{
|
|
||||||
data = ref meshletData,
|
|
||||||
materialIndex = part.materialIndex
|
|
||||||
};
|
|
||||||
|
|
||||||
Build(in config, in clodMesh, ref context, MeshletOutputCallback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
meshletData.meshletCount = meshletData.meshlets.IsCreated ? meshletData.meshlets.Count : 0;
|
try
|
||||||
|
|
||||||
if (meshletData.groups.IsCreated && meshletData.groups.Count > 0)
|
|
||||||
{
|
{
|
||||||
var maxLodLevel = 0u;
|
for (var i = 0; i < parts.Length; i++)
|
||||||
for (var j = 0; j < meshletData.groups.Count; j++)
|
|
||||||
{
|
{
|
||||||
maxLodLevel = Math.Max(maxLodLevel, meshletData.groups[j].lodLevel);
|
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
|
||||||
|
{
|
||||||
|
vertexPositions = (float*)Unsafe.AsPointer(in vertices[0].position),
|
||||||
|
vertexCount = (nuint)vertices.Count,
|
||||||
|
vertexPositionsStride = (nuint)sizeof(Vertex),
|
||||||
|
vertexAttributes = (float*)Unsafe.AsPointer(in vertices[0].normal),
|
||||||
|
vertexAttributesStride = (nuint)sizeof(Vertex),
|
||||||
|
indices = (uint*)indices.GetUnsafePtr() + part.indexStart,
|
||||||
|
indexCount = (nuint)part.indexCount,
|
||||||
|
attributeProtectMask = 0, // TODO: Protect UVs at material boundaries.
|
||||||
|
};
|
||||||
|
|
||||||
|
var context = new MeshletContext
|
||||||
|
{
|
||||||
|
data = (MeshletMeshData*)meshletData,
|
||||||
|
materialIndex = part.materialIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
var job = new MeshletBuildJob
|
||||||
|
{
|
||||||
|
clodConfig = config,
|
||||||
|
clodMesh = clodMesh,
|
||||||
|
context = context
|
||||||
|
};
|
||||||
|
|
||||||
|
jobs[i] = job;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meshletData.lodLevelCount = (int)maxLodLevel + 1;
|
foreach (var job in jobs)
|
||||||
}
|
{
|
||||||
|
var handle = jobScheduler.Schedule(in job);
|
||||||
|
await jobScheduler.WaitAsync(handle, token);
|
||||||
|
}
|
||||||
|
|
||||||
var maxMaterialSlot = 0;
|
unsafe
|
||||||
for (var j = 0; j < parts.Length; j++)
|
{
|
||||||
|
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 < pMeshletData->groups.Count; j++)
|
||||||
|
{
|
||||||
|
maxLodLevel = Math.Max(maxLodLevel, pMeshletData->groups[j].lodLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
pMeshletData->lodLevelCount = (int)maxLodLevel + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxMaterialSlot = 0;
|
||||||
|
for (var j = 0; j < parts.Length; j++)
|
||||||
|
{
|
||||||
|
maxMaterialSlot = Math.Max(maxMaterialSlot, parts[j].materialIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
pMeshletData->materialSlotCount = maxMaterialSlot + 1;
|
||||||
|
|
||||||
|
return new DisposablePtr<MeshletMeshData>(pMeshletData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
{
|
{
|
||||||
maxMaterialSlot = Math.Max(maxMaterialSlot, parts[j].materialIndex);
|
unsafe
|
||||||
}
|
{
|
||||||
|
NativeMemory.Free((void*)meshletData);
|
||||||
|
}
|
||||||
|
|
||||||
meshletData.materialSlotCount = maxMaterialSlot + 1;
|
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,6 +1133,67 @@ internal static unsafe partial class MeshProcessor
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
gathered.Dispose();
|
gathered.Dispose();
|
||||||
|
scope.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe struct BuildClusterLodHierarchyJob : IJob
|
||||||
|
{
|
||||||
|
public MeshletMeshData* meshletData;
|
||||||
|
|
||||||
|
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, scope.AllocationHandle);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rootIndex = BuildBinaryTree(ref binaryNodes, meshletIndices, 0, meshletIndices.Length, meshletData->meshlets);
|
||||||
|
|
||||||
|
if (!meshletData->hierarchyNodes.IsCreated)
|
||||||
|
{
|
||||||
|
meshletData->hierarchyNodes = new UnsafeList<MeshletHierarchyNode>(meshletData->meshletCount, AllocationHandle.TLSF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binaryNodes[rootIndex].leftChild == -1)
|
||||||
|
{
|
||||||
|
var bvhNode = new MeshletHierarchyNode();
|
||||||
|
bvhNode.minX = new float4(float.PositiveInfinity);
|
||||||
|
bvhNode.minY = new float4(float.PositiveInfinity);
|
||||||
|
bvhNode.minZ = new float4(float.PositiveInfinity);
|
||||||
|
bvhNode.maxX = new float4(float.NegativeInfinity);
|
||||||
|
bvhNode.maxY = new float4(float.NegativeInfinity);
|
||||||
|
bvhNode.maxZ = new float4(float.NegativeInfinity);
|
||||||
|
bvhNode.maxParentError = new float4(0);
|
||||||
|
bvhNode.nodeData = new uint4(0xFFFFFFFF);
|
||||||
|
|
||||||
|
var childNode = binaryNodes[rootIndex];
|
||||||
|
bvhNode.minX.x = childNode.bounds.Min.x;
|
||||||
|
bvhNode.minY.x = childNode.bounds.Min.y;
|
||||||
|
bvhNode.minZ.x = childNode.bounds.Min.z;
|
||||||
|
bvhNode.maxX.x = childNode.bounds.Max.x;
|
||||||
|
bvhNode.maxY.x = childNode.bounds.Max.y;
|
||||||
|
bvhNode.maxZ.x = childNode.bounds.Max.z;
|
||||||
|
bvhNode.maxParentError.x = childNode.maxParentError;
|
||||||
|
bvhNode.nodeData.x = (uint)childNode.meshletIndex;
|
||||||
|
|
||||||
|
meshletData->hierarchyNodes.Add(bvhNode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CollapseTo4Ary(binaryNodes, rootIndex, meshletData->hierarchyNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
binaryNodes.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1080,59 +1201,24 @@ internal static unsafe partial class MeshProcessor
|
|||||||
/// Builds a cluster LOD hierarchy from the input meshlet data.
|
/// Builds a cluster LOD hierarchy from the input meshlet data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="meshletData">The meshlet data.</param>
|
/// <param name="meshletData">The meshlet data.</param>
|
||||||
public static void BuildClusterLodHierarchy(ref MeshletMeshData meshletData, AllocationHandle allocationHandle)
|
public static async Task BuildClusterLodHierarchyAsync(JobScheduler jobScheduler, SharedPtr<MeshletMeshData> meshletData, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (meshletData.meshletCount == 0) return;
|
if (meshletData.GetRef().meshletCount == 0)
|
||||||
|
|
||||||
using var meshletIndices = new UnsafeArray<int>(meshletData.meshletCount, AllocationHandle.Persistent);
|
|
||||||
for (var i = 0; i < meshletData.meshletCount; i++)
|
|
||||||
{
|
{
|
||||||
meshletIndices[i] = i;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var binaryNodes = new UnsafeList<TempBinaryNode>(meshletData.meshletCount * 2, AllocationHandle.Persistent);
|
JobHandle handle;
|
||||||
|
unsafe
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var rootIndex = BuildBinaryTree(ref binaryNodes, meshletIndices, 0, meshletIndices.Length, meshletData.meshlets);
|
var job = new BuildClusterLodHierarchyJob
|
||||||
|
|
||||||
if (!meshletData.hierarchyNodes.IsCreated)
|
|
||||||
{
|
{
|
||||||
meshletData.hierarchyNodes = new UnsafeList<MeshletHierarchyNode>(meshletData.meshletCount, allocationHandle);
|
meshletData = meshletData.Get()
|
||||||
}
|
};
|
||||||
|
|
||||||
if (binaryNodes[rootIndex].leftChild == -1)
|
handle = jobScheduler.Schedule(in job);
|
||||||
{
|
|
||||||
var bvhNode = new MeshletHierarchyNode();
|
|
||||||
bvhNode.minX = new float4(float.PositiveInfinity);
|
|
||||||
bvhNode.minY = new float4(float.PositiveInfinity);
|
|
||||||
bvhNode.minZ = new float4(float.PositiveInfinity);
|
|
||||||
bvhNode.maxX = new float4(float.NegativeInfinity);
|
|
||||||
bvhNode.maxY = new float4(float.NegativeInfinity);
|
|
||||||
bvhNode.maxZ = new float4(float.NegativeInfinity);
|
|
||||||
bvhNode.maxParentError = new float4(0);
|
|
||||||
bvhNode.nodeData = new uint4(0xFFFFFFFF);
|
|
||||||
|
|
||||||
var childNode = binaryNodes[rootIndex];
|
|
||||||
bvhNode.minX.x = childNode.bounds.Min.x;
|
|
||||||
bvhNode.minY.x = childNode.bounds.Min.y;
|
|
||||||
bvhNode.minZ.x = childNode.bounds.Min.z;
|
|
||||||
bvhNode.maxX.x = childNode.bounds.Max.x;
|
|
||||||
bvhNode.maxY.x = childNode.bounds.Max.y;
|
|
||||||
bvhNode.maxZ.x = childNode.bounds.Max.z;
|
|
||||||
bvhNode.maxParentError.x = childNode.maxParentError;
|
|
||||||
bvhNode.nodeData.x = (uint)childNode.meshletIndex;
|
|
||||||
|
|
||||||
meshletData.hierarchyNodes.Add(bvhNode);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CollapseTo4Ary(binaryNodes, rootIndex, meshletData.hierarchyNodes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
binaryNodes.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
// 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;
|
||||||
|
|||||||
@@ -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."));
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,38 +30,59 @@ 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)";
|
||||||
_cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)");
|
private const string SqlClearDeps = "DELETE FROM dependencies WHERE from_guid = @guid";
|
||||||
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
|
private const string SqlEnumerate = "SELECT guid, source_path FROM assets";
|
||||||
_cmdEnumerate = CreateCommand("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";
|
||||||
_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");
|
private const string SqlDeleteSubAssetsForParent = "DELETE FROM assets WHERE parent_guid = @parent_guid";
|
||||||
_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();
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateSchemaInternal(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SqliteCommand CreateCommand(string sql)
|
private SqliteConnection OpenConnection()
|
||||||
{
|
{
|
||||||
var cmd = _connection.CreateCommand();
|
var connection = new SqliteConnection(_connectionString);
|
||||||
cmd.CommandText = sql;
|
connection.Open();
|
||||||
return cmd;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateSchema()
|
private static void CreateSchemaInternal(SqliteConnection connection)
|
||||||
{
|
{
|
||||||
using var cmd = _connection.CreateCommand();
|
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,
|
||||||
imported_at_ms INTEGER,
|
imported_at_ms INTEGER,
|
||||||
parent_guid BLOB(16),
|
parent_guid BLOB (16),
|
||||||
subasset_kind TEXT,
|
subasset_kind TEXT,
|
||||||
display_name TEXT,
|
display_name TEXT,
|
||||||
stable_path TEXT
|
stable_path 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)
|
||||||
@@ -153,64 +112,53 @@ public sealed partial class AssetCatalog : IDisposable
|
|||||||
{
|
{
|
||||||
return Path.GetFullPath(path).Replace('\\', '/');
|
return Path.GetFullPath(path).Replace('\\', '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
|
cmd.CommandText = SqlGetImportedAt;
|
||||||
if (result is long ticks)
|
cmd.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||||
{
|
|
||||||
return new DateTime(ticks, DateTimeKind.Utc);
|
var result = cmd.ExecuteScalar();
|
||||||
}
|
return result is long ticks ? new DateTime(ticks, DateTimeKind.Utc) : null;
|
||||||
|
|
||||||
return 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())
|
||||||
{
|
{
|
||||||
@@ -336,48 +308,28 @@ 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)
|
using var connection = OpenConnection();
|
||||||
{
|
using var cmd = connection.CreateCommand();
|
||||||
_cmdDeleteSubAssetsForParent.Parameters.Clear();
|
cmd.CommandText = SqlDeleteSubAssetsForParent;
|
||||||
_cmdDeleteSubAssetsForParent.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
|
cmd.Parameters.AddWithValue("@parent_guid", parentGuid.ToByteArray());
|
||||||
_cmdDeleteSubAssetsForParent.ExecuteNonQuery();
|
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))
|
Remove(subAsset.Guid);
|
||||||
{
|
|
||||||
_cmdDelete.Parameters.Clear();
|
|
||||||
_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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -20,4 +21,5 @@ public static class ShaderPropertiesRegistry
|
|||||||
{
|
{
|
||||||
return s_nameToCode.TryGetValue(name, out info);
|
return s_nameToCode.TryGetValue(name, out info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user