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

@@ -305,6 +305,11 @@ public static class Logger
{
System.Diagnostics.Debug.Fail(message ?? "Assertion failed.");
}
#elif GHOST_EDITOR
if (!condition)
{
throw new InvalidOperationException(message ?? "Assertion failed.");
}
#endif
}
}

View File

@@ -71,6 +71,25 @@ public readonly struct Result
return Result<T>.Failure(status.ToString());
}
public static Result Aggregate(params ReadOnlySpan<Result> results)
{
var sb = new System.Text.StringBuilder();
foreach (var result in results)
{
if (result.IsFailure)
{
sb.AppendLine(result.Message);
}
}
if (sb.Length == 0)
{
return Success();
}
return Failure(sb.ToString());
}
public void Deconstruct(out bool success, out string? message)
{
success = _isSuccess;

View File

@@ -103,49 +103,51 @@ public unsafe struct BufferReader
private readonly byte* _buffer;
private readonly nuint _size;
private byte* _position;
private byte* _address;
public readonly byte* Position => _position;
public readonly byte* CurrentAddress => _address;
public nuint Offset
public nuint Position
{
readonly get => (nuint)(_buffer + (_position - _buffer));
set => _position = _buffer + value;
readonly get => (nuint)(_buffer + (_address - _buffer));
set => _address = _buffer + value;
}
public readonly nuint RemainingBytes => (nuint)(_buffer + _size - _address);
public BufferReader(byte* buffer, nuint size)
{
_buffer = buffer;
_size = size;
_position = _buffer;
_address = _buffer;
}
public T Read<T>()
where T : unmanaged
{
var value = *(T*)_position;
_position += (nuint)sizeof(T);
var value = *(T*)_address;
_address += (nuint)sizeof(T);
return value;
}
public ReadOnlySpan<T> ReadSpan<T>(int length)
where T : unmanaged
{
length = Math.Min(length, (int)((nuint)(_buffer + _size - _position) / (nuint)sizeof(T)));
length = Math.Min(length, (int)((nuint)(_buffer + _size - _address) / (nuint)sizeof(T)));
var size = sizeof(T) * length;
var span = new ReadOnlySpan<T>(_position, length);
var span = new ReadOnlySpan<T>(_address, length);
_position += (nuint)size;
_address += (nuint)size;
return span;
}
public ReadOnlySpan<T> ReadToEnd<T>()
where T : unmanaged
{
var span = new ReadOnlySpan<T>(_position, (int)(_buffer + _size - _position));
var span = new ReadOnlySpan<T>(_address, (int)(_buffer + _size - _address));
_position += (nuint)(span.Length * sizeof(T));
_address += (nuint)(span.Length * sizeof(T));
return span;
}
}

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()

View File

