feat: implement core graphics rendering system and D3D12 RHI backend infrastructure

This commit is contained in:
2026-04-18 21:03:05 +09:00
parent abd5ad74d5
commit 4f5556ee1b
25 changed files with 1072 additions and 924 deletions

View File

@@ -3,11 +3,95 @@ using Ghost.Core.Utilities;
using Ghost.Engine.AssetLoader;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities;
using Misaki.HighPerformance.LowLevel.Buffer;
namespace Ghost.Engine;
public partial class AssetManager
{
private partial class AssetEntry
{
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 RecordTextureUpload(ICommandBuffer commandBuffer)
{
var pData = (byte*)_rawData.GetUnsafePtr();
var reader = new BufferReader(pData, _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,
commandBuffer,
reader.CurrentAddress,
reader.RemainingBytes,
in textureDesc);
if (newHandle.IsInvalid)
{
return Result.Failure("Failed to create GPU texture.");
}
var oldHandle = GetStorage<Handle<GPUTexture>>();
SetStorage((oldHandle, newHandle));
return Result.Success();
}
private void OnTextureUploadComplete()
{
var (oldHandle, newHandle) = GetStorage<(Handle<GPUTexture>, Handle<GPUTexture>)>();
ResourceDatabase.Swap(oldHandle.AsResource(), newHandle.AsResource());
ResourceDatabase.ReleaseResource(newHandle.AsResource()); // releases old fallback slot
SetStorage((oldHandle, Handle<GPUTexture>.Invalid)); // Old handle is now the new handle, and the old fallback slot is released. Use Invalid handle to clear second slot.
_rawData.Dispose();
_rawData = default;
}
}
private Handle<GPUTexture> AllocateTextureHandle()
{
// This will create a new slot in the database, but not allocation any GPU resource.
@@ -15,76 +99,6 @@ public partial class AssetManager
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)
@@ -93,7 +107,7 @@ public partial class AssetManager
}
var entry = GetOrCreateEntry(assetID);
Logger.DebugAssert(entry.assetType == AssetType.Texture);
Logger.DebugAssert(entry.AssetType == AssetType.Texture);
return entry.GetStorage<Handle<GPUTexture>>();
}

View File

