Refactor asset handler system and catalog for safety

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

View File

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

View File

@@ -0,0 +1,25 @@
namespace Ghost.Core.Graphics;
#if DEBUG || GHOST_EDITOR
public struct ShaderPropertyInfo
{
public string shaderName;
public string code;
public uint size;
}
public static class ShaderPropertiesRegistry
{
private static readonly Dictionary<string, ShaderPropertyInfo> s_nameToCode = new Dictionary<string, ShaderPropertyInfo>(StringComparer.Ordinal);
public static void Register(string name, string code, uint size)
{
s_nameToCode[name] = new ShaderPropertyInfo { shaderName = name, code = code, size = size };
}
public static bool TryGetInfo(string name, out ShaderPropertyInfo info)
{
return s_nameToCode.TryGetValue(name, out info);
}
}
#endif

View File

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

View File

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

View File

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

View File

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