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:
154
src/Runtime/Ghost.Engine/AssetLoader/Asset.cs
Normal file
154
src/Runtime/Ghost.Engine/AssetLoader/Asset.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Misaki.HighPerformance.LowLevel;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Engine.AssetLoader;
|
||||
|
||||
public abstract class Asset : IResourceReleasable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
public Guid ID
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public abstract AssetType Type
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
protected Asset(Guid id)
|
||||
{
|
||||
ID = id;
|
||||
}
|
||||
|
||||
protected virtual void Release(IResourceDatabase resourceDatabase)
|
||||
{
|
||||
}
|
||||
|
||||
public void ReleaseResource(IResourceDatabase database)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Release(database);
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 64)] // Leave extra space for future expansion without breaking compatibility
|
||||
public struct TextureContentHeader
|
||||
{
|
||||
public uint width;
|
||||
public uint height;
|
||||
public uint depth;
|
||||
public uint mipLevels;
|
||||
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
|
||||
public uint colorComponents;
|
||||
}
|
||||
|
||||
public class TextureAsset : Asset
|
||||
{
|
||||
private MemoryBlock _textureData;
|
||||
private readonly uint _width;
|
||||
private readonly uint _height;
|
||||
private readonly uint _depth;
|
||||
private readonly uint _colorComponents;
|
||||
private readonly uint _mipLevels;
|
||||
private readonly uint _dimension;
|
||||
|
||||
private Handle<GPUTexture> _textureHandle;
|
||||
|
||||
public override AssetType Type => AssetType.Texture;
|
||||
|
||||
public uint Width => _width;
|
||||
public uint Height => _height;
|
||||
public uint Depth => _depth;
|
||||
public uint MipLevels => _mipLevels;
|
||||
public uint Dimension => _dimension;
|
||||
public uint ColorComponents => _colorComponents;
|
||||
|
||||
public Handle<GPUTexture> TextureHandle => _textureHandle;
|
||||
|
||||
internal TextureAsset([OwnershipTransfer] ref MemoryBlock data, TextureContentHeader header, Guid id)
|
||||
: base(id)
|
||||
{
|
||||
_textureData = data;
|
||||
_width = header.width;
|
||||
_height = header.height;
|
||||
_depth = header.depth;
|
||||
_mipLevels = header.mipLevels;
|
||||
_dimension = header.dimension;
|
||||
_colorComponents = header.colorComponents;
|
||||
}
|
||||
|
||||
internal void SetTextureHandle(Handle<GPUTexture> handle, bool disposeCPUData = true)
|
||||
{
|
||||
_textureHandle = handle;
|
||||
if (disposeCPUData)
|
||||
{
|
||||
_textureData.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public ReadOnlySpan<T> GeData<T>()
|
||||
where T : unmanaged
|
||||
{
|
||||
return _textureData.AsSpan<T>();
|
||||
}
|
||||
|
||||
protected override void Release(IResourceDatabase resourceDatabase)
|
||||
{
|
||||
_textureData.Dispose();
|
||||
resourceDatabase.ReleaseResource(_textureHandle.AsResource());
|
||||
}
|
||||
}
|
||||
42
src/Runtime/Ghost.Engine/AssetLoader/TextureLoader.cs
Normal file
42
src/Runtime/Ghost.Engine/AssetLoader/TextureLoader.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Core.Utilities;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Engine.AssetLoader;
|
||||
|
||||
internal sealed class TextureLoader : IRuntimeAssetLoader
|
||||
{
|
||||
public static readonly AssetType AssetType = AssetType.Texture;
|
||||
|
||||
public async ValueTask<Result<Asset>> LoadAsync(Stream cookedData, Guid id, CancellationToken token)
|
||||
{
|
||||
var header = new TextureContentHeader();
|
||||
cookedData.ReadExactly(MemoryMarshal.AsBytes(new Span<TextureContentHeader>(ref header)));
|
||||
|
||||
var alignment = header.depth switch
|
||||
{
|
||||
8 => MemoryUtility.AlignOf<byte>(),
|
||||
16 => MemoryUtility.AlignOf<ushort>(),
|
||||
32 => MemoryUtility.AlignOf<float>(),
|
||||
_ => MemoryUtility.AlignOf<float>()
|
||||
};
|
||||
|
||||
var data = new MemoryBlock((nuint)(cookedData.Length - cookedData.Position), alignment, AllocationHandle.Persistent);
|
||||
|
||||
// C# built-in collections use int for indexing, so we need to ensure that the buffer size does not exceed int.MaxValue
|
||||
var maxBufferSize = (int)Math.Min(0x7effffffu, header.width * header.height * header.depth / 8u * header.colorComponents);
|
||||
var offset = 0u;
|
||||
|
||||
while (offset < data.Size)
|
||||
{
|
||||
using var memoryManager = NativeMemoryManager<byte>.FromMemoryBlock(data, (int)offset, maxBufferSize);
|
||||
|
||||
await cookedData.ReadExactlyAsync(memoryManager.Memory, token);
|
||||
offset += (uint)memoryManager.Memory.Length;
|
||||
}
|
||||
|
||||
return new TextureAsset(ref data, header, id);
|
||||
}
|
||||
}
|
||||
100
src/Runtime/Ghost.Engine/AssetManager.Texture.cs
Normal file
100
src/Runtime/Ghost.Engine/AssetManager.Texture.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Core.Utilities;
|
||||
using Ghost.Engine.AssetLoader;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Ghost.Graphics.Utilities;
|
||||
|
||||
namespace Ghost.Engine;
|
||||
|
||||
public partial class AssetManager
|
||||
{
|
||||
private Handle<GPUTexture> AllocateTextureHandle()
|
||||
{
|
||||
// This will create a new slot in the database, but not allocation any GPU resource.
|
||||
// Everything in the slot will have the same value as the fallback texture, expect the slot will be marked as shared.
|
||||
return _resourceDatabase.CreateShared(_fallbackTexture.AsResource()).AsTexture();
|
||||
}
|
||||
|
||||
private static TextureFormat GetTextureFormat(uint depth, uint colorComponents)
|
||||
{
|
||||
return colorComponents switch
|
||||
{
|
||||
1 => depth switch
|
||||
{
|
||||
8 => TextureFormat.R8_UNorm,
|
||||
16 => TextureFormat.R16_UNorm,
|
||||
32 => TextureFormat.R32_UInt,
|
||||
_ => TextureFormat.Unknown,
|
||||
},
|
||||
2 => depth switch
|
||||
{
|
||||
8 => TextureFormat.R8G8_UNorm,
|
||||
16 => TextureFormat.R16G16_UNorm,
|
||||
32 => TextureFormat.R32G32_Float,
|
||||
_ => TextureFormat.Unknown,
|
||||
},
|
||||
3 or 4 => depth switch
|
||||
{
|
||||
8 => TextureFormat.R8G8B8A8_UNorm,
|
||||
16 => TextureFormat.R16G16B16A16_Float,
|
||||
32 => TextureFormat.R32G32B32A32_Float,
|
||||
_ => TextureFormat.Unknown,
|
||||
},
|
||||
_ => TextureFormat.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
private unsafe Result UploadTexture(AssetEntry entry)
|
||||
{
|
||||
var pData = (byte*)entry.rawData.GetUnsafePtr();
|
||||
var reader = new BufferReader(pData, entry.rawData.Size);
|
||||
|
||||
var header = reader.Read<TextureContentHeader>();
|
||||
|
||||
var textureDesc = new TextureDesc
|
||||
{
|
||||
Width = header.width,
|
||||
Height = header.height,
|
||||
MipLevels = header.mipLevels,
|
||||
Slice = 1,
|
||||
Format = GetTextureFormat(header.depth, header.colorComponents),
|
||||
Dimension = (TextureDimension)header.dimension,
|
||||
Usage = TextureUsage.ShaderResource,
|
||||
};
|
||||
|
||||
var newHandle = RenderingUtility.CreateTexture(
|
||||
_resourceManager,
|
||||
_resourceDatabase,
|
||||
_resourceAllocator,
|
||||
_uploadedBatch.CommandBuffer,
|
||||
reader.Position,
|
||||
in textureDesc);
|
||||
|
||||
if (newHandle.IsInvalid)
|
||||
{
|
||||
return Result.Failure("Failed to create GPU texture.");
|
||||
}
|
||||
|
||||
// FIX: We can not Swap right now, we must wait on the GPU to finish the upload.
|
||||
var oldHandle = entry.GetStorage<Handle<GPUTexture>>();
|
||||
_resourceDatabase.Swap(oldHandle.AsResource(), newHandle.AsResource());
|
||||
// Release the new handle since it now contains the old handle's resource.
|
||||
// Because the old handle is shared, it will only release the slot in the database, not the actuall GPU resource, which is the fallback texture in this case.
|
||||
_resourceDatabase.ReleaseResource(newHandle.AsResource());
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public Handle<GPUTexture> ResolveTexture(Guid assetID)
|
||||
{
|
||||
if (assetID == Guid.Empty)
|
||||
{
|
||||
return _fallbackTexture;
|
||||
}
|
||||
|
||||
var entry = GetOrCreateEntry(assetID);
|
||||
Logger.DebugAssert(entry.assetType == AssetType.Texture);
|
||||
|
||||
return entry.GetStorage<Handle<GPUTexture>>();
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,429 @@
|
||||
using Ghost.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
using Ghost.Core.Utilities;
|
||||
using Ghost.Engine.AssetLoader;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Ghost.Graphics.Services;
|
||||
using Ghost.Graphics.Utilities;
|
||||
using Misaki.HighPerformance.Buffer;
|
||||
using Misaki.HighPerformance.Jobs;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Engine;
|
||||
|
||||
internal abstract class RuntimeAsset;
|
||||
|
||||
internal interface IRuntimeAssetLoader
|
||||
public enum AssetState : byte
|
||||
{
|
||||
ValueTask<Result<RuntimeAsset>> LoadAsync(Stream cookedData, Guid id, CancellationToken token = default);
|
||||
Unloaded = 0,
|
||||
Scheduled = 1,
|
||||
Loading = 2,
|
||||
Loaded = 3,
|
||||
Ready = 4,
|
||||
Failed = 5,
|
||||
}
|
||||
|
||||
internal sealed class RuntimeLoaderRegistry
|
||||
public enum AssetType : byte
|
||||
{
|
||||
private readonly Dictionary<Guid, IRuntimeAssetLoader> _loaders = new();
|
||||
public void Register(Guid cookedTypeId, IRuntimeAssetLoader loader)
|
||||
Texture = 0,
|
||||
Mesh = 1,
|
||||
Material = 2,
|
||||
Audio = 3,
|
||||
Scene = 4,
|
||||
Video = 5,
|
||||
Json = 6,
|
||||
|
||||
Unknown = 255,
|
||||
}
|
||||
|
||||
internal interface IContentProvider
|
||||
{
|
||||
bool HasAsset(Guid guid);
|
||||
|
||||
Result<Stream> OpenRead(Guid guid, CancellationToken token = default);
|
||||
|
||||
Guid[] GetDependencies(Guid guid);
|
||||
|
||||
AssetType GetAssetType(Guid guid);
|
||||
}
|
||||
|
||||
// TODO: Support DirectStorage.
|
||||
public partial class AssetManager : IDisposable
|
||||
{
|
||||
private unsafe class AssetEntry : IDisposable
|
||||
{
|
||||
_loaders[cookedTypeId] = loader;
|
||||
private static readonly ObjectPool<AssetEntry> s_pool = new ObjectPool<AssetEntry>(() => new AssetEntry(), (entry) => entry.Reset());
|
||||
|
||||
public struct __storage
|
||||
{
|
||||
public fixed byte data[64];
|
||||
}
|
||||
|
||||
public Guid assetId;
|
||||
public __storage storage;
|
||||
public MemoryBlock rawData;
|
||||
|
||||
public JobHandle loadJobHandle;
|
||||
public AssetType assetType;
|
||||
public int state;
|
||||
public int refCount;
|
||||
|
||||
public static AssetEntry Create()
|
||||
{
|
||||
return s_pool.Rent();
|
||||
}
|
||||
|
||||
private AssetEntry()
|
||||
{
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
assetId = Guid.Empty;
|
||||
assetType = AssetType.Unknown;
|
||||
storage = default;
|
||||
rawData = default;
|
||||
state = (int)AssetState.Unloaded;
|
||||
refCount = 0;
|
||||
loadJobHandle = default;
|
||||
}
|
||||
|
||||
public void SetStorage<T>(T asset)
|
||||
where T : unmanaged
|
||||
{
|
||||
Unsafe.WriteUnaligned(ref storage.data[0], asset);
|
||||
}
|
||||
|
||||
public T GetStorage<T>()
|
||||
where T : unmanaged
|
||||
{
|
||||
return Unsafe.ReadUnaligned<T>(ref storage.data[0]);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
s_pool.Return(this);
|
||||
}
|
||||
}
|
||||
public IRuntimeAssetLoader? GetLoader(Guid cookedTypeId)
|
||||
|
||||
private struct LoadAssetJob : IJob
|
||||
{
|
||||
_loaders.TryGetValue(cookedTypeId, out var loader);
|
||||
return loader;
|
||||
public Guid assetID;
|
||||
public AssetType assetType;
|
||||
|
||||
public void Execute(ref readonly JobExecutionContext ctx)
|
||||
{
|
||||
var assetManager = ctx.State as AssetManager;
|
||||
|
||||
Debug.Assert(assetManager is not null);
|
||||
Debug.Assert(assetManager._contentProvider.GetAssetType(assetID) == assetType);
|
||||
|
||||
if (!assetManager._entries.TryGetValue(assetID, out var entry))
|
||||
{
|
||||
Logger.Error($"Asset entry not found for {assetID}");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = assetManager.LoadRawData(entry);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
Volatile.Write(ref entry.state, (int)AssetState.Failed);
|
||||
Logger.Error($"Failed to load asset {assetID}: {result.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
Volatile.Write(ref entry.state, (int)AssetState.Loaded);
|
||||
|
||||
// Ensure the buffer inside the resource database does not move.
|
||||
assetManager._resourceDatabase.EnterParallelRead();
|
||||
|
||||
try
|
||||
{
|
||||
switch (assetType)
|
||||
{
|
||||
case AssetType.Texture:
|
||||
result = assetManager.UploadTexture(entry);
|
||||
break;
|
||||
case AssetType.Mesh:
|
||||
break;
|
||||
case AssetType.Material:
|
||||
break;
|
||||
case AssetType.Audio:
|
||||
break;
|
||||
case AssetType.Scene:
|
||||
break;
|
||||
case AssetType.Video:
|
||||
break;
|
||||
case AssetType.Json:
|
||||
break;
|
||||
case AssetType.Unknown:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.IsFailure)
|
||||
{
|
||||
Logger.Error($"Failed to upload asset {assetID}: {result.Message}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
assetManager._resourceDatabase.ExitParallelRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IContentProvider _contentProvider;
|
||||
|
||||
private readonly ResourceManager _resourceManager;
|
||||
private readonly IResourceAllocator _resourceAllocator;
|
||||
private readonly IResourceDatabase _resourceDatabase;
|
||||
private readonly ResourceUploadBatch _uploadedBatch; // Upload via copy queue.
|
||||
|
||||
private readonly JobScheduler _jobScheduler;
|
||||
private readonly ConcurrentDictionary<Guid, AssetEntry> _entries;
|
||||
private readonly ConcurrentQueue<Guid> _pendingUploads;
|
||||
|
||||
// TODO
|
||||
private Handle<GPUTexture> _fallbackTexture;
|
||||
private Handle<GPUTexture> _fallbackNormalMap;
|
||||
private Handle<Mesh> _fallbackMesh;
|
||||
private Handle<Material> _fallbackMaterial;
|
||||
|
||||
internal AssetManager(IContentProvider contentProvider, ResourceManager resourceManager, IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, ResourceUploadBatch uploadBatch)
|
||||
{
|
||||
_contentProvider = contentProvider;
|
||||
_resourceManager = resourceManager;
|
||||
_resourceAllocator = resourceAllocator;
|
||||
_resourceDatabase = resourceDatabase;
|
||||
_uploadedBatch = uploadBatch;
|
||||
|
||||
// Ideally we should use a single JobScheduler across the entire engine, and schedule the streaming jobs to that scheduler as low priority background jobs.
|
||||
// But how can we get the reference to the AssetManager? Is force job types to be unmanaged a wrong decision? Because we don't have burst compiler at all.
|
||||
var threadCount = Environment.ProcessorCount < 8 ? 1 : 2;
|
||||
_jobScheduler = new JobScheduler(threadCount, ThreadPriority.BelowNormal, this);
|
||||
_entries = new ConcurrentDictionary<Guid, AssetEntry>();
|
||||
_pendingUploads = new ConcurrentQueue<Guid>();
|
||||
}
|
||||
|
||||
private JobHandle EnsureScheduled(Guid assetID)
|
||||
{
|
||||
if (_entries.TryGetValue(assetID, out var existing) && existing.state >= (int)AssetState.Scheduled)
|
||||
{
|
||||
return existing.loadJobHandle;
|
||||
}
|
||||
|
||||
// Resolve dependencies (in-memory manifest/catalog lookup — instant)
|
||||
var deps = _contentProvider.GetDependencies(assetID);
|
||||
|
||||
// Schedule all dependencies first (recursive, depth-first)
|
||||
JobHandle dependency = default;
|
||||
if (deps.Length > 0)
|
||||
{
|
||||
var depHandles = deps.Length <= 8
|
||||
? stackalloc JobHandle[deps.Length]
|
||||
: new JobHandle[deps.Length];
|
||||
|
||||
for (int i = 0; i < deps.Length; i++)
|
||||
{
|
||||
var depEntry = GetOrCreateEntry(deps[i]);
|
||||
depHandles[i] = depEntry.loadJobHandle;
|
||||
}
|
||||
|
||||
dependency = _jobScheduler.CombineDependencies(depHandles);
|
||||
}
|
||||
|
||||
if (_entries.TryGetValue(assetID, out var entry))
|
||||
{
|
||||
var job = new LoadAssetJob
|
||||
{
|
||||
assetID = assetID,
|
||||
assetType = entry.assetType,
|
||||
};
|
||||
|
||||
entry.loadJobHandle = _jobScheduler.Schedule(ref job, dependency);
|
||||
return entry.loadJobHandle;
|
||||
}
|
||||
|
||||
// This should not happen, because GetOrCreateEntry should have created the entry and scheduled the job.
|
||||
Debug.Fail($"Entry for {assetID} should have been created by GetOrCreateEntry");
|
||||
return JobHandle.Invalid;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private AssetEntry GetOrCreateEntry(Guid guid)
|
||||
{
|
||||
return _entries.GetOrAdd(guid, static (id, self) =>
|
||||
{
|
||||
var entry = AssetEntry.Create();
|
||||
entry.assetId = id;
|
||||
entry.assetType = self._contentProvider.GetAssetType(id);
|
||||
entry.state = (int)AssetState.Scheduled;
|
||||
|
||||
switch (entry.assetType)
|
||||
{
|
||||
case AssetType.Texture:
|
||||
entry.SetStorage(self.AllocateTextureHandle());
|
||||
break;
|
||||
case AssetType.Mesh:
|
||||
break;
|
||||
case AssetType.Material:
|
||||
break;
|
||||
case AssetType.Audio:
|
||||
break;
|
||||
case AssetType.Scene:
|
||||
break;
|
||||
case AssetType.Video:
|
||||
break;
|
||||
case AssetType.Json:
|
||||
break;
|
||||
case AssetType.Unknown:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
entry.loadJobHandle = self.EnsureScheduled(entry.assetId);
|
||||
|
||||
return entry;
|
||||
}, this);
|
||||
}
|
||||
|
||||
private Result LoadRawData(AssetEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = _contentProvider.OpenRead(entry.assetId).GetValueOrThrow();
|
||||
|
||||
var data = new MemoryBlock((nuint)stream.Length, MemoryUtility.AlignOf<IntPtr>(), AllocationHandle.Persistent);
|
||||
|
||||
// C# built-in collections use int for indexing, so we need to ensure that the buffer size does not exceed int.MaxValue
|
||||
var maxChunkSize = (int)Math.Min(0x7fffffffu, data.Size);
|
||||
var offset = 0u;
|
||||
|
||||
while (offset < data.Size)
|
||||
{
|
||||
using var mem = NativeMemoryManager<byte>.FromMemoryBlock(data, (int)offset, maxChunkSize);
|
||||
stream.ReadExactly(mem.Memory.Span);
|
||||
offset += (uint)mem.Memory.Length;
|
||||
}
|
||||
|
||||
entry.rawData = data;
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render thread only — single-threaded by design ──
|
||||
// TODO: Does this must be called on render thread? Can you just create a dedicated thred or a worker in thread pool for uploading?
|
||||
// Also, I think we may don't need this RenderContext at all, because the CommandBuffer is from the ResourceUploadBatch (via async upload), and the ResourceManager/Database/Allocator can be passed in the constructor.
|
||||
public void ProcessUploads(RenderContext ctx, int maxPerFrame = 4)
|
||||
{
|
||||
_uploadedBatch.Begin();
|
||||
|
||||
for (var i = 0; i < maxPerFrame; i++)
|
||||
{
|
||||
if (!_pendingUploads.TryDequeue(out var guid))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_entries.TryGetValue(guid, out var entry) || entry.asset is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var error = Error.Success;
|
||||
switch (entry.assetType)
|
||||
{
|
||||
case AssetType.Texture:
|
||||
var textureDesc = new TextureDesc
|
||||
{
|
||||
Width = textureAsset.Width,
|
||||
Height = textureAsset.Height,
|
||||
MipLevels = textureAsset.MipLevels,
|
||||
Slice = 1,
|
||||
Format = GetTextureFormat(textureAsset.Depth, textureAsset.ColorComponents),
|
||||
Dimension = GetTextureDimension(textureAsset.Dimension),
|
||||
Usage = TextureUsage.ShaderResource,
|
||||
};
|
||||
|
||||
// NOTE: We use Color128 here to avoid that c# span can't hold 16k x 16k x sizeof(float) x 4 textures, because the max span length is int.MaxValue.
|
||||
// Internal method will cast the data to void* so the type does not matter as long as the format and size are correct.
|
||||
var handle = RenderingUtility.CreateTexture(
|
||||
ctx.ResourceManager,
|
||||
ctx.ResourceDatabase,
|
||||
ctx.ResourceAllocator,
|
||||
_uploadedBatch.CommandBuffer,
|
||||
textureAsset.GeData<Color128>(),
|
||||
in textureDesc);
|
||||
|
||||
textureAsset.SetTextureHandle(handle);
|
||||
break;
|
||||
|
||||
default:
|
||||
error = Error.NotSupported;
|
||||
break;
|
||||
}
|
||||
|
||||
if (error.IsSuccess)
|
||||
{
|
||||
Volatile.Write(ref entry.state, (int)AssetState.Ready);
|
||||
}
|
||||
else
|
||||
{
|
||||
_pendingUploads.Enqueue(guid); // retry next frame
|
||||
}
|
||||
}
|
||||
|
||||
_uploadedBatch.End();
|
||||
|
||||
// TODO: Do we need to wait?
|
||||
// await _uploadedBatch.WaitAsync(); // WaitIdle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blocking load. Returns when the asset reaches at least Loaded state.
|
||||
/// GPU upload still happens via ProcessUploads on the render thread.
|
||||
/// Use for loading screens or synchronous initialization.
|
||||
/// </summary>
|
||||
public async ValueTask<T?> LoadAsync<T>(AssetRef<T> assetRef, CancellationToken token = default)
|
||||
where T : Asset
|
||||
{
|
||||
if (!assetRef.IsValid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entry = _entries.GetOrAdd(assetRef.guid, static (guid, self) =>
|
||||
{
|
||||
var e = new AssetEntryOld { assetId = guid, state = (int)AssetState.Loading };
|
||||
e.loadTask = Task.Run(() => self.ExecuteLoadAsync(e));
|
||||
return e;
|
||||
}, this);
|
||||
|
||||
if (Volatile.Read(ref entry.state) >= (int)AssetState.Loaded)
|
||||
{
|
||||
return entry.asset as T;
|
||||
}
|
||||
|
||||
var loadTask = entry.loadTask;
|
||||
if (loadTask is not null)
|
||||
{
|
||||
await loadTask.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return _entries.TryGetValue(assetRef.guid, out var e) ? e.asset as T : null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CookedTextureLoader : IRuntimeAssetLoader
|
||||
{
|
||||
public static readonly Guid TYPE_ID = TextureAsset.s_typeGuid;
|
||||
public async ValueTask<Result<RuntimeAsset>> LoadAsync(Stream cookedData, Guid id, CancellationToken token)
|
||||
{
|
||||
// Read the ImageContentHeader you wrote during import
|
||||
var header = new ImageContentHeader();
|
||||
cookedData.ReadExactly(MemoryMarshal.AsBytes(new Span<ImageContentHeader>(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
|
||||
{
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user