Refactor asset pipeline: new registry, loader, and runtime

Major overhaul of asset system:
- Split assets into source, .gmeta (JSON), and cooked .imported binaries
- Replaced Asset base class; added TextureAsset, TextureLoader
- AssetManager now uses job-based, dependency-aware loading
- Unified IAssetHandler API; removed legacy handler interfaces
- Updated D3D12 allocator and graphics code for new resource model
- Improved error handling, memory management, and GPU upload logic
- Updated docs and removed obsolete code/interfaces
This commit is contained in:
2026-04-18 01:46:37 +09:00
parent 13bf1501e4
commit abd5ad74d5
32 changed files with 4348 additions and 570 deletions

View File

@@ -1,69 +0,0 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.AssetHandler;
public abstract class Asset
{
public Guid ID
{
get;
}
public abstract Guid TypeID
{
get;
}
protected Asset(Guid id)
{
ID = id;
}
public virtual ValueTask RefreshAsync(IAssetRegistry db, CancellationToken token = default)
{
return ValueTask.CompletedTask;
}
}
public readonly struct AssetReference : IEquatable<AssetReference>
{
private readonly int _value;
/// <summary>
/// The index of the asset in the dependency list.
/// </summary>
public int Index
{
get => Math.Abs(_value) - 1;
}
public static AssetReference Null => default;
public readonly bool IsInternal => _value >= 0;
public readonly bool IsExternal => _value < 0;
public bool Equals(AssetReference other)
{
return _value == other._value;
}
public override int GetHashCode()
{
return _value.GetHashCode();
}
public override bool Equals(object? obj)
{
return obj is AssetReference reference && Equals(reference);
}
public static bool operator ==(AssetReference left, AssetReference right)
{
return left.Equals(right);
}
public static bool operator !=(AssetReference left, AssetReference right)
{
return !(left == right);
}
}

View File

@@ -1,5 +1,4 @@
using Ghost.Core;
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.AssetHandler;
@@ -15,40 +14,9 @@ public interface IAssetExportOptions;
public interface IAssetHandler
{
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);
}
bool CanExport => false;
public interface IImportableAssetHandler : IAssetHandler
{
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);
}
public static class AssetHandlerExtensions
{
public static async ValueTask<Result> ImportAsync(this IImportableAssetHandler handler, string sourceFilePath, string targetFilePath, Guid id, IAssetSettings? settings = null, CancellationToken token = default)
{
await using var sourceStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await using var targetStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
return await handler.ImportAsync(sourceStream, targetStream, id, settings, token);
}
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, 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, id, assetDatabase, token);
}
}
}

View File

@@ -1,4 +1,5 @@
using Ghost.Editor.Core.Contracts;
using Ghost.Engine.AssetLoader;
namespace Ghost.Editor.Core.AssetHandler;

View File

@@ -1,5 +1,5 @@
using Ghost.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Engine.AssetLoader;
using Ghost.Graphics.RHI;
using ImageMagick;
using System.Runtime.InteropServices;
@@ -46,55 +46,6 @@ public enum MipmapFilter : uint
MitchellNetravali
}
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 byte[] _textureData;
private readonly uint _width;
private readonly uint _height;
private readonly uint _depth;
private readonly uint _colorComponents;
public override Guid TypeID => s_typeGuid;
/// <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)
{
_textureData = data;
_width = header.width;
_height = header.height;
_depth = header.depth;
_colorComponents = header.colorComponents;
}
}
public class TextureAssetSettings : IAssetSettings
{
public struct BasicSettings()
@@ -242,65 +193,14 @@ public class TextureAssetSettings : IAssetSettings
} = new SamplerSettings();
}
[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
[CustomAssetHandler(TextureAsset.TYPE_ID, [".png", ".jpg", ".jpeg", ".tga", ".bmp", ".hdr"], 1)]
internal class TextureAssetHandler : IAssetHandler
{
public IAssetSettings? CreateDefaultSettings()
{
return new TextureAssetSettings();
}
public async ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, Guid id, IAssetRegistry assetRegistry, CancellationToken token = default)
{
try
{
// 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.
// 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}");
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 load texture asset: {ex.Message}");
}
}
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
@@ -309,20 +209,22 @@ internal class TextureAssetHandler : IImportableAssetHandler
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)
var (path, mip) = await TextureProcessor.CompressToCacheAsync(EditorApplication.ImportsFolderPath, id, bytes, image.Width, image.Height, image.Depth, textureSettings, token)
.ConfigureAwait(false);
targetStream.Seek(0, SeekOrigin.Begin);
var contentHeader = new ImageContentHeader
var contentHeader = new TextureContentHeader
{
width = image.Width,
height = image.Height,
depth = image.Depth,
colorComponents = image.ChannelCount
colorComponents = image.ChannelCount,
mipLevels = (uint)mip,
dimension = (int)TextureDimension.Texture2D // TODO: Implement dimension calculation
};
targetStream.Write(MemoryMarshal.AsBytes(new Span<ImageContentHeader>(ref contentHeader)));
targetStream.Write(MemoryMarshal.AsBytes(new Span<TextureContentHeader>(ref contentHeader)));
await targetStream.WriteAsync(bytes, token).ConfigureAwait(false);
await targetStream.FlushAsync(token).ConfigureAwait(false);
@@ -334,4 +236,9 @@ internal class TextureAssetHandler : IImportableAssetHandler
return Result.Failure($"Failed to import texture asset: {ex.Message}");
}
}
public ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default)
{
return ValueTask.FromResult(Result.Failure("Exporting texture assets is not supported yet."));
}
}