@@ -1,13 +1,12 @@
using Ghost.Core;
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;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections.Concurrent;
using System.Diagnostics;
@@ -21,8 +20,9 @@ public enum AssetState : byte
Scheduled = 1,
Loading = 2,
Loaded = 3,
Ready = 4,
Failed = 5,
Uploading = 4,
Ready = 5,
Failed = 6,
}
public enum AssetType : byte
@@ -52,221 +52,52 @@ internal interface IContentProvider
// TODO: Support DirectStorage.
public partial class AssetManager : IDisposable
{
private unsafe class AssetEntry : IDisposable
private unsafe partial class AssetEntry : IDisposable
{
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;
private readonly AssetManager _assetManager;
public JobHandle loadJobHandle;
public AssetType assetType;
public int state;
public int refCount;
private Guid _assetId;
private __storage _storage;
private MemoryBlock _rawData;
public static AssetEntry Create()
private JobHandle _loadJobHandle;
private AssetType _assetType;
private int _refCount;
private int _state;
private ResourceManager ResourceManager => _assetManager._resourceManager;
private IResourceDatabase ResourceDatabase => _assetManager._resourceDatabase;
private IResourceAllocator ResourceAllocator => _assetManager._resourceAllocator;
public Guid AssetId => _assetId;
public MemoryBlock RawData => _rawData;
public JobHandle LoadJobHandle => _loadJobHandle;
public AssetType AssetType => _assetType;
public int RefCount => Volatile.Read(ref _refCount);
public AssetState State
{
return s_pool.Rent();
get => (AssetState)Volatile.Read(ref _state);
set => Volatile.Write(ref _state, (int)value);
}
private AssetEntry()
public AssetEntry(AssetManager manager, Guid assetId, AssetType assetType)
{
}
_assetManager = manager;
private void Reset()
{
assetId = Guid.Empty;
assetType = AssetType.Unknown;
storage = default;
rawData = default;
state = (int)AssetState.Unloaded;
refCount = 0;
loadJobHandle = default;
}
_assetId = assetId;
_assetType = assetType;
_refCount = 1;
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);
}
}
private struct LoadAssetJob : IJob
{
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)
switch (assetType)
{
case AssetType.Texture:
entry.SetStorage(self.AllocateTextureHandle());
SetStorage(manager.AllocateTextureHandle());
break;
case AssetType.Mesh:
break;
@@ -284,142 +115,366 @@ public partial class AssetManager : IDisposable
default:
break;
}
}
entry.loadJobHandle = self.EnsureScheduled(entry.assetId);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetStorage<T>(T asset)
where T : unmanaged
{
Unsafe.WriteUnaligned(ref _storage.data[0], asset);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetStorage<T>()
where T : unmanaged
{
return Unsafe.ReadUnaligned<T>(ref _storage.data[0]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetRawData([OwnershipTransfer] ref MemoryBlock data)
{
_rawData = data;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetLoadJobHandle(JobHandle handle)
{
_loadJobHandle = handle;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRef()
{
Interlocked.Increment(ref _refCount);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Release()
{
var newRefCount = Interlocked.Decrement(ref _refCount);
Debug.Assert(newRefCount >= 0, "Reference count should not be negative");
if (newRefCount == 0)
{
Dispose();
}
return newRefCount;
}
public void OnRecordUploadCommands(ICommandBuffer commandBuffer)
{
switch (_assetType)
{
case AssetType.Texture:
RecordTextureUpload(commandBuffer);
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:
break;
default:
break;
}
}
public void OnUploadComplete()
{
switch (_assetType)
{
case AssetType.Texture:
OnTextureUploadComplete();
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:
break;
default:
break;
}
Volatile.Write(ref _state, (int)AssetState.Ready);
}
public void Dispose()
{
var handle = GetStorage<Handle<GPUTexture>>();
ResourceDatabase.ReleaseResource(handle.AsResource());
_assetManager.RemoveEntry(_assetId);
}
}
private struct LoadAssetJob : IJob
{
public Guid assetID;
public AssetType assetType;
private static Result LoadRawData(IContentProvider contentProvider, 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.SetRawData(ref data);
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure(ex.Message);
}
}
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;
}
entry.State = AssetState.Loading;
var result = LoadRawData(assetManager._contentProvider, entry);
if (result.IsFailure)
{
entry.State = AssetState.Failed;
Logger.Error($"Failed to load asset {assetID}: {result.Message}");
return;
}
entry.State = AssetState.Loaded;
}
}
private const int _MAX_UPLOADS_PER_FRAME = 8;
private readonly IContentProvider _contentProvider;
private readonly ResourceManager _resourceManager;
private readonly IResourceAllocator _resourceAllocator;
private readonly IResourceDatabase _resourceDatabase;
private readonly AsyncCopyPipeline _copyPipeline; // Upload via copy queue.
private readonly JobScheduler _jobScheduler;
private readonly ConcurrentDictionary<Guid, AssetEntry> _entries;
private readonly ConcurrentQueue<AssetEntry> _pendingFinalize;
private ulong _pendingCopyFenceValue;
// 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, AsyncCopyPipeline uploadBatch)
{
_contentProvider = contentProvider;
_resourceManager = resourceManager;
_resourceAllocator = resourceAllocator;
_resourceDatabase = resourceDatabase;
_copyPipeline = uploadBatch;
var desc = new JobSchedulerDesc
{
ThreadCount = Environment.ProcessorCount < 8 ? 1 : 2,
ThreadPriority = ThreadPriority.BelowNormal,
State = this,
};
_jobScheduler = new JobScheduler(in desc);
_entries = new ConcurrentDictionary<Guid, AssetEntry>();
_pendingFinalize = new ConcurrentQueue<AssetEntry>();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool RemoveEntry(Guid guid)
{
return _entries.TryRemove(guid, out _);
}
private void EnsureScheduled(AssetEntry entry)
{
if ((int)entry.State >= (int)AssetState.Scheduled)
{
return;
}
// Resolve dependencies (in-memory manifest/catalog lookup — instant)
var deps = _contentProvider.GetDependencies(entry.AssetId);
var dependency = JobHandle.Invalid;
if (deps.Length > 0)
{
// Avoid stack overflow for deep dependency tree like a whole scene.
// Stack allocator here is fine, because it use virtual memory and has 32 mb capacity per thread.
using var scope = AllocationManager.CreateStackScope();
using var list = new UnsafeList<Guid>(deps.Length * 2, scope.AllocationHandle);
using var stack = new UnsafeStack<Guid>(deps.Length * 2, scope.AllocationHandle);
using var visited = new UnsafeHashSet<Guid>(deps.Length * 2, scope.AllocationHandle);
for (var i = 0; i < deps.Length; i++)
{
stack.Push(deps[i]);
}
while (stack.TryPop(out var guid))
{
if (visited.Contains(guid))
{
continue;
}
visited.Add(guid);
list.Add(guid);
var depss = _contentProvider.GetDependencies(guid);
foreach (var d in depss)
{
if (!visited.Contains(d))
{
stack.Push(d);
}
}
}
using var depHandles = new UnsafeList<JobHandle>(list.Count, scope.AllocationHandle);
// Schedule all dependencies first (depth-first)
for (var i = list.Count - 1; i >= 0; i--)
{
// This should create the entry and schedule the job on those assets does not have any dependency first.
var handle = GetOrCreateEntry(list[i]).LoadJobHandle;
Debug.Assert(handle.IsValid);
depHandles.Add(handle);
}
dependency = _jobScheduler.CombineDependencies(depHandles);
}
var job = new LoadAssetJob
{
assetID = entry.AssetId,
assetType = entry.AssetType,
};
entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, dependency));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private AssetEntry GetOrCreateEntry(Guid guid)
{
return _entries.GetOrAdd(guid, static (id, self) =>
{
var entry = new AssetEntry(self, id, self._contentProvider.GetAssetType(id))
{
State = AssetState.Scheduled
};
self.EnsureScheduled(entry);
return entry;
}, this);
}
private Result LoadRawData(AssetEntry entry)
// NOTE: Render thread only.
internal void ProcessPendingUploads()
{
try
// 1. If there's a pending copy batch from last frame, check its fence
if (_pendingCopyFenceValue > 0 && _copyPipeline.CurrentFenceValue() >= _pendingCopyFenceValue)
{
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)
while (_pendingFinalize.TryDequeue(out var item))
{
using var mem = NativeMemoryManager<byte>.FromMemoryBlock(data, (int)offset, maxChunkSize);
stream.ReadExactly(mem.Memory.Span);
offset += (uint)mem.Memory.Length;
item.OnUploadComplete();
}
entry.rawData = data;
return Result.Success();
_pendingCopyFenceValue = 0;
}
catch (Exception ex)
if (_pendingCopyFenceValue > 0)
{
return Result.Failure(ex.Message);
return;
}
}
// ── 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();
// 2. Collect entries that are in state == Loaded (I/O done, not yet uploaded)
// Cap per frame to avoid stalling (e.g., max 8 textures per frame)
_copyPipeline.Begin();
for (var i = 0; i < maxPerFrame; i++)
var cmdCopy = _copyPipeline.GetCommandBuffer();
var uploadCount = 0;
foreach (var (guid, entry) in _entries)
{
if (!_pendingUploads.TryDequeue(out var guid))
{
break;
}
if (!_entries.TryGetValue(guid, out var entry) || entry.asset is null)
if (entry.State != AssetState.Loaded)
{
continue;
}
var error = Error.Success;
switch (entry.assetType)
if (uploadCount >= _MAX_UPLOADS_PER_FRAME)
{
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;
break;
}
if (error.IsSuccess)
// Record copy commands into cmdCopy
entry.OnRecordUploadCommands(cmdCopy);
entry.State = AssetState.Uploading;
_pendingFinalize.Enqueue(entry);
uploadCount++;
}
// 3. Submit the batch
if (uploadCount > 0)
{
var result = _copyPipeline.End();
if (result.IsSuccess)
{
Volatile.Write(ref entry.state, (int)AssetState.Ready);
}
else
{
_pendingUploads.Enqueue(guid); // retry next frame
_pendingCopyFenceValue = _copyPipeline.SignaledFenceValue();
}
}
_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()