feat(asset): asset manager + registration generator

Add runtime AssetManager and AssetHandlerRegistrationGenerator source generator.
Update editor asset handler types and services to work with new registration
mechanism and asset catalog. Remove legacy contracts ICloneable and IReleasable.

Files added:
- src/Runtime/Ghost.Engine/AssetManager.cs
- src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs

Major edits:
- Editor asset handler classes and services (Asset*, Texture*, Registry)
- Runtime Handle.cs and project files
- Render graph executor and tests updated accordingly

This commit introduces the foundation for the modern asset pipeline
including generated registration of asset handlers and a centralized
runtime AssetManager that will drive asset lifecycle.
This commit is contained in:
2026-04-15 22:21:00 +09:00
parent 6615fe794e
commit 13bf1501e4
29 changed files with 425 additions and 365 deletions

View File

@@ -0,0 +1,90 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
namespace Ghost.Generator;
[Generator]
internal class AssetHandlerRegistrationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var handerCandidates = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (s, _) => s is ClassDeclarationSyntax,
transform: GetAssetHandlerSymbol)
.Where(symbol => symbol != null)
.Collect();
context.RegisterSourceOutput(handerCandidates, GenerateRegistrationCode);
}
private void GenerateRegistrationCode(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> array)
{
if (array.IsDefaultOrEmpty)
{
return;
}
var sb = new System.Text.StringBuilder();
foreach (var symbol in array)
{
var attribute = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "Ghost.Editor.Core.AssetHandler.CustomAssetHandlerAttribute");
if (attribute == null)
{
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;
sb.Append($" global::Ghost.Editor.Core.AssetHandler.AssetHandlerRegistry.RegisterHandler(new {symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}(), Guid.Parse(\"{id}\"), {extensions}, {version});");
}
var registerTypeName = "g_assethandler_registeration";
var code = $@"// <auto-generated />
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName}
{{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void RegisterAssetHandlers()
{{
{sb}
}}
}}";
context.AddSource($"{registerTypeName}.gen.cs", code);
}
private INamedTypeSymbol GetAssetHandlerSymbol(GeneratorSyntaxContext context, CancellationToken token)
{
var classSyntax = (ClassDeclarationSyntax)context.Node;
if (context.SemanticModel.GetDeclaredSymbol(classSyntax) is not INamedTypeSymbol symbol)
{
return null;
}
var iHandlerSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("Ghost.Editor.Core.AssetHandler.IAssetHandler");
if (iHandlerSymbol == null)
{
return null;
}
foreach (var iface in symbol.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(iface, iHandlerSymbol))
{
return symbol;
}
}
return null;
}
}

View File

@@ -1,6 +1,5 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
@@ -14,7 +13,6 @@ namespace Ghost.Generator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. Pipeline: Find all structs implementing IComponent
var componentCandidates = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (s, _) => s is StructDeclarationSyntax,
@@ -22,7 +20,6 @@ namespace Ghost.Generator
.Where(symbol => symbol != null)
.Collect();
// 4. Output: Generate the source using both pieces of data at once
context.RegisterSourceOutput(componentCandidates, GenerateRegistrationCode);
}
@@ -30,7 +27,7 @@ namespace Ghost.Generator
private static INamedTypeSymbol GetComponentSymbol(GeneratorSyntaxContext ctx, CancellationToken _)
{
var structSyntax = (StructDeclarationSyntax)ctx.Node;
if (!(ctx.SemanticModel.GetDeclaredSymbol(structSyntax) is INamedTypeSymbol symbol))
if (ctx.SemanticModel.GetDeclaredSymbol(structSyntax) is not INamedTypeSymbol symbol)
{
return null;
}
@@ -73,7 +70,7 @@ namespace Ghost.Generator
}
var typeName = $"g_component_registration";
var code = $@"// <auto-generated/>
var code = $@"// <auto-generated/>
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {typeName}

View File

@@ -13,11 +13,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
</ItemGroup>
</Project>