View File

@@ -33,6 +33,8 @@ internal static class TextureProcessor
private readonly TextureAssetSettings _settings;
private readonly TaskCompletionSource _completionSource;
public int mipmapCount;
public Task Task => _completionSource.Task;
public NvttPipelineTask(string outputPath, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings)
@@ -119,7 +121,6 @@ internal static class TextureProcessor
var nvttFilter = SelectMipmapFilter(_settings.Advanced.MipmapFilter);
int mipmapCount;
if (!_settings.Advanced.GenerateMipmaps)
{
mipmapCount = 1;
@@ -162,23 +163,19 @@ internal static class TextureProcessor
}
}
private const string _TEXTURE_CACHE_SUBFOLDER = "TextureCache";
public static async ValueTask<string> CompressToCacheAsync(string cachesFolderPath, Guid assetId, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
public static async ValueTask<(string cachePath, int mipmapCount)> CompressToCacheAsync(string cachesFolderPath, Guid assetId, byte[] image, uint width, uint height, uint depth, TextureAssetSettings settings, CancellationToken cancellationToken)
{
var cacheDir = Path.Combine(cachesFolderPath, _TEXTURE_CACHE_SUBFOLDER);
Directory.CreateDirectory(cacheDir);
var settingsHash = ComputeSettingsHash(settings);
var cacheFileName = $"{assetId:N}_{settingsHash:X16}.dds";
var cachePath = Path.Combine(cacheDir, cacheFileName);
var cacheFileName = $"texturecache_{assetId:N}_{settingsHash:X16}.dds";
var cachePath = Path.Combine(cachesFolderPath, cacheFileName);
if (File.Exists(cachePath))
{
return cachePath;
// TODO: Implement mipmap count retrieval from existing cache file
return (cachePath, 0);
}
foreach (var stale in Directory.EnumerateFiles(cacheDir, $"{assetId:N}_*.dds"))
foreach (var stale in Directory.EnumerateFiles(cachesFolderPath, $"texturecache_{assetId:N}_*.dds"))
{
File.Delete(stale);
}
@@ -187,7 +184,7 @@ internal static class TextureProcessor
ThreadPool.UnsafeQueueUserWorkItem(workItem, true);
await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
return cachePath;
return (cachePath, workItem.mipmapCount);
}
private static NvttFormat SelectFormat(TextureAssetSettings settings)

View File

@@ -1,6 +1,6 @@
using Ghost.Core;
using Ghost.Editor.Core.AssetHandler;
using Ghost.Editor.Core.Services;
using Ghost.Engine.AssetLoader;
namespace Ghost.Editor.Core.Contracts;

View File

@@ -1,8 +1,8 @@
using System.Collections.Concurrent;
using System.Reflection;
using Ghost.Core;
using Ghost.Editor.Core.AssetHandler;
using Ghost.Editor.Core.Contracts;
using Ghost.Engine.AssetLoader;
using System.Collections.Concurrent;
namespace Ghost.Editor.Core.Services;
@@ -157,7 +157,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
var ext = Path.GetExtension(relativePath);
var handler = _handlerRegistry.GetByExtension(ext);
var importable = handler as IImportableAssetHandler;
var importable = handler as IAssetHandler;
var metaPath = AssetMetaIO.GetMetaPath(fullPath);
if (File.Exists(metaPath))
@@ -169,7 +169,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
var meta = new AssetMeta
{
Guid = Guid.NewGuid(),
HandlerTypeId = handlerTypeId is string str? Guid.Parse(str) : null,
HandlerTypeId = handlerTypeId is string str ? Guid.Parse(str) : null,
HandlerVersion = 1,
Settings = importable?.CreateDefaultSettings()
};

View File

@@ -1,8 +1,8 @@
using System.Threading.Channels;
using Ghost.Core;
using Ghost.Editor.Core.AssetHandler;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Channels;
namespace Ghost.Editor.Core.Services;
@@ -115,7 +115,7 @@ internal sealed class ImportCoordinator : IDisposable
}
var importResult = Result.Success();
if (handler is IImportableAssetHandler importable)
if (handler is IAssetHandler importable)
{
// TODO: This should be handled by EditorApplication.
var importsDir = Path.Combine(_libraryRoot, "Imports");