@@ -215,78 +215,88 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
_resourceDatabase.globalBarrier = new ResourceBarrierData(BarrierLayout.Undefined, desc.AccessAfter, desc.SyncAfter);
break;
case BarrierType.Buffer:
{
var r = _resourceDatabase.GetResourceRecord(desc.Resource);
if (r.IsFailure)
{
var r = _resourceDatabase.GetResourceRecord(desc.Resource);
if (r.IsFailure)
{
RecordError(nameof(Barrier), r.Error);
continue;
}
ref var record = ref r.Value;
var accessBefore = desc.IsAliasing ? BarrierAccess.NoAccess : record.barrierData.access;
if (record.barrierData.sync == desc.SyncAfter && accessBefore == desc.AccessAfter)
{
continue;
}
var resource = record.ResourcePtr;
pBufferBarriers[bufferIndex++] = new D3D12_BUFFER_BARRIER
{
SyncBefore = (D3D12_BARRIER_SYNC)record.barrierData.sync,
SyncAfter = (D3D12_BARRIER_SYNC)desc.SyncAfter,
AccessBefore = (D3D12_BARRIER_ACCESS)accessBefore,
AccessAfter = (D3D12_BARRIER_ACCESS)desc.AccessAfter,
pResource = resource,
Offset = 0,
Size = ulong.MaxValue
};
record.barrierData = new ResourceBarrierData(BarrierLayout.Undefined, desc.AccessAfter, desc.SyncAfter);
RecordError(nameof(Barrier), r.Error);
continue;
}
break;
ref var record = ref r.Value;
if (!record.Allocated)
{
return;
}
var accessBefore = desc.IsAliasing ? BarrierAccess.NoAccess : record.barrierData.access;
if (record.barrierData.sync == desc.SyncAfter && accessBefore == desc.AccessAfter)
{
continue;
}
var resource = record.ResourcePtr;
pBufferBarriers[bufferIndex++] = new D3D12_BUFFER_BARRIER
{
SyncBefore = (D3D12_BARRIER_SYNC)record.barrierData.sync,
SyncAfter = (D3D12_BARRIER_SYNC)desc.SyncAfter,
AccessBefore = (D3D12_BARRIER_ACCESS)accessBefore,
AccessAfter = (D3D12_BARRIER_ACCESS)desc.AccessAfter,
pResource = resource,
Offset = 0,
Size = ulong.MaxValue
};
record.barrierData = new ResourceBarrierData(BarrierLayout.Undefined, desc.AccessAfter, desc.SyncAfter);
}
break;
case BarrierType.Texture:
{
var r = _resourceDatabase.GetResourceRecord(desc.Resource);
if (r.IsFailure)
{
var r = _resourceDatabase.GetResourceRecord(desc.Resource);
if (r.IsFailure)
{
RecordError(nameof(Barrier), r.Error);
continue;
}
ref var record = ref r.Value;
var accessBefore = desc.IsAliasing ? BarrierAccess.NoAccess : record.barrierData.access;
var layoutBefore = desc.IsAliasing ? BarrierLayout.Undefined : record.barrierData.layout;
if (record.barrierData.sync == desc.SyncAfter && accessBefore == desc.AccessAfter && layoutBefore == desc.LayoutAfter)
{
continue;
}
var resource = record.ResourcePtr;
pTextureBarriers[textureIndex++] = new D3D12_TEXTURE_BARRIER
{
SyncBefore = (D3D12_BARRIER_SYNC)record.barrierData.sync,
SyncAfter = (D3D12_BARRIER_SYNC)desc.SyncAfter,
AccessBefore = (D3D12_BARRIER_ACCESS)accessBefore,
AccessAfter = (D3D12_BARRIER_ACCESS)desc.AccessAfter,
LayoutBefore = (D3D12_BARRIER_LAYOUT)layoutBefore,
LayoutAfter = (D3D12_BARRIER_LAYOUT)desc.LayoutAfter,
pResource = resource,
Subresources = new D3D12_BARRIER_SUBRESOURCE_RANGE
{
IndexOrFirstMipLevel = desc.Subresources.IndexOrFirstMipLevel,
NumMipLevels = desc.Subresources.NumMipLevels,
FirstArraySlice = desc.Subresources.FirstArraySlice,
NumArraySlices = desc.Subresources.NumArraySlices
},
Flags = desc.Discard ? D3D12_TEXTURE_BARRIER_FLAGS.D3D12_TEXTURE_BARRIER_FLAG_DISCARD : D3D12_TEXTURE_BARRIER_FLAGS.D3D12_TEXTURE_BARRIER_FLAG_NONE
};
record.barrierData = new ResourceBarrierData(desc.LayoutAfter, desc.AccessAfter, desc.SyncAfter);
RecordError(nameof(Barrier), r.Error);
continue;
}
break;
ref var record = ref r.Value;
if (!record.Allocated)
{
return;
}
var accessBefore = desc.IsAliasing ? BarrierAccess.NoAccess : record.barrierData.access;
var layoutBefore = desc.IsAliasing ? BarrierLayout.Undefined : record.barrierData.layout;
if (record.barrierData.sync == desc.SyncAfter && accessBefore == desc.AccessAfter && layoutBefore == desc.LayoutAfter)
{
continue;
}
var resource = record.ResourcePtr;
pTextureBarriers[textureIndex++] = new D3D12_TEXTURE_BARRIER
{
SyncBefore = (D3D12_BARRIER_SYNC)record.barrierData.sync,
SyncAfter = (D3D12_BARRIER_SYNC)desc.SyncAfter,
AccessBefore = (D3D12_BARRIER_ACCESS)accessBefore,
AccessAfter = (D3D12_BARRIER_ACCESS)desc.AccessAfter,
LayoutBefore = (D3D12_BARRIER_LAYOUT)layoutBefore,
LayoutAfter = (D3D12_BARRIER_LAYOUT)desc.LayoutAfter,
pResource = resource,
Subresources = new D3D12_BARRIER_SUBRESOURCE_RANGE
{
IndexOrFirstMipLevel = desc.Subresources.IndexOrFirstMipLevel,
NumMipLevels = desc.Subresources.NumMipLevels,
FirstArraySlice = desc.Subresources.FirstArraySlice,
NumArraySlices = desc.Subresources.NumArraySlices
},
Flags = desc.Discard ? D3D12_TEXTURE_BARRIER_FLAGS.D3D12_TEXTURE_BARRIER_FLAG_DISCARD : D3D12_TEXTURE_BARRIER_FLAGS.D3D12_TEXTURE_BARRIER_FLAG_NONE
};
record.barrierData = new ResourceBarrierData(desc.LayoutAfter, desc.AccessAfter, desc.SyncAfter);
}
break;
}
}
@@ -364,7 +374,13 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
continue;
}
var viewGroup = recordResult.Value.viewGroup;
ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var viewGroup = record.viewGroup;
pRtvHandles[i] = _descriptorAllocator.GetCpuHandle(viewGroup.rtv);
rtvCount++;
@@ -380,7 +396,13 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
return;
}
var viewGroup = recordResult.Value.viewGroup;
ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var viewGroup = record.viewGroup;
pDsvHandle[0] = _descriptorAllocator.GetCpuHandle(viewGroup.dsv);
}
@@ -407,6 +429,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
}
ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.rtv);
pNativeObject->ClearRenderTargetView(cpuHandle, (float*)&clearColor, 0, null);
@@ -432,6 +459,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
}
ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv);
var flag = (inlcudeDepth ? D3D12_CLEAR_FLAG_DEPTH : 0) | (includeStencil ? D3D12_CLEAR_FLAG_STENCIL : 0);
@@ -473,6 +505,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
}
ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.rtv);
var format = record.desc.TextureDescriptor.Format.ToDXGIFormat();
var clearColor = rtDesc.ClearColor;
@@ -527,6 +564,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
}
ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv);
var format = record.desc.TextureDescriptor.Format.ToDXGIFormat();
@@ -675,6 +717,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
IncrementCommandCount();
var resource = _resourceDatabase.GetResource(buffer.AsResource());
if (resource == null)
{
return;
}
pNativeObject->SetGraphicsRootConstantBufferView(slot, resource.Get()->GetGPUVirtualAddress());
}
@@ -698,6 +745,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
}
ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var vbView = new D3D12_VERTEX_BUFFER_VIEW
{
BufferLocation = record.ResourcePtr.Get()->GetGPUVirtualAddress() + offset,
@@ -721,6 +773,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
IncrementCommandCount();
var resource = _resourceDatabase.GetResource(buffer.AsResource());
if (resource == null)
{
return;
}
var ibView = new D3D12_INDEX_BUFFER_VIEW
{
BufferLocation = resource.Get()->GetGPUVirtualAddress() + offset,
@@ -891,7 +948,6 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
var pSrcResource = _resourceDatabase.GetResource(src.AsResource());
if (pSrcResource == null || pDstResource == null)
{
RecordError(nameof(CopyBuffer), Error.InvalidArgument);
return;
}
@@ -922,7 +978,6 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
var d3d12Intermediate = _resourceDatabase.GetResource(intermediate);
if (d3d12Intermediate == null || d3d12Resource == null)
{
RecordError(nameof(UpdateSubResources), Error.InvalidArgument);
return;
}
@@ -992,7 +1047,6 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
var pSrcResource = _resourceDatabase.GetResource(src.AsResource());
if (pSrcResource == null || pDstResource == null)
{
RecordError(nameof(CopyTexture), Error.InvalidArgument);
return;
}

View File

@@ -1,8 +1,6 @@
using Ghost.Graphics.D3D12.Utilities;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel;
using System.Diagnostics;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Ghost.Graphics.D3D12;
@@ -11,11 +9,6 @@ namespace Ghost.Graphics.D3D12;
/// </summary>
internal unsafe class D3D12CommandQueue : D3D12Object<ID3D12CommandQueue>, ICommandQueue
{
private UniquePtr<ID3D12Fence> _fence;
private readonly AutoResetEvent _fenceEvent;
private ulong _fenceValue;
public CommandQueueType Type
{
get;
@@ -39,13 +32,6 @@ internal unsafe class D3D12CommandQueue : D3D12Object<ID3D12CommandQueue>, IComm
: base(CreateCommandQueue(device.NativeObject, type))
{
Type = type;
_fenceEvent = new AutoResetEvent(false);
_fenceValue = 0;
ID3D12Fence* pFence = default;
ThrowIfFailed(device.NativeObject.Get()->CreateFence(0, D3D12_FENCE_FLAGS.D3D12_FENCE_FLAG_NONE, __uuidof(pFence), (void**)&pFence));
_fence.Attach(pFence);
}
private static D3D12_COMMAND_LIST_TYPE ConvertCommandQueueType(CommandQueueType type)
@@ -123,83 +109,24 @@ internal unsafe class D3D12CommandQueue : D3D12Object<ID3D12CommandQueue>, IComm
pNativeObject->ExecuteCommandLists((uint)currentIndex, ppCommandLists);
}
public ulong Signal(ulong value)
public ulong Signal(IFence fence, ulong value)
{
AssertNotDisposed();
_fenceValue = value;
ThrowIfFailed(pNativeObject->Signal(_fence.Get(), _fenceValue));
return _fenceValue;
var d3d12Fence = fence as D3D12Fence;
Debug.Assert(d3d12Fence != null, "Fence must be a D3D12Fence");
ThrowIfFailed(pNativeObject->Signal(d3d12Fence.NativeObject, value));
return value;
}
public void WaitForValue(ulong value)
public void Wait(IFence fence, ulong value)
{
AssertNotDisposed();
if (_fence.Get()->GetCompletedValue() < value)
{
var handle = new HANDLE((void*)_fenceEvent.SafeWaitHandle.DangerousGetHandle());
if (_fence.Get()->SetEventOnCompletion(value, handle).SUCCEEDED)
{
_fenceEvent.WaitOne();
}
}
}
var d3d12Fence = fence as D3D12Fence;
Debug.Assert(d3d12Fence != null, "Fence must be a D3D12Fence");
public ulong GetCompletedValue()
{
AssertNotDisposed();
return _fence.Get()->GetCompletedValue();
}
public void WaitIdle()
{
AssertNotDisposed();
var fenceValue = Signal(Interlocked.Increment(ref _fenceValue));
WaitForValue(fenceValue);
}
public Task WaitAsync()
{
AssertNotDisposed();
var fenceValue = Signal(Interlocked.Increment(ref _fenceValue));
if (_fence.Get()->GetCompletedValue() >= fenceValue)
{
return Task.CompletedTask;
}
var tcs = new TaskCompletionSource();
var handle = new HANDLE((void*)_fenceEvent.SafeWaitHandle.DangerousGetHandle());
if (_fence.Get()->SetEventOnCompletion(fenceValue, handle).FAILED)
{
throw new InvalidOperationException("Failed to set event on completion.");
}
var registeredWait = ThreadPool.RegisterWaitForSingleObject(
_fenceEvent,
(state, timedOut) =>
{
var capturedTcs = (TaskCompletionSource)state!;
capturedTcs.SetResult();
_fenceEvent.Dispose();
},
tcs,
Timeout.Infinite,
executeOnlyOnce: true
);
tcs.Task.ContinueWith(_ => registeredWait.Unregister(null));
return tcs.Task;
}
protected override void Dispose(bool disposing)
{
_fence.Dispose();
_fenceEvent.Dispose();
pNativeObject->Wait(d3d12Fence.NativeObject, value);
}
}

View File

@@ -0,0 +1,75 @@
using Ghost.Graphics.RHI;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Ghost.Graphics.D3D12;
internal unsafe class D3D12Fence : D3D12Object<ID3D12Fence>, IFence
{
private readonly AutoResetEvent _fenceEvent;
public ulong CompletedValue => pNativeObject->GetCompletedValue();
public nint WaitHandle => _fenceEvent.SafeWaitHandle.DangerousGetHandle();
private static ID3D12Fence* CreateFence(D3D12RenderDevice device, ulong initialValue)
{
ID3D12Fence* pFence = default;
ThrowIfFailed(device.NativeObject.Get()->CreateFence(initialValue, D3D12_FENCE_FLAGS.D3D12_FENCE_FLAG_NONE, __uuidof(pFence), (void**)&pFence));
return pFence;
}
public D3D12Fence(D3D12RenderDevice device, ulong initialValue = 0)
: base(CreateFence(device, initialValue))
{
_fenceEvent = new AutoResetEvent(false);
}
public void WaitForValue(ulong value)
{
AssertNotDisposed();
if (pNativeObject->GetCompletedValue() < value)
{
var handle = new HANDLE((void*)WaitHandle);
if (pNativeObject->SetEventOnCompletion(value, handle).SUCCEEDED)
{
_fenceEvent.WaitOne();
}
}
}
public Task WaitForValueAsync(ulong value)
{
AssertNotDisposed();
if (pNativeObject->GetCompletedValue() >= value)
{
return Task.CompletedTask;
}
var tcs = new TaskCompletionSource();
var handle = new HANDLE((void*)_fenceEvent.SafeWaitHandle.DangerousGetHandle());
if (pNativeObject->SetEventOnCompletion(value, handle).FAILED)
{
throw new InvalidOperationException("Failed to set event on completion.");
}
var registeredWait = ThreadPool.RegisterWaitForSingleObject(
_fenceEvent,
(state, timedOut) =>
{
var capturedTcs = (TaskCompletionSource)state!;
capturedTcs.SetResult();
_fenceEvent.Dispose();
},
tcs,
Timeout.Infinite,
executeOnlyOnce: true
);
tcs.Task.ContinueWith(_ => registeredWait.Unregister(null));
return tcs.Task;
}
}

View File

@@ -126,6 +126,12 @@ internal class D3D12GraphicsEngine : IGraphicsEngine
return new DXGISwapChain(_resourceDatabase, _descriptorAllocator, _device, desc, _desc.FrameBufferCount);
}
public IFence CreateFence(ulong initialValue = 0)
{
Logger.DebugAssert(!_disposed);
return new D3D12Fence(_device, initialValue);
}
public ICommandSignature CreateCommandSignature(ref readonly CommandSignatureDesc desc, Key128<PipelineState> pipelineKey)
{
Logger.DebugAssert(!_disposed);

View File

@@ -506,6 +506,7 @@ internal sealed unsafe partial class D3D12ResourceAllocator : IResourceAllocator
return hr;
}
// TODO: Should we move this to device?
public ResourceSizeInfo GetSizeInfo(ResourceDesc desc)
{
D3D12_RESOURCE_DESC1 d3d12Desc;

View File

@@ -7,7 +7,6 @@ using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
using System.Runtime.InteropServices;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Ghost.Graphics.D3D12;
@@ -115,7 +114,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
private UnsafeQueue<ReleaseEntry> _releaseQueue;
private int _writeLock;
private readonly Lock _writeLock;
private ulong _cpuFrame;
private bool _disposed;
@@ -133,6 +132,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
#endif
_releaseQueue = new UnsafeQueue<ReleaseEntry>(32, AllocationHandle.Persistent);
_writeLock = new Lock();
}
~D3D12ResourceDatabase()
@@ -152,13 +152,10 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
return Handle<GPUResource>.Invalid;
}
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
// It's fine here to use lock. System.Threading.Lock use LowLevelSpinWaiter internally before it escalates to a kernel lock, so it should be very cheap in the uncontended case.
// And adding resources is not a very frequent operation, so we can afford the potential overhead here for the sake of simplicity and correctness.
// We do not choose a concurrent collection here because we want maximum access speed for read operations.
lock (_writeLock)
{
var id = _resources.Add(new ResourceRecord(pResource, initialBarrierData, viewGroup, desc), out var generation);
var handle = new Handle<GPUResource>(id, generation);
@@ -173,10 +170,6 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
return handle;
}
finally
{
Interlocked.Exchange(ref _writeLock, 0);
}
}
internal Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null)
@@ -191,13 +184,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
return Handle<GPUResource>.Invalid;
}
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
lock (_writeLock)
{
var id = _resources.Add(new ResourceRecord(allocation, initialBarrierData, resourceDescriptor, desc), out var generation);
var handle = new Handle<GPUResource>(id, generation);
@@ -217,22 +204,6 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
return handle;
}
finally
{
Interlocked.Exchange(ref _writeLock, 0);
}
}
public void EnterParallelRead()
{
Logger.DebugAssert(!_disposed);
Interlocked.Exchange(ref _writeLock, 1);
}
public void ExitParallelRead()
{
Logger.DebugAssert(!_disposed);
Interlocked.Exchange(ref _writeLock, 0);
}
public bool HasResource(Handle<GPUResource> handle)
@@ -416,30 +387,20 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
return Handle<GPUResource>.Invalid;
}
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
var (srcRecord, error) = GetResourceRecord(src);
if (error.IsFailure)
{
spinner.SpinOnce();
return Handle<GPUResource>.Invalid;
}
try
lock (_writeLock)
{
var (srcRecord, error) = GetResourceRecord(src);
if (error.IsFailure)
{
return Handle<GPUResource>.Invalid;
}
var newRecord = srcRecord.Get();
newRecord.isShared = true;
var id = _resources.Add(newRecord, out var generation);
return new Handle<GPUResource>(id, generation);
}
finally
{
Interlocked.Exchange(ref _writeLock, 0);
}
}
public void* MapResource(Handle<GPUResource> handle, uint subResource, ResourceRange? readRange)

View File

@@ -122,7 +122,7 @@ internal static unsafe class D3D12Utility
{
return dimension switch
{
D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_TEXTURE1D => TextureDimension.Texture2D,
D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_TEXTURE1D => TextureDimension.Texture1D,
D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_TEXTURE2D => TextureDimension.Texture2D,
D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_TEXTURE3D => TextureDimension.Texture3D,
_ => throw new NotSupportedException($"Resource dimension {dimension} is not supported."),

View File

@@ -1411,11 +1411,12 @@ public enum TextureDimension
{
Unknown = -1,
None = 0,
Texture2D = 1,
Texture3D = 2,
TextureCube = 3,
Texture2DArray = 4,
TextureCubeArray = 5
Texture1D = 1,
Texture2D = 2,
Texture3D = 3,
TextureCube = 4,
Texture2DArray = 5,
TextureCubeArray = 6
}
public enum RenderTargetType

View File

@@ -28,30 +28,18 @@ public interface ICommandQueue : IRHIObject
/// <summary>
/// Signals a fence with the specified Value
/// </summary>
/// <param name="fence">Fence to signal</param>
/// <param name="value">Value to signal</param>
/// <returns>The fence Value that was signaled</returns>
ulong Signal(ulong value);
ulong Signal(IFence fence, ulong value);
/// <summary>
/// Waits for the fence to reach the specified Value
/// Insert a GPU wait on the specified fence and value. The GPU will wait until the fence reaches the specified value before executing any further commands.
/// </summary>
/// <remarks>
/// CPU will return immediately.
/// </remarks>
/// <param name="fence">Fence to wait on</param>
/// <param name="value">Value to wait for</param>
void WaitForValue(ulong value);
/// <summary>
/// Gets the last completed fence Value
/// </summary>
/// <returns>Last completed fence Value</returns>
ulong GetCompletedValue();
/// <summary>
/// Waits until all submitted commands have finished executing
/// </summary>
void WaitIdle();
/// <summary>
/// Waits asynchronously until all submitted commands have finished executing
/// </summary>
/// <returns>Task that completes when the queue is idle</returns>
Task WaitAsync();
void Wait(IFence fence, ulong value);
}

View File

@@ -0,0 +1,18 @@
namespace Ghost.Graphics.RHI;
public interface IFence : IRHIObject
{
ulong CompletedValue
{
get;
}
nint WaitHandle
{
get;
}
void WaitForValue(ulong value);
Task WaitForValueAsync(ulong value);
}

View File

@@ -66,6 +66,13 @@ public interface IGraphicsEngine : IDisposable
/// <returns>A new swap chain instance</returns>
ISwapChain CreateSwapChain(SwapChainDesc desc);
/// <summary>
/// Creates a fence for GPU synchronization with an optional initial value.
/// </summary>
/// <param name="initialValue">The initial value for the fence</param>
/// <returns>The created fence instance</returns>
IFence CreateFence(ulong initialValue = 0);
/// <summary>
/// Begin the current frame.
/// </summary>

View File

@@ -33,16 +33,6 @@ public enum BindlessAccess
public unsafe interface IResourceDatabase : IDisposable
{
/// <summary>
/// Enters a parallel read section, allowing multiple threads to read from the resource database concurrently and block any write operations until all readers have exited.
/// </summary>
void EnterParallelRead();
/// <summary>
/// Exits a parallel read section, allowing write operations to proceed once all readers have exited.
/// </summary>
void ExitParallelRead();
/// <summary>
/// Checks if a resource with the specified handle exists in the database.
/// </summary>

View File

@@ -1,4 +1,5 @@
using Ghost.Core;
using System.Runtime.CompilerServices;
namespace Ghost.Graphics.RHI;
@@ -10,21 +11,25 @@ public readonly struct Sampler;
public static class ResourceHandleExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Handle<GPUResource> AsResource(this Handle<GPUTexture> texture)
{
return new Handle<GPUResource>(texture.ID, texture.Generation);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Handle<GPUResource> AsResource(this Handle<GPUBuffer> buffer)
{
return new Handle<GPUResource>(buffer.ID, buffer.Generation);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Handle<GPUTexture> AsTexture(this Handle<GPUResource> resource)
{
return new Handle<GPUTexture>(resource.ID, resource.Generation);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Handle<GPUBuffer> AsBuffer(this Handle<GPUResource> resource)
{
return new Handle<GPUBuffer>(resource.ID, resource.Generation);

View File

@@ -92,6 +92,7 @@ public class RenderSystem : IDisposable
private readonly SwapChainManager _swapChainManager;
private readonly ShaderLibrary _shaderLibrary;
private readonly IFence _fence;
private readonly FrameResource[] _frameResources;
private readonly Thread _renderThread;
private readonly AutoResetEvent _shutdownEvent;
@@ -181,6 +182,7 @@ public class RenderSystem : IDisposable
_swapChainManager = new SwapChainManager(_graphicsEngine);
_shaderLibrary = new ShaderLibrary(desc.ShaderCompilationBridge, desc.ShaderCacheDirectory);
_fence = _graphicsEngine.CreateFence(0);
// Create frame resources for synchronization
_frameResources = new FrameResource[desc.FrameBufferCount];
for (var i = 0; i < desc.FrameBufferCount; i++)
@@ -259,7 +261,7 @@ public class RenderSystem : IDisposable
continue;
}
_graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue);
_fence.WaitForValue(frameResource.FenceValue);
if (!_resizeRequest.IsEmpty)
{
@@ -275,7 +277,7 @@ public class RenderSystem : IDisposable
}
}
var completedFrame = _graphicsEngine.Device.GraphicsQueue.GetCompletedValue();
var completedFrame = _fence.CompletedValue;
if (_submittedFenceValue < completedFrame)
{
_submittedFenceValue = completedFrame;
@@ -318,10 +320,10 @@ public class RenderSystem : IDisposable
}
_submittedFenceValue++;
frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(_submittedFenceValue);
frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(_fence, _submittedFenceValue);
frameResource.GpuReadyEvent.Set();
completedFrame = _graphicsEngine.Device.GraphicsQueue.GetCompletedValue();
completedFrame = _fence.CompletedValue;
// End the frame and retire resources based on the freshest observed GPU progress.
_resourceManager.EndFrame(completedFrame);
@@ -386,7 +388,7 @@ public class RenderSystem : IDisposable
var requiredGpuFence = _cpuFenceValue < _config.FrameBufferCount ? 0 : _cpuFenceValue - _config.FrameBufferCount + 1;
if (requiredGpuFence > 0 && _graphicsEngine.Device.GraphicsQueue.GetCompletedValue() < requiredGpuFence)
if (requiredGpuFence > 0 && _fence.CompletedValue < requiredGpuFence)
{
return false;
}
@@ -418,7 +420,7 @@ public class RenderSystem : IDisposable
{
if (frameResource.FenceValue > 0)
{
_graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue);
_fence.WaitForValue(frameResource.FenceValue);
}
}
}

View File

@@ -0,0 +1,77 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
namespace Ghost.Graphics.Services;
public class AsyncCopyPipeline
{
private readonly IRenderDevice _device;
private readonly ICommandAllocator _commandAllocator;
private readonly ICommandBuffer _commandBuffer;
private readonly IFence _fence;
private ulong _fenceValue;
internal AsyncCopyPipeline(IGraphicsEngine engine)
{
_device = engine.Device;
_commandAllocator = engine.CreateCommandAllocator(CommandBufferType.Copy);
_commandBuffer = engine.CreateCommandBuffer(CommandBufferType.Copy);
_fence = engine.CreateFence(0);
_commandAllocator.Name = $"AsyncCopyPipeline_CommandAllocator";
_commandBuffer.Name = $"AsyncCopyPipeline_CommandBuffer";
_fence.Name = "AsyncCopyPipeline_Fence";
}
internal void Begin()
{
_commandAllocator.Reset();
_commandBuffer.Begin(_commandAllocator);
}
internal Result End()
{
var result = _commandBuffer.End();
if (result.IsSuccess)
{
_device.CopyQueue.Submit(_commandBuffer);
_device.CopyQueue.Signal(_fence, ++_fenceValue);
}
return result;
}
public ICommandBuffer GetCommandBuffer()
{
return _commandBuffer;
}
public ulong CurrentFenceValue()
{
return _fence.CompletedValue;
}
public ulong SignaledFenceValue()
{
return _fenceValue;
}
public bool IsCopyComplete()
{
return _fence.CompletedValue >= _fenceValue;
}
public void WaitIdle()
{
_fence.WaitForValue(_fenceValue);
}
public Task WaitAsync()
{
return _fence.WaitForValueAsync(_fenceValue);
}
}

View File

@@ -27,11 +27,13 @@ public partial class ResourceManager
public ulong retireFrame;
}
private UnsafeList<Page> _activePages = new UnsafeList<Page>(4, AllocationHandle.Persistent);
private UnsafeQueue<Page> _freePages = new UnsafeQueue<Page>(4, AllocationHandle.Persistent);
private UnsafeQueue<RetiringPage> _retiringPages = new UnsafeQueue<RetiringPage>(4, AllocationHandle.Persistent);
private UnsafeList<Page> _activePages = new UnsafeList<Page>(8, AllocationHandle.Persistent);
private UnsafeQueue<Page> _freePages = new UnsafeQueue<Page>(8, AllocationHandle.Persistent);
private UnsafeQueue<RetiringPage> _retiringPages = new UnsafeQueue<RetiringPage>(8, AllocationHandle.Persistent);
private UnsafeList<Handle<GPUResource>> _frameTransientResources = new UnsafeList<Handle<GPUResource>>(4, AllocationHandle.Persistent);
private UnsafeList<Handle<GPUResource>> _frameTransientResources = new UnsafeList<Handle<GPUResource>>(8, AllocationHandle.Persistent);
private readonly Lock _transientWriteLock = new Lock();
private static bool IsHeapFlagsCompatible(HeapFlags pageHeapFlags, HeapFlags requiredHeapFlags)
{
@@ -96,159 +98,167 @@ public partial class ResourceManager
var isRTOrDS = desc.Usage.HasFlag(TextureUsage.DepthStencil) || desc.Usage.HasFlag(TextureUsage.RenderTarget);
var size = _resourceAllocator.GetSizeInfo(ResourceDesc.Texture(desc));
if (size.Size > DEFAULT_TRANSIENT_PAGE_SIZE)
// TODO: Any better way?
lock (_transientWriteLock)
{
var texHandle = _resourceAllocator.CreateTexture(in desc, name);
if (texHandle.IsValid)
if (size.Size > DEFAULT_TRANSIENT_PAGE_SIZE)
{
_frameTransientResources.Add(texHandle.AsResource());
var texHandle = _resourceAllocator.CreateTexture(in desc, name);
if (texHandle.IsValid)
{
_frameTransientResources.Add(texHandle.AsResource());
}
return texHandle;
}
return texHandle;
var requiredHeapFlags = _renderDevice.FeatureSupport.HasFlag(FeatureSupport.AliasBuffersAndTextures) ?
HeapFlags.AllowAllBufferAndTexture :
isRTOrDS ? HeapFlags.AllowOnlyRTAndDS : HeapFlags.AllowOnlyTextures;
var foundPageIndex = -1;
var alignedOffset = 0UL;
for (var i = 0; i < _activePages.Count; i++)
{
ref var p = ref _activePages[i];
if (p.heapType != HeapType.Default)
{
continue;
}
if (!IsHeapFlagsCompatible(p.heapFlags, requiredHeapFlags))
{
continue;
}
var proposedOffset = (p.offset + (size.Alignment - 1)) & ~(size.Alignment - 1);
if (proposedOffset + size.Size <= DEFAULT_TRANSIENT_PAGE_SIZE)
{
foundPageIndex = i;
alignedOffset = proposedOffset;
break;
}
}
if (foundPageIndex == -1)
{
var error = CreateNewActivePage(HeapType.Default, requiredHeapFlags);
if (error != Error.None)
{
Debug.Fail($"Failed to create a new page for transient texture: {error}");
return Handle<GPUTexture>.Invalid;
}
foundPageIndex = _activePages.Count - 1;
alignedOffset = 0;
}
ref var page = ref _activePages[foundPageIndex];
var handle = _resourceAllocator.CreateTexture(in desc, name, new CreationOptions
{
AllocationType = ResourceAllocationType.Suballocation,
Heap = page.heap,
Offset = alignedOffset,
});
if (handle.IsValid)
{
page.offset = alignedOffset + size.Size;
_frameTransientResources.Add(handle.AsResource());
}
return handle;
}
var requiredHeapFlags = _renderDevice.FeatureSupport.HasFlag(FeatureSupport.AliasBuffersAndTextures) ?
HeapFlags.AllowAllBufferAndTexture :
isRTOrDS ? HeapFlags.AllowOnlyRTAndDS : HeapFlags.AllowOnlyTextures;
var foundPageIndex = -1;
var alignedOffset = 0UL;
for (var i = 0; i < _activePages.Count; i++)
{
ref var p = ref _activePages[i];
if (p.heapType != HeapType.Default)
{
continue;
}
if (!IsHeapFlagsCompatible(p.heapFlags, requiredHeapFlags))
{
continue;
}
var proposedOffset = (p.offset + (size.Alignment - 1)) & ~(size.Alignment - 1);
if (proposedOffset + size.Size <= DEFAULT_TRANSIENT_PAGE_SIZE)
{
foundPageIndex = i;
alignedOffset = proposedOffset;
break;
}
}
if (foundPageIndex == -1)
{
var error = CreateNewActivePage(HeapType.Default, requiredHeapFlags);
if (error != Error.None)
{
Debug.Fail($"Failed to create a new page for transient texture: {error}");
return Handle<GPUTexture>.Invalid;
}
foundPageIndex = _activePages.Count - 1;
alignedOffset = 0;
}
ref var page = ref _activePages[foundPageIndex];
var handle = _resourceAllocator.CreateTexture(in desc, name, new CreationOptions
{
AllocationType = ResourceAllocationType.Suballocation,
Heap = page.heap,
Offset = alignedOffset,
});
if (handle.IsValid)
{
page.offset = alignedOffset + size.Size;
_frameTransientResources.Add(handle.AsResource());
}
return handle;
}
public Handle<GPUBuffer> CreateTransientBuffer(ref readonly BufferDesc desc, string? name = null)
{
var size = _resourceAllocator.GetSizeInfo(ResourceDesc.Buffer(desc));
if (size.Size > DEFAULT_TRANSIENT_PAGE_SIZE)
lock (_transientWriteLock)
{
var bufHandle = _resourceAllocator.CreateBuffer(in desc, name);
if (bufHandle.IsValid)
if (size.Size > DEFAULT_TRANSIENT_PAGE_SIZE)
{
_frameTransientResources.Add(bufHandle.AsResource());
var bufHandle = _resourceAllocator.CreateBuffer(in desc, name);
if (bufHandle.IsValid)
{
_frameTransientResources.Add(bufHandle.AsResource());
}
return bufHandle;
}
return bufHandle;
var requiredHeapType = desc.HeapType switch
{
HeapType.Upload => HeapType.Upload,
HeapType.Readback => HeapType.Readback,
_ => HeapType.Default
};
var requiredHeapFlags = _renderDevice.FeatureSupport.HasFlag(FeatureSupport.AliasBuffersAndTextures) ?
HeapFlags.AllowAllBufferAndTexture : HeapFlags.AllowOnlyBuffers;
var foundPageIndex = -1;
var alignedOffset = 0UL;
for (var i = 0; i < _activePages.Count; i++)
{
ref var p = ref _activePages[i];
if (p.heapType != requiredHeapType)
{
continue;
}
if (!IsHeapFlagsCompatible(p.heapFlags, requiredHeapFlags))
{
continue;
}
var proposedOffset = (p.offset + (size.Alignment - 1)) & ~(size.Alignment - 1);
if (proposedOffset + size.Size <= DEFAULT_TRANSIENT_PAGE_SIZE)
{
foundPageIndex = i;
alignedOffset = proposedOffset;
break;
}
}
if (foundPageIndex == -1)
{
var error = CreateNewActivePage(requiredHeapType, requiredHeapFlags);
if (error != Error.None)
{
Debug.Fail($"Failed to create a new page for transient buffer: {error}");
return Handle<GPUBuffer>.Invalid;
}
foundPageIndex = _activePages.Count - 1;
alignedOffset = 0;
}
ref var page = ref _activePages[foundPageIndex];
var handle = _resourceAllocator.CreateBuffer(in desc, name, new CreationOptions
{
AllocationType = ResourceAllocationType.Suballocation,
Heap = page.heap,
Offset = alignedOffset,
});
if (handle.IsValid)
{
page.offset = alignedOffset + size.Size;
_frameTransientResources.Add(handle.AsResource());
}
return handle;
}
var requiredHeapType = desc.HeapType switch
{
HeapType.Upload => HeapType.Upload,
HeapType.Readback => HeapType.Readback,
_ => HeapType.Default
};
var requiredHeapFlags = _renderDevice.FeatureSupport.HasFlag(FeatureSupport.AliasBuffersAndTextures) ?
HeapFlags.AllowAllBufferAndTexture : HeapFlags.AllowOnlyBuffers;
var foundPageIndex = -1;
var alignedOffset = 0UL;
for (var i = 0; i < _activePages.Count; i++)
{
ref var p = ref _activePages[i];
if (p.heapType != requiredHeapType)
{
continue;
}
if (!IsHeapFlagsCompatible(p.heapFlags, requiredHeapFlags))
{
continue;
}
var proposedOffset = (p.offset + (size.Alignment - 1)) & ~(size.Alignment - 1);
if (proposedOffset + size.Size <= DEFAULT_TRANSIENT_PAGE_SIZE)
{
foundPageIndex = i;
alignedOffset = proposedOffset;
break;
}
}
if (foundPageIndex == -1)
{
var error = CreateNewActivePage(requiredHeapType, requiredHeapFlags);
if (error != Error.None)
{
Debug.Fail($"Failed to create a new page for transient buffer: {error}");
return Handle<GPUBuffer>.Invalid;
}
foundPageIndex = _activePages.Count - 1;
alignedOffset = 0;
}
ref var page = ref _activePages[foundPageIndex];
var handle = _resourceAllocator.CreateBuffer(in desc, name, new CreationOptions
{
AllocationType = ResourceAllocationType.Suballocation,
Heap = page.heap,
Offset = alignedOffset,
});
if (handle.IsValid)
{
page.offset = alignedOffset + size.Size;
_frameTransientResources.Add(handle.AsResource());
}
return handle;
}
private void EndFramePool(ulong completedFrame)

