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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Engine.AssetLoader;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandler;
|
||||
|
||||
|
||||
@@ -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."));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user