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:
@@ -8,7 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1" />
|
||||
<PackageReference Include="Antlr4BuildTasks" Version="12.11.0" />
|
||||
<PackageReference Include="Antlr4BuildTasks" Version="12.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<byte> buffer = stackalloc byte[SIZE];
|
||||
stream.ReadExactly(buffer);
|
||||
return Unsafe.ReadUnaligned<AssetMetadata>(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<byte> AsBytes()
|
||||
{
|
||||
return MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1));
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct AssetReference : IEquatable<AssetReference>
|
||||
{
|
||||
private readonly int _value;
|
||||
|
||||
@@ -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<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default);
|
||||
ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, Guid id, IAssetRegistry assetRegistry, CancellationToken token = default);
|
||||
ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default);
|
||||
}
|
||||
|
||||
public interface IImportableAssetHandler : IAssetHandler
|
||||
{
|
||||
IAssetSettings? CreateDefaultSettings() => null;
|
||||
IAssetSettings? CreateDefaultSettings();
|
||||
ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default);
|
||||
}
|
||||
|
||||
public interface IExportableAssetHandler : IAssetHandler
|
||||
{
|
||||
ValueTask<Result> 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<Result> ExportAsync(this IImportableAssetHandler handler, string assetFilePath, string targetFilePath, IAssetExportOptions? options, CancellationToken token = default)
|
||||
public static async ValueTask<Result> 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<Result<Asset>> LoadAsync(this IAssetHandler handler, string assetFilePath, IAssetRegistry assetDatabase, CancellationToken token = default)
|
||||
public static async ValueTask<Result<Asset>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using System.Reflection;
|
||||
using Ghost.Editor.Core.Utilities;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
/// <summary>
|
||||
/// One-time scan at editor startup → two dictionaries.
|
||||
/// All lookups are O(1) after construction.
|
||||
/// </summary>
|
||||
internal sealed class AssetHandlerRegistry
|
||||
public sealed class AssetHandlerRegistry
|
||||
{
|
||||
private readonly Dictionary<string, IAssetHandler> _byExtension;
|
||||
private readonly Dictionary<Guid, IAssetHandler> _byTypeId;
|
||||
@@ -18,49 +15,17 @@ internal sealed class AssetHandlerRegistry
|
||||
_byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase);
|
||||
_byTypeId = new Dictionary<Guid, IAssetHandler>();
|
||||
_versionByTypeId = new Dictionary<Guid, int>();
|
||||
}
|
||||
|
||||
foreach (var typeInfo in TypeCache.GetTypes())
|
||||
public void RegisterHandler(IAssetHandler handler, ReadOnlySpan<string> 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<CustomAssetHandlerAttribute>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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<GPUTexture> _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<GPUTexture> Texture => _texture;
|
||||
|
||||
public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings, Handle<GPUTexture> texture)
|
||||
: base(id, dependencies, settings)
|
||||
/// <summary>
|
||||
/// Gets the raw texture data in a compressed format.
|
||||
/// </summary>
|
||||
public ReadOnlyMemory<byte> TextureData => _textureData;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the texture in pixels.
|
||||
/// </summary>
|
||||
public uint Width => _width;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the texture in pixels.
|
||||
/// </summary>
|
||||
public uint Height => _height;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit depth of the texture.
|
||||
/// </summary>
|
||||
public uint Depth => _depth;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of color components in the texture.
|
||||
/// </summary>
|
||||
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<Result<long>> WriteSettingsToStreamAsync(TextureAssetSettings settings, Stream stream, CancellationToken token = default)
|
||||
public async ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, Guid id, IAssetRegistry assetRegistry, CancellationToken token = default)
|
||||
{
|
||||
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>();
|
||||
var tempArray = ArrayPool<byte>.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<BasicSettings>()), settings.Advanced);
|
||||
Unsafe.WriteUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()), 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<long>(size);
|
||||
|
||||
|
||||
var header = new ImageContentHeader();
|
||||
sourceStream.ReadExactly(MemoryMarshal.AsBytes(new Span<ImageContentHeader>(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<byte>.Shared.Return(tempArray);
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask<Result<IAssetSettings>> ReadSettingsFromStreamAsync(Stream stream, CancellationToken token = default)
|
||||
{
|
||||
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>();
|
||||
var tempArray = ArrayPool<byte>.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<BasicSettings>(ref tempArray[0]);
|
||||
var advanced = Unsafe.ReadUnaligned<AdvancedSettings>(ref tempArray[Unsafe.SizeOf<BasicSettings>()]);
|
||||
var sampler = Unsafe.ReadUnaligned<SamplerSettings>(ref tempArray[Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()]);
|
||||
|
||||
var settings = new TextureAssetSettings
|
||||
{
|
||||
Basic = basic,
|
||||
Advanced = advanced,
|
||||
Sampler = sampler
|
||||
};
|
||||
|
||||
return Result.Success<IAssetSettings>(settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to read texture asset settings from stream: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(tempArray);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async ValueTask<Result> 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<ImageContentHeader>(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<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetRegistry, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetRegistry, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async ValueTask<Result> 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<ImageContentHeader>(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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Magick.NET-Q16-HDRI-OpenMP-x64" Version="14.12.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.6" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
@@ -25,6 +25,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
|
||||
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
|
||||
<ProjectReference Include="..\..\Runtime\Ghost.Generator\Ghost.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="..\..\ThridParty\Ghost.DXC\Ghost.DXC.csproj" />
|
||||
<ProjectReference Include="..\..\ThridParty\Ghost.Nvtt\Ghost.Nvtt.csproj" />
|
||||
</ItemGroup>
|
||||
@@ -40,4 +41,4 @@
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -224,6 +224,19 @@ internal sealed class AssetCatalog : IDisposable
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<Guid> GetDependencies(Guid guid)
|
||||
{
|
||||
_cmdGetDependencies.Parameters.Clear();
|
||||
_cmdGetDependencies.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||
using var reader = _cmdGetDependencies.ExecuteReader();
|
||||
var list = new List<Guid>();
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add(new Guid((byte[])reader[0]));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<(Guid guid, string sourcePath)> GetDirtyAssets()
|
||||
{
|
||||
using var reader = _cmdGetDirty.ExecuteReader();
|
||||
|
||||
@@ -165,26 +165,32 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
var handlerTypeId = handler?.GetType().GetCustomAttribute<CustomAssetHandlerAttribute>()?.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<Result<Guid>> 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<Result> SaveAssetAsync(Asset asset, CancellationToken token = default) => throw new NotImplementedException();
|
||||
public ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ namespace Ghost.Editor.Core.Utilities;
|
||||
|
||||
public static class TypeCache
|
||||
{
|
||||
private static TypeInfo[] s_types;
|
||||
private static Dictionary<nint, List<MethodInfo>> s_attributeMethodCache;
|
||||
private static TypeInfo[] s_types = null!;
|
||||
private static Dictionary<nint, List<MethodInfo>> s_attributeMethodCache = null!;
|
||||
private static Dictionary<nint, List<int>> 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<nint, List<int>> FindTypesWithAttribute()
|
||||
{
|
||||
var dict = new Dictionary<nint, List<int>>();
|
||||
for (int i = 0; i < s_types.Length; i++)
|
||||
{
|
||||
TypeInfo? type = s_types[i];
|
||||
var attrs = type.GetCustomAttributes<DiscoverableAttributeBase>(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<int>();
|
||||
}
|
||||
|
||||
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<TypeInfo> GetTypes()
|
||||
@@ -79,7 +103,7 @@ public static class TypeCache
|
||||
return s_types;
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<MethodInfo>? GetMethodsWithAttribute<T>()
|
||||
public static IEnumerable<MethodInfo>? GetMethodsWithAttribute<T>()
|
||||
where T : DiscoverableAttributeBase
|
||||
{
|
||||
var key = typeof(T).TypeHandle.Value;
|
||||
@@ -90,4 +114,16 @@ public static class TypeCache
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<TypeInfo>? GetTypesWithAttribute<T>()
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ public partial class App : Application
|
||||
|
||||
services.AddTransient<ProjectBrowserViewModel>();
|
||||
|
||||
// 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));
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.6" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
|
||||
|
||||
Reference in New Issue
Block a user