View File

@@ -33,8 +33,15 @@ public sealed partial class ResourceManager : IDisposable
private readonly MaterialPaletteStore _materialPalettes;
// TODO: Any better way? System.Threading.Lock is very fast though, it use spin lock before entering kernel.
// rw lock slim is an option but it has more overhead on read, and for more than 90% of the time we are reading, it may not be a good option.
// Plus UnsafeSlotMap use jagged array internally, which means we can have concurrent read and write on different slots without any issue, so we only need to lock when writing to those slots.
private readonly Lock _meshWriteLock;
private readonly Lock _materialWriteLock;
private readonly Lock _shaderWriteLock;
private readonly Lock _computeShaderWriteLock;
private ulong _submittedFrame;
private int _writeLock;
private bool _disposed;
@@ -50,6 +57,11 @@ public sealed partial class ResourceManager : IDisposable
_computeShaders = new UnsafeSlotMap<ComputeShader>(16, AllocationHandle.Persistent);
_materialPalettes = new MaterialPaletteStore();
_meshWriteLock = new Lock();
_materialWriteLock = new Lock();
_shaderWriteLock = new Lock();
_computeShaderWriteLock = new Lock();
}
~ResourceManager()
@@ -69,18 +81,6 @@ public sealed partial class ResourceManager : IDisposable
EndFramePool(completedFrame);
}
public void EnterParallelRead()
{
Logger.DebugAssert(!_disposed);
Volatile.Write(ref _writeLock, 1);
}
public void ExitParallelRead()
{
Logger.DebugAssert(!_disposed);
Volatile.Write(ref _writeLock, 0);
}
/// <summary>
/// Creates a new mesh from the specified vertex and index data.
/// </summary>
@@ -91,13 +91,7 @@ public sealed partial class ResourceManager : IDisposable
{
Logger.DebugAssert(!_disposed);
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
lock (_meshWriteLock)
{
var vertexBufferDesc = new BufferDesc
{
@@ -139,10 +133,6 @@ public sealed partial class ResourceManager : IDisposable
var id = _meshes.Add(mesh, out var generation);
return new Handle<Mesh>(id, generation);
}
finally
{
Volatile.Write(ref _writeLock, 0);
}
}
/// <summary>
@@ -154,15 +144,10 @@ public sealed partial class ResourceManager : IDisposable
{
Logger.DebugAssert(!_disposed);
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
var material = new Material();
try
lock (_materialWriteLock)
{
var material = new Material();
if (material.SetShader(shader, this, _resourceDatabase, _resourceAllocator) != Error.None)
{
return Handle<Material>.Invalid;
@@ -171,10 +156,6 @@ public sealed partial class ResourceManager : IDisposable
var id = _materials.Add(material, out var generation);
return new Handle<Material>(id, generation);
}
finally
{
Volatile.Write(ref _writeLock, 0);
}
}
/// <summary>
@@ -186,44 +167,26 @@ public sealed partial class ResourceManager : IDisposable
{
Logger.DebugAssert(!_disposed);
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
var shader = new Shader(descriptor);
try
lock (_shaderWriteLock)
{
var shader = new Shader(descriptor);
var id = _shaders.Add(shader, out var generation);
return new Handle<Shader>(id, generation);
}
finally
{
Volatile.Write(ref _writeLock, 0);
}
}
public Handle<ComputeShader> CreateComputeShader(ComputeShaderDescriptor descriptor)
{
Logger.DebugAssert(!_disposed);
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
var computeShader = new ComputeShader(descriptor);
lock (_computeShaderWriteLock)
{
var computeShader = new ComputeShader(descriptor);
var id = _computeShaders.Add(computeShader, out var generation);
return new Handle<ComputeShader>(id, generation);
}
finally
{
Volatile.Write(ref _writeLock, 0);
}
}
/// <summary>
@@ -261,14 +224,17 @@ public sealed partial class ResourceManager : IDisposable
{
Logger.DebugAssert(!_disposed);
ref var mesh = ref _meshes.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist)
lock (_meshWriteLock)
{
return;
}
ref var mesh = ref _meshes.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist)
{
return;
}
_meshes.Remove(handle.ID, handle.Generation);
mesh.ReleaseResource(_resourceDatabase);
_meshes.Remove(handle.ID, handle.Generation);
mesh.ReleaseResource(_resourceDatabase);
}
}
/// <summary>
@@ -306,14 +272,17 @@ public sealed partial class ResourceManager : IDisposable
{
Logger.DebugAssert(!_disposed);
ref var material = ref _materials.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist)
lock (_materialWriteLock)
{
return;
}
ref var material = ref _materials.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist)
{
return;
}
_materials.Remove(handle.ID, handle.Generation);
material.ReleaseResource(_resourceDatabase);
_materials.Remove(handle.ID, handle.Generation);
material.ReleaseResource(_resourceDatabase);
}
}
/// <summary>
@@ -412,14 +381,17 @@ public sealed partial class ResourceManager : IDisposable
{
Logger.DebugAssert(!_disposed);
ref var shader = ref _shaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist)
lock (_shaderWriteLock)
{
return;
}
ref var shader = ref _shaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist)
{
return;
}
_shaders.Remove(handle.ID, handle.Generation);
shader.ReleaseResource(_resourceDatabase);
_shaders.Remove(handle.ID, handle.Generation);
shader.ReleaseResource(_resourceDatabase);
}
}
/// <summary>
@@ -457,14 +429,17 @@ public sealed partial class ResourceManager : IDisposable
{
Logger.DebugAssert(!_disposed);
ref var computeShader = ref _computeShaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist)
lock (_computeShaderWriteLock)
{
return;
}
ref var computeShader = ref _computeShaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist)
{
return;
}
_computeShaders.Remove(handle.ID, handle.Generation);
computeShader.ReleaseResource(_resourceDatabase);
_computeShaders.Remove(handle.ID, handle.Generation);
computeShader.ReleaseResource(_resourceDatabase);
}
}
public void Dispose()

