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 @@ - +