From 13bf1501e4643b836fb5fecf023114f4fff5d1ca Mon Sep 17 00:00:00 2001 From: Misaki Date: Wed, 15 Apr 2026 22:21:00 +0900 Subject: [PATCH] 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. --- src/Editor/Ghost.DSL/Ghost.DSL.csproj | 2 +- .../Ghost.Editor.Core/AssetHandler/Asset.cs | 110 +------- .../AssetHandler/AssetHandler.cs | 27 +- .../AssetHandler/AssetHandlerRegistry.cs | 55 +--- .../AssetHandler/AssetMeta.cs | 6 +- .../AssetHandler/TextureAsset.cs | 241 ++++++++---------- .../AssetHandler/TextureProcessor.cs | 2 +- src/Editor/Ghost.Editor.Core/Attributes.cs | 14 +- .../Ghost.Editor.Core.csproj | 5 +- .../Services/AssetCatalog.cs | 13 + .../Services/AssetRegistry.cs | 27 +- .../Services/ImportCoordinator.cs | 2 +- .../Ghost.Editor.Core/Utilities/TypeCache.cs | 48 +++- src/Editor/Ghost.Editor/App.xaml.cs | 1 + src/Editor/Ghost.Editor/Ghost.Editor.csproj | 2 +- .../Ghost.Core/Contracts/ICloneable.cs | 11 - .../Ghost.Core/Contracts/IReleasable.cs | 6 - src/Runtime/Ghost.Core/Ghost.Core.csproj | 2 +- src/Runtime/Ghost.Core/Handle.cs | 49 +++- src/Runtime/Ghost.Engine/AssetManager.cs | 44 ++++ .../AssetHandlerRegistrationGenerator.cs | 90 +++++++ .../ComponentRegistrationGenerator.cs | 7 +- .../Ghost.Generator/Ghost.Generator.csproj | 4 +- .../Ghost.Graphics.D3D12.csproj | 2 +- .../RenderGraphModule/RenderGraphExecutor.cs | 1 - .../Ghost.Graphics.Test.csproj | 10 +- .../Systems/RenderExtractionSystem.cs | 3 +- src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj | 4 +- .../Ghost.NativeWrapperGen.csproj | 2 +- 29 files changed, 425 insertions(+), 365 deletions(-) delete mode 100644 src/Runtime/Ghost.Core/Contracts/ICloneable.cs delete mode 100644 src/Runtime/Ghost.Core/Contracts/IReleasable.cs create mode 100644 src/Runtime/Ghost.Engine/AssetManager.cs create mode 100644 src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs diff --git a/src/Editor/Ghost.DSL/Ghost.DSL.csproj b/src/Editor/Ghost.DSL/Ghost.DSL.csproj index 89bdeed..f06620a 100644 --- a/src/Editor/Ghost.DSL/Ghost.DSL.csproj +++ b/src/Editor/Ghost.DSL/Ghost.DSL.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/Asset.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/Asset.cs index 623ff61..f5d4917 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/Asset.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/Asset.cs @@ -1,6 +1,4 @@ using Ghost.Editor.Core.Contracts; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Ghost.Editor.Core.AssetHandler; @@ -16,21 +14,9 @@ public abstract class Asset get; } - public Guid[] Dependencies - { - get; - } - - public IAssetSettings? Settings - { - get; - } - - protected Asset(Guid id, Guid[] dependencies, IAssetSettings? settings) + protected Asset(Guid id) { ID = id; - Dependencies = dependencies; - Settings = settings; } public virtual ValueTask RefreshAsync(IAssetRegistry db, CancellationToken token = default) @@ -39,100 +25,6 @@ public abstract class Asset } } -// Do not change the order of the fields in this struct, as it is used for binary serialization/deserialization. -[StructLayout(LayoutKind.Sequential, Size = SIZE)] -internal struct AssetMetadata -{ - public const int CURRENT_FORMAT_VERSION = 1; - public const int SIZE = 128; // Fixed size for metadata header. We choose 128 bytes to allow future expansion without breaking compatibility. - - public AssetMetadata(Guid id, Guid typeID) - { - FormatVersion = CURRENT_FORMAT_VERSION; - ID = id; - TypeID = typeID; - } - - public int FormatVersion - { - get; - } - - public Guid ID - { - get; - } - - public Guid TypeID - { - get; - } - - public int HandlerVersion - { - get; set; - } - - public int DependencyCount - { - get; set; - } - - public long DependenciesOffset - { - get; set; - } - - public long SettingsOffset - { - get; set; - } - - public long SettingsSize - { - get; set; - } - - public long ContentOffset - { - get; set; - } - - public long ContentSize - { - get; set; - } - - public static void WriteToStream(Stream stream, scoped ref readonly AssetMetadata metadata) - { - var buffer = MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in metadata, 1)); - stream.Write(buffer); - } - - public static AssetMetadata ReadFromStream(Stream stream) - { - Span buffer = stackalloc byte[SIZE]; - stream.ReadExactly(buffer); - return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(buffer)); - } -} - -[StructLayout(LayoutKind.Sequential, Size = SIZE)] -public readonly struct DependencyInfo -{ - public const int SIZE = 16; - - public Guid ID - { - get; init; - } - - public readonly ReadOnlySpan AsBytes() - { - return MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1)); - } -} - public readonly struct AssetReference : IEquatable { private readonly int _value; diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs index 4885429..4344af6 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandler.cs @@ -6,34 +6,27 @@ namespace Ghost.Editor.Core.AssetHandler; [AttributeUsage(AttributeTargets.Class)] public sealed class CustomAssetHandlerAttribute : Attribute { - public required string ID + public CustomAssetHandlerAttribute(string id, string[] supportedExtensions, int version = 1) { - get; init; } - - public required string[] SupportedExtensions - { - get; init; - } - - public bool AllowCaching - { - get; init; - } = true; } public interface IAssetExportOptions; public interface IAssetHandler { - ValueTask> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default); + ValueTask> LoadAsync(Stream sourceStream, Guid id, IAssetRegistry assetRegistry, CancellationToken token = default); ValueTask SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default); } public interface IImportableAssetHandler : IAssetHandler { - IAssetSettings? CreateDefaultSettings() => null; + IAssetSettings? CreateDefaultSettings(); ValueTask ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default); +} + +public interface IExportableAssetHandler : IAssetHandler +{ ValueTask ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default); } @@ -46,16 +39,16 @@ public static class AssetHandlerExtensions return await handler.ImportAsync(sourceStream, targetStream, id, settings, token); } - public static async ValueTask ExportAsync(this IImportableAssetHandler handler, string assetFilePath, string targetFilePath, IAssetExportOptions? options, CancellationToken token = default) + public static async ValueTask ExportAsync(this IExportableAssetHandler handler, string assetFilePath, string targetFilePath, IAssetExportOptions? options, CancellationToken token = default) { await using var assetStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); await using var targetStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None); return await handler.ExportAsync(assetStream, targetStream, options, token); } - public static async ValueTask> LoadAsync(this IAssetHandler handler, string assetFilePath, IAssetRegistry assetDatabase, CancellationToken token = default) + public static async ValueTask> LoadAsync(this IAssetHandler handler, string assetFilePath, Guid id, IAssetRegistry assetDatabase, CancellationToken token = default) { await using var sourceStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); - return await handler.LoadAsync(sourceStream, assetDatabase, token); + return await handler.LoadAsync(sourceStream, id, assetDatabase, token); } } diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs index ef62488..c347295 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetHandlerRegistry.cs @@ -1,13 +1,10 @@ -using System.Reflection; -using Ghost.Editor.Core.Utilities; - namespace Ghost.Editor.Core.AssetHandler; /// /// One-time scan at editor startup → two dictionaries. /// All lookups are O(1) after construction. /// -internal sealed class AssetHandlerRegistry +public sealed class AssetHandlerRegistry { private readonly Dictionary _byExtension; private readonly Dictionary _byTypeId; @@ -18,49 +15,17 @@ internal sealed class AssetHandlerRegistry _byExtension = new Dictionary(StringComparer.OrdinalIgnoreCase); _byTypeId = new Dictionary(); _versionByTypeId = new Dictionary(); + } - foreach (var typeInfo in TypeCache.GetTypes()) + public void RegisterHandler(IAssetHandler handler, ReadOnlySpan extensions, Guid typeId, int version) + { + _byTypeId[typeId] = handler; + _versionByTypeId[typeId] = version; + + foreach (var ext in extensions) { - if (typeInfo.IsAbstract || typeInfo.IsInterface) - { - continue; - } - - if (!typeof(IAssetHandler).IsAssignableFrom(typeInfo)) - { - continue; - } - - var attr = typeInfo.GetCustomAttribute(); - if (attr == null) - { - continue; - } - - if (!Guid.TryParse(attr.ID, out var typeId)) - { - continue; - } - - try - { - if (Activator.CreateInstance(typeInfo) is IAssetHandler handler) - { - _byTypeId[typeId] = handler; - // Note: Versioning could be expanded, but for now we assume version 1 or look for a constant - _versionByTypeId[typeId] = 1; - - foreach (var ext in attr.SupportedExtensions) - { - var normalizedExt = ext.StartsWith('.') ? ext : "." + ext; - _byExtension[normalizedExt] = handler; - } - } - } - catch - { - // Log failure to instantiate handler in real app - } + var normalizedExt = ext.StartsWith('.') ? ext : "." + ext; + _byExtension[normalizedExt] = handler; } } diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs index ad8107a..ae2f003 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/AssetMeta.cs @@ -67,6 +67,8 @@ public sealed class AssetMeta internal static class AssetMetaIO { + private const string _META_EXTENSION = ".gmeta"; + private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true, @@ -108,7 +110,7 @@ internal static class AssetMetaIO File.Move(tempPath, metaPath); } - public static string GetMetaPath(string sourceFilePath) => sourceFilePath + ".gmeta"; + public static string GetMetaPath(string sourceFilePath) => sourceFilePath + _META_EXTENSION; - public static string GetSourcePath(string metaPath) => metaPath[..^".gmeta".Length]; + public static string GetSourcePath(string metaPath) => metaPath[..^_META_EXTENSION.Length]; } diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs index e7aefd6..d461289 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureAsset.cs @@ -2,10 +2,7 @@ using Ghost.Core; using Ghost.Editor.Core.Contracts; using Ghost.Graphics.RHI; using ImageMagick; -using System.Buffers; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using static Ghost.Editor.Core.AssetHandler.TextureAssetSettings; namespace Ghost.Editor.Core.AssetHandler; @@ -54,15 +51,47 @@ public class TextureAsset : Asset internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F"; internal static readonly Guid s_typeGuid = Guid.Parse(_TYPE_ID); - private readonly Handle _texture; + private readonly byte[] _textureData; + private readonly uint _width; + private readonly uint _height; + private readonly uint _depth; + private readonly uint _colorComponents; public override Guid TypeID => s_typeGuid; - public Handle Texture => _texture; - public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings, Handle texture) - : base(id, dependencies, settings) + /// + /// Gets the raw texture data in a compressed format. + /// + public ReadOnlyMemory TextureData => _textureData; + + /// + /// Gets the width of the texture in pixels. + /// + public uint Width => _width; + + /// + /// Gets the height of the texture in pixels. + /// + public uint Height => _height; + + /// + /// Gets the bit depth of the texture. + /// + public uint Depth => _depth; + + /// + /// Gets the number of color components in the texture. + /// + public uint ColorComponents => _colorComponents; + + internal TextureAsset(byte[] data, ImageContentHeader header, Guid id) + : base(id) { - _texture = texture; + _textureData = data; + _width = header.width; + _height = header.height; + _depth = header.depth; + _colorComponents = header.colorComponents; } } @@ -213,148 +242,96 @@ public class TextureAssetSettings : IAssetSettings } = new SamplerSettings(); } -[CustomAssetHandler(ID = TextureAsset._TYPE_ID, SupportedExtensions = new[] { ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr" })] +[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility +internal struct ImageContentHeader +{ + public uint width; + public uint height; + public uint depth; + public uint colorComponents; +} + +[CustomAssetHandler(TextureAsset._TYPE_ID, [ ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr" ], 1)] internal class TextureAssetHandler : IImportableAssetHandler { - private const int _CURRENT_VERSION = 1; - - private struct ImageContentHeader + public IAssetSettings? CreateDefaultSettings() { - public uint width; - public uint height; - public uint depth; - public uint colorComponents; + return new TextureAssetSettings(); } - private static async ValueTask> WriteSettingsToStreamAsync(TextureAssetSettings settings, Stream stream, CancellationToken token = default) + public async ValueTask> LoadAsync(Stream sourceStream, Guid id, IAssetRegistry assetRegistry, CancellationToken token = default) { - var size = Unsafe.SizeOf() + Unsafe.SizeOf() + Unsafe.SizeOf(); - var tempArray = ArrayPool.Shared.Rent(size); - try { - ref var address = ref MemoryMarshal.GetReference(tempArray); - Unsafe.WriteUnaligned(ref address, settings.Basic); - Unsafe.WriteUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf()), settings.Advanced); - Unsafe.WriteUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf() + Unsafe.SizeOf()), settings.Sampler); + // FIX: Should the sourceStream be the stream of the imported file or the raw asset file? + // Or should we change our paramemters to inlcude more information and let each handler decide how to load the asset? + // The problem of a single sourceStream is, for example, for texture assets, we don't even need to read the ".png" file at all, + // but for some other asset types, we may don't even have imported intermediate files at all. - await stream.WriteAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false); + // var path = assetRegistry.GetAssetPath(id); + // if (string.IsNullOrEmpty(path)) + // { + // return Result.Failure("Asset path not found in registry."); + // } + // + // var metadataPath = AssetMetaIO.GetMetaPath(path); + // var meta = await AssetMetaIO.ReadAsync(metadataPath, token).ConfigureAwait(false); + // Logger.DebugAssert(meta != null, $"Missing or invalid metadata for asset at {path}"); - return Result.Success(size); + + + var header = new ImageContentHeader(); + sourceStream.ReadExactly(MemoryMarshal.AsBytes(new Span(ref header))); + + var imageDataSize = (int)(sourceStream.Length - sourceStream.Position); + var imageData = new byte[imageDataSize]; + await sourceStream.ReadExactlyAsync(imageData, token).ConfigureAwait(false); + + var asset = new TextureAsset(imageData, header, id); + return asset; } catch (Exception ex) { - return Result.Failure($"Failed to write texture asset settings to stream: {ex.Message}"); + return Result.Failure($"Failed to load texture asset: {ex.Message}"); } - finally - { - ArrayPool.Shared.Return(tempArray); - } - } - - private static async ValueTask> ReadSettingsFromStreamAsync(Stream stream, CancellationToken token = default) - { - var size = Unsafe.SizeOf() + Unsafe.SizeOf() + Unsafe.SizeOf(); - var tempArray = ArrayPool.Shared.Rent(size); - - try - { - await stream.ReadAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false); - - // Use index-based reads after the await to avoid 'ref across await' errors. - var basic = Unsafe.ReadUnaligned(ref tempArray[0]); - var advanced = Unsafe.ReadUnaligned(ref tempArray[Unsafe.SizeOf()]); - var sampler = Unsafe.ReadUnaligned(ref tempArray[Unsafe.SizeOf() + Unsafe.SizeOf()]); - - var settings = new TextureAssetSettings - { - Basic = basic, - Advanced = advanced, - Sampler = sampler - }; - - return Result.Success(settings); - } - catch (Exception ex) - { - return Result.Failure($"Failed to read texture asset settings from stream: {ex.Message}"); - } - finally - { - ArrayPool.Shared.Return(tempArray); - } - } - - public ValueTask ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default) - { - throw new NotImplementedException(); - } - - public async ValueTask ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) - { - var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); - using var image = new MagickImage(sourceStream); - var bytes = image.ToByteArray(); - - await TextureProcessor.CompressToCacheAsync(EditorApplication.LibraryFolderPath, id, bytes, image.Width, image.Height, image.Depth, textureSettings, token).ConfigureAwait(false); - - var header = new AssetMetadata(id, TextureAsset.s_typeGuid) - { - HandlerVersion = _CURRENT_VERSION, - SettingsOffset = AssetMetadata.SIZE, - }; - - targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin); - var sizeResult = await WriteSettingsToStreamAsync(textureSettings, targetStream, token).ConfigureAwait(false); - if (sizeResult.IsFailure) - { - return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}"); - } - - // Content layout (all little-endian): - // uint32 width - // uint32 height - // uint32 depth - // uint32 colorComponents - // byte[] pixelBytes - - header.SettingsSize = sizeResult.Value; - header.ContentOffset = header.SettingsOffset + sizeResult.Value; - unsafe - { - header.ContentSize = sizeof(ImageContentHeader) + image.Width * image.Height * (image.Depth / 8) * image.ChannelCount; - } - - // Write raw image content - targetStream.Seek(header.ContentOffset, SeekOrigin.Begin); - - var contentHeader = new ImageContentHeader - { - width = image.Width, - height = image.Height, - depth = image.Depth, - colorComponents = image.ChannelCount - }; - - targetStream.Write(MemoryMarshal.AsBytes(new Span(ref contentHeader))); - - await targetStream.WriteAsync(bytes, token).ConfigureAwait(false); - await targetStream.FlushAsync(token).ConfigureAwait(false); - - // Patch header now that all sizes are known - targetStream.Seek(0, SeekOrigin.Begin); - AssetMetadata.WriteToStream(targetStream, ref header); - - return Result.Success(); - } - - public ValueTask> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default) - { - throw new NotImplementedException(); } public ValueTask SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default) { throw new NotImplementedException(); } + + public async ValueTask ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default) + { + try + { + var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings(); + using var image = new MagickImage(sourceStream); + var bytes = image.ToByteArray(); + + await TextureProcessor.CompressToCacheAsync(EditorApplication.LibraryFolderPath, id, bytes, image.Width, image.Height, image.Depth, textureSettings, token) + .ConfigureAwait(false); + + targetStream.Seek(0, SeekOrigin.Begin); + + var contentHeader = new ImageContentHeader + { + width = image.Width, + height = image.Height, + depth = image.Depth, + colorComponents = image.ChannelCount + }; + + targetStream.Write(MemoryMarshal.AsBytes(new Span(ref contentHeader))); + + await targetStream.WriteAsync(bytes, token).ConfigureAwait(false); + await targetStream.FlushAsync(token).ConfigureAwait(false); + + return Result.Success(); + } + catch (Exception ex) + { + return Result.Failure($"Failed to import texture asset: {ex.Message}"); + } + } } diff --git a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs index 721be18..8e336dd 100644 --- a/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs +++ b/src/Editor/Ghost.Editor.Core/AssetHandler/TextureProcessor.cs @@ -186,7 +186,7 @@ internal static class TextureProcessor var workItem = new NvttPipelineTask(cachePath, image, width, height, depth, settings); ThreadPool.UnsafeQueueUserWorkItem(workItem, true); await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - + return cachePath; } diff --git a/src/Editor/Ghost.Editor.Core/Attributes.cs b/src/Editor/Ghost.Editor.Core/Attributes.cs index 34b940b..17fdaff 100644 --- a/src/Editor/Ghost.Editor.Core/Attributes.cs +++ b/src/Editor/Ghost.Editor.Core/Attributes.cs @@ -57,8 +57,20 @@ public class EditorInjectionAttribute : DiscoverableAttributeBase Transient, } + public ServiceLifetime Lifetime + { + get; + } + + public Type ImplementationType + { + get; + } + public EditorInjectionAttribute(ServiceLifetime lifetime, Type implementationType) { + Lifetime = lifetime; + ImplementationType = implementationType; } } @@ -86,4 +98,4 @@ public sealed class ContextMenuItemAttribute : DiscoverableAttributeBase Name = name; Group = group; } -} \ No newline at end of file +} diff --git a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj index 9183680..addfc6e 100644 --- a/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj +++ b/src/Editor/Ghost.Editor.Core/Ghost.Editor.Core.csproj @@ -15,7 +15,7 @@ - + @@ -25,6 +25,7 @@ + @@ -40,4 +41,4 @@ Designer - + \ No newline at end of file diff --git a/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs b/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs index a89baee..88b0db3 100644 --- a/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs +++ b/src/Editor/Ghost.Editor.Core/Services/AssetCatalog.cs @@ -224,6 +224,19 @@ internal sealed class AssetCatalog : IDisposable return list; } + public List GetDependencies(Guid guid) + { + _cmdGetDependencies.Parameters.Clear(); + _cmdGetDependencies.Parameters.AddWithValue("@guid", guid.ToByteArray()); + using var reader = _cmdGetDependencies.ExecuteReader(); + var list = new List(); + while (reader.Read()) + { + list.Add(new Guid((byte[])reader[0])); + } + return list; + } + public List<(Guid guid, string sourcePath)> GetDirtyAssets() { using var reader = _cmdGetDirty.ExecuteReader(); diff --git a/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs b/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs index 0248dec..55d8902 100644 --- a/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs +++ b/src/Editor/Ghost.Editor.Core/Services/AssetRegistry.cs @@ -165,26 +165,32 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable return; } - var handlerTypeId = handler?.GetType().GetCustomAttribute()?.ID; + var handlerTypeId = handler?.GetType().GetCustomAttributesData().FirstOrDefault(d => d.AttributeType == typeof(CustomAssetHandlerAttribute))?.ConstructorArguments[0].Value; var meta = new AssetMeta { Guid = Guid.NewGuid(), - HandlerTypeId = handlerTypeId == null ? null : Guid.Parse(handlerTypeId), + HandlerTypeId = handlerTypeId is string str? Guid.Parse(str) : null, HandlerVersion = 1, Settings = importable?.CreateDefaultSettings() }; _ignoreMetaWrites[metaPath] = true; await AssetMetaIO.WriteAsync(metaPath, meta); - + _catalog.Upsert(meta, relativePath); await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, relativePath, metaPath, ImportReason.NewAsset)); } - public string? GetAssetPath(Guid id) => _catalog.GetSourcePath(id); + public string? GetAssetPath(Guid id) + { + return _catalog.GetSourcePath(id); + } - public Guid GetAssetGuid(string path) => _catalog.GetGuid(path.Replace('\\', '/')); + public Guid GetAssetGuid(string path) + { + return _catalog.GetGuid(path.Replace(Path.DirectorySeparatorChar, '/')); + } public async ValueTask> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default) { @@ -192,7 +198,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable // Current requirement: "returns the new GUID immediately (import happens in background)" var ext = Path.GetExtension(sourceFilePath); - var relativePath = targetAssetPath.Replace('\\', '/'); + var relativePath = targetAssetPath.Replace(Path.DirectorySeparatorChar, '/'); var fullPath = Path.Combine(_assetsRoot, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); @@ -224,7 +230,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable { if (_loadedAssets.TryGetValue(id, out var weakRef) && weakRef.TryGetTarget(out var asset)) { - return Result.Success(asset); + return asset; } await _loadLock.WaitAsync(token); @@ -232,7 +238,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable { if (_loadedAssets.TryGetValue(id, out weakRef) && weakRef.TryGetTarget(out asset)) { - return Result.Success(asset); + return asset; } var importedPath = Path.Combine(_libraryRoot, "Imports", $"{id:N}.imported"); @@ -257,7 +263,10 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable } } - public ValueTask SaveAssetAsync(Asset asset, CancellationToken token = default) => throw new NotImplementedException(); + public ValueTask SaveAssetAsync(Asset asset, CancellationToken token = default) + { + throw new NotImplementedException(); + } public void Dispose() { diff --git a/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs b/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs index 67ce375..8e4e832 100644 --- a/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs +++ b/src/Editor/Ghost.Editor.Core/Services/ImportCoordinator.cs @@ -97,7 +97,7 @@ internal sealed class ImportCoordinator : IDisposable return; } - var handler = (meta.HandlerTypeId.HasValue) + var handler = meta.HandlerTypeId.HasValue ? _handlers.GetByTypeId(meta.HandlerTypeId.Value) : _handlers.GetByExtension(Path.GetExtension(job.SourcePath)); diff --git a/src/Editor/Ghost.Editor.Core/Utilities/TypeCache.cs b/src/Editor/Ghost.Editor.Core/Utilities/TypeCache.cs index d0df615..38bbbf5 100644 --- a/src/Editor/Ghost.Editor.Core/Utilities/TypeCache.cs +++ b/src/Editor/Ghost.Editor.Core/Utilities/TypeCache.cs @@ -6,13 +6,13 @@ namespace Ghost.Editor.Core.Utilities; public static class TypeCache { - private static TypeInfo[] s_types; - private static Dictionary> s_attributeMethodCache; + private static TypeInfo[] s_types = null!; + private static Dictionary> s_attributeMethodCache = null!; + private static Dictionary> s_attributeTypeCache = null!; static TypeCache() { - s_types = LoadTypes(); - s_attributeMethodCache = FindMethodWithAttribute(); + Reload(); } private static TypeInfo[] LoadTypes() @@ -62,6 +62,29 @@ public static class TypeCache return dict; } + private static Dictionary> FindTypesWithAttribute() + { + var dict = new Dictionary>(); + for (int i = 0; i < s_types.Length; i++) + { + TypeInfo? type = s_types[i]; + var attrs = type.GetCustomAttributes(false); + foreach (var attr in attrs) + { + var key = attr.GetType().TypeHandle.Value; + ref var typeList = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out var exist); + if (!exist) + { + typeList = new List(); + } + + typeList!.Add(i); + } + } + + return dict; + } + internal static void Initialize() { // Intentionally left blank. @@ -72,6 +95,7 @@ public static class TypeCache { s_types = LoadTypes(); s_attributeMethodCache = FindMethodWithAttribute(); + s_attributeTypeCache = FindTypesWithAttribute(); } public static IReadOnlyCollection GetTypes() @@ -79,7 +103,7 @@ public static class TypeCache return s_types; } - public static IReadOnlyCollection? GetMethodsWithAttribute() + public static IEnumerable? GetMethodsWithAttribute() where T : DiscoverableAttributeBase { var key = typeof(T).TypeHandle.Value; @@ -90,4 +114,16 @@ public static class TypeCache return null; } -} \ No newline at end of file + + public static IEnumerable? GetTypesWithAttribute() + where T : DiscoverableAttributeBase + { + var key = typeof(T).TypeHandle.Value; + if (s_attributeTypeCache.TryGetValue(key, out var typeIndices)) + { + return typeIndices.Select(i => s_types[i]); + } + + return null; + } +} diff --git a/src/Editor/Ghost.Editor/App.xaml.cs b/src/Editor/Ghost.Editor/App.xaml.cs index 52ca88c..c377cb3 100644 --- a/src/Editor/Ghost.Editor/App.xaml.cs +++ b/src/Editor/Ghost.Editor/App.xaml.cs @@ -72,6 +72,7 @@ public partial class App : Application services.AddTransient(); + // TODO: Use source generators to generate this code at compile time instead of using reflection at runtime. foreach (var type in TypeCache.GetTypes()) { var data = type.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType == typeof(EditorInjectionAttribute)); diff --git a/src/Editor/Ghost.Editor/Ghost.Editor.csproj b/src/Editor/Ghost.Editor/Ghost.Editor.csproj index 5b50c39..b81341c 100644 --- a/src/Editor/Ghost.Editor/Ghost.Editor.csproj +++ b/src/Editor/Ghost.Editor/Ghost.Editor.csproj @@ -39,7 +39,7 @@ - + diff --git a/src/Runtime/Ghost.Core/Contracts/ICloneable.cs b/src/Runtime/Ghost.Core/Contracts/ICloneable.cs deleted file mode 100644 index 4e1740e..0000000 --- a/src/Runtime/Ghost.Core/Contracts/ICloneable.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Ghost.Core.Contracts; - -public interface ICloneable -{ - object Clone(); -} - -public interface ICloneable -{ - T Clone(); -} \ No newline at end of file diff --git a/src/Runtime/Ghost.Core/Contracts/IReleasable.cs b/src/Runtime/Ghost.Core/Contracts/IReleasable.cs deleted file mode 100644 index 61a5f53..0000000 --- a/src/Runtime/Ghost.Core/Contracts/IReleasable.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ghost.Core.Contracts; - -internal interface IReleasable -{ - void InternalRelease(); -} \ No newline at end of file diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj index 0d8d178..fc32708 100644 --- a/src/Runtime/Ghost.Core/Ghost.Core.csproj +++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj @@ -26,7 +26,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Runtime/Ghost.Core/Handle.cs b/src/Runtime/Ghost.Core/Handle.cs index cc9a6f3..c6b2027 100644 --- a/src/Runtime/Ghost.Core/Handle.cs +++ b/src/Runtime/Ghost.Core/Handle.cs @@ -1,3 +1,5 @@ +using System.Runtime.InteropServices; + namespace Ghost.Core; public readonly struct Handle : IEquatable> @@ -245,4 +247,49 @@ public readonly struct Key128 : IEquatable> public static implicit operator UInt128(Key128 key) => key.Value; public static implicit operator Key128(UInt128 value) => new Key128(value); -} \ No newline at end of file +} + +[StructLayout(LayoutKind.Sequential)] +public readonly struct AssetRef : IEquatable> +{ + public readonly Guid guid; + + public static AssetRef Null => default; + + public bool IsValid => guid != Guid.Empty; + + public AssetRef(Guid guid) + { + this.guid = guid; + } + + public bool Equals(AssetRef other) + { + return guid == other.guid; + } + + public override int GetHashCode() + { + return guid.GetHashCode(); + } + + public override bool Equals(object? obj) + { + return obj is AssetRef r && Equals(r); + } + + public override string ToString() + { + return $"AssetRef<{typeof(T).Name}>({guid:N})"; + } + + public static bool operator ==(AssetRef a, AssetRef b) + { + return a.Equals(b); + } + + public static bool operator !=(AssetRef a, AssetRef b) + { + return !a.Equals(b); + } +} diff --git a/src/Runtime/Ghost.Engine/AssetManager.cs b/src/Runtime/Ghost.Engine/AssetManager.cs new file mode 100644 index 0000000..f3e476e --- /dev/null +++ b/src/Runtime/Ghost.Engine/AssetManager.cs @@ -0,0 +1,44 @@ +using Ghost.Core; +using System.Runtime.InteropServices; + +namespace Ghost.Engine; + +internal abstract class RuntimeAsset; + +internal interface IRuntimeAssetLoader +{ + ValueTask> LoadAsync(Stream cookedData, Guid id, CancellationToken token = default); +} + +internal sealed class RuntimeLoaderRegistry +{ + private readonly Dictionary _loaders = new(); + public void Register(Guid cookedTypeId, IRuntimeAssetLoader loader) + { + _loaders[cookedTypeId] = loader; + } + public IRuntimeAssetLoader? GetLoader(Guid cookedTypeId) + { + _loaders.TryGetValue(cookedTypeId, out var loader); + return loader; + } +} + +internal sealed class CookedTextureLoader : IRuntimeAssetLoader +{ + public static readonly Guid TYPE_ID = TextureAsset.s_typeGuid; + public async ValueTask> LoadAsync(Stream cookedData, Guid id, CancellationToken token) + { + // Read the ImageContentHeader you wrote during import + var header = new ImageContentHeader(); + cookedData.ReadExactly(MemoryMarshal.AsBytes(new Span(ref header))); + // Read the rest as raw GPU data (DDS/BC compressed bytes) + var data = new byte[cookedData.Length - cookedData.Position]; + await cookedData.ReadExactlyAsync(data, token); + return new TextureAsset(data, header, id); + } +} + +public class AssetManager +{ +} diff --git a/src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs b/src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs new file mode 100644 index 0000000..e0c301b --- /dev/null +++ b/src/Runtime/Ghost.Generator/AssetHandlerRegistrationGenerator.cs @@ -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 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 = $@"// + +[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; + } +} diff --git a/src/Runtime/Ghost.Generator/ComponentRegistrationGenerator.cs b/src/Runtime/Ghost.Generator/ComponentRegistrationGenerator.cs index f651849..288cda0 100644 --- a/src/Runtime/Ghost.Generator/ComponentRegistrationGenerator.cs +++ b/src/Runtime/Ghost.Generator/ComponentRegistrationGenerator.cs @@ -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 = $@"// + var code = $@"// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] internal static partial class {typeName} diff --git a/src/Runtime/Ghost.Generator/Ghost.Generator.csproj b/src/Runtime/Ghost.Generator/Ghost.Generator.csproj index 16c9f5f..1813c5d 100644 --- a/src/Runtime/Ghost.Generator/Ghost.Generator.csproj +++ b/src/Runtime/Ghost.Generator/Ghost.Generator.csproj @@ -13,11 +13,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Runtime/Ghost.Graphics.D3D12/Ghost.Graphics.D3D12.csproj b/src/Runtime/Ghost.Graphics.D3D12/Ghost.Graphics.D3D12.csproj index 6706756..9d7316f 100644 --- a/src/Runtime/Ghost.Graphics.D3D12/Ghost.Graphics.D3D12.csproj +++ b/src/Runtime/Ghost.Graphics.D3D12/Ghost.Graphics.D3D12.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs index d8bec56..143df69 100644 --- a/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs +++ b/src/Runtime/Ghost.Graphics/RenderGraphModule/RenderGraphExecutor.cs @@ -1,7 +1,6 @@ using Ghost.Core; using Ghost.Graphics.RHI; using Ghost.Graphics.Services; -using System.Diagnostics; namespace Ghost.Graphics.RenderGraphModule; diff --git a/src/Test/Ghost.Graphics.Test/Ghost.Graphics.Test.csproj b/src/Test/Ghost.Graphics.Test/Ghost.Graphics.Test.csproj index c5b7443..eefd10e 100644 --- a/src/Test/Ghost.Graphics.Test/Ghost.Graphics.Test.csproj +++ b/src/Test/Ghost.Graphics.Test/Ghost.Graphics.Test.csproj @@ -42,11 +42,11 @@ - - - - - + + + + + diff --git a/src/Test/Ghost.Graphics.Test/Systems/RenderExtractionSystem.cs b/src/Test/Ghost.Graphics.Test/Systems/RenderExtractionSystem.cs index bd02440..56ae0d7 100644 --- a/src/Test/Ghost.Graphics.Test/Systems/RenderExtractionSystem.cs +++ b/src/Test/Ghost.Graphics.Test/Systems/RenderExtractionSystem.cs @@ -5,13 +5,12 @@ using Ghost.Engine.Components; using Ghost.Engine.Systems; using Ghost.Entities; using Ghost.Graphics.Core; -using Ghost.Graphics.Test.RenderPipeline; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.Mathematics; namespace Ghost.Graphics.Test.Systems; -[RenderPipelineSystem] +//[RenderPipelineSystem] [UpdateAfter] public class RenderExtractionSystem : ISystem { diff --git a/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj b/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj index 9b2af54..bf5f2ed 100644 --- a/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj +++ b/src/Test/Ghost.UnitTest/Ghost.UnitTest.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj b/src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj index 5da3cf6..3c5b9b6 100644 --- a/src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj +++ b/src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj @@ -8,7 +8,7 @@ - +