View File

@@ -1,50 +0,0 @@
using Ghost.Core;
using Ghost.Graphics.RHI;
namespace Ghost.Graphics.Services;
public class ResourceUploadBatch
{
private readonly IRenderDevice _device;
private readonly ICommandAllocator _commandAllocator;
private readonly ICommandBuffer _commandBuffer;
public ICommandBuffer CommandBuffer => _commandBuffer;
internal ResourceUploadBatch(IGraphicsEngine engine)
{
_device = engine.Device;
_commandAllocator = engine.CreateCommandAllocator(CommandBufferType.Copy);
_commandBuffer = engine.CreateCommandBuffer(CommandBufferType.Copy);
}
public void Begin()
{
_commandAllocator.Reset();
_commandBuffer.Begin(_commandAllocator);
}
public Result End()
{
var r = _commandBuffer.End();
if (r.IsFailure)
{
return r;
}
_device.CopyQueue.Submit(_commandBuffer);
return Result.Success();
}
public void WaitIdle()
{
_device.CopyQueue.WaitIdle();
}
public Task WaitAsync()
{
return _device.CopyQueue.WaitAsync();
}
}

View File

@@ -89,7 +89,7 @@ public static unsafe class RenderingUtility
}
}
public static Error UploadTexture(ResourceManager resourceManager, IResourceDatabase resourceDatabase, ICommandBuffer cmd, Handle<GPUTexture> texture, void* pData)
public static Error UploadTexture(ResourceManager resourceManager, IResourceDatabase resourceDatabase, ICommandBuffer cmd, Handle<GPUTexture> texture, void* pData, nuint sizeInBytes)
{
var (desc, error) = resourceDatabase.GetResourceDescription(texture.AsResource());
if (error.IsFailure)
@@ -100,6 +100,11 @@ public static unsafe class RenderingUtility
desc.TextureDescriptor.Format.GetSurfaceInfo(desc.TextureDescriptor.Width, desc.TextureDescriptor.Height, out var rowPitch, out var slicePitch, out _);
var requiredSize = resourceDatabase.GetIntermediateResourceSize(texture.AsResource(), 0, 1);
if (sizeInBytes < requiredSize)
{
return Error.InvalidArgument;
}
var uploadDesc = new BufferDesc
{
Size = requiredSize,
@@ -133,18 +138,18 @@ public static unsafe class RenderingUtility
{
fixed (T* pData = data)
{
return UploadTexture(resourceManager, resourceDatabase, cmd, texture, pData);
return UploadTexture(resourceManager, resourceDatabase, cmd, texture, pData, (nuint)(data.Length * sizeof(T)));
}
}
public static Handle<GPUTexture> CreateTexture(ResourceManager resourceManager, IResourceDatabase resourceDatabase, IResourceAllocator resourceAllocator, ICommandBuffer cmd, void* pData, ref readonly TextureDesc desc, string? name = null)
public static Handle<GPUTexture> CreateTexture(ResourceManager resourceManager, IResourceDatabase resourceDatabase, IResourceAllocator resourceAllocator, ICommandBuffer cmd, void* pData, nuint sizeInBytes, ref readonly TextureDesc desc, string? name = null)
{
var error = Error.UnknownError;
var textureHandle = resourceAllocator.CreateTexture(in desc, name);
if (!textureHandle.IsInvalid)
{
error = UploadTexture(resourceManager, resourceDatabase, cmd, textureHandle, pData);
error = UploadTexture(resourceManager, resourceDatabase, cmd, textureHandle, pData, sizeInBytes);
}
if (error.IsSuccess)
@@ -161,7 +166,7 @@ public static unsafe class RenderingUtility
{
fixed (T* pData = data)
{
return CreateTexture(resourceManager, resourceDatabase, resourceAllocator, cmd, pData, in desc, name);
return CreateTexture(resourceManager, resourceDatabase, resourceAllocator, cmd, pData, (nuint)(data.Length * sizeof(T)), in desc, name);
}
}
}