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

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@
AGENTS.md AGENTS.md
.opencode/ .opencode/
.code-review-graph/ .code-review-graph/
.github/instructions/
ref/ ref/
docfx/ docfx/

View File

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

View File

@@ -71,6 +71,25 @@ public readonly struct Result
return Result<T>.Failure(status.ToString()); 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) public void Deconstruct(out bool success, out string? message)
{ {
success = _isSuccess; success = _isSuccess;

View File

@@ -103,49 +103,51 @@ public unsafe struct BufferReader
private readonly byte* _buffer; private readonly byte* _buffer;
private readonly nuint _size; 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)); readonly get => (nuint)(_buffer + (_address - _buffer));
set => _position = _buffer + value; set => _address = _buffer + value;
} }
public readonly nuint RemainingBytes => (nuint)(_buffer + _size - _address);
public BufferReader(byte* buffer, nuint size) public BufferReader(byte* buffer, nuint size)
{ {
_buffer = buffer; _buffer = buffer;
_size = size; _size = size;
_position = _buffer; _address = _buffer;
} }
public T Read<T>() public T Read<T>()
where T : unmanaged where T : unmanaged
{ {
var value = *(T*)_position; var value = *(T*)_address;
_position += (nuint)sizeof(T); _address += (nuint)sizeof(T);
return value; return value;
} }
public ReadOnlySpan<T> ReadSpan<T>(int length) public ReadOnlySpan<T> ReadSpan<T>(int length)
where T : unmanaged 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 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; return span;
} }
public ReadOnlySpan<T> ReadToEnd<T>() public ReadOnlySpan<T> ReadToEnd<T>()
where T : unmanaged 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; return span;
} }
} }

View File

@@ -3,18 +3,14 @@ using Ghost.Core.Utilities;
using Ghost.Engine.AssetLoader; using Ghost.Engine.AssetLoader;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Utilities; using Ghost.Graphics.Utilities;
using Misaki.HighPerformance.LowLevel.Buffer;
namespace Ghost.Engine; namespace Ghost.Engine;
public partial class AssetManager public partial class AssetManager
{ {
private Handle<GPUTexture> AllocateTextureHandle() private partial class AssetEntry
{ {
// This will create a new slot in the database, but not allocation any GPU resource.
// Everything in the slot will have the same value as the fallback texture, expect the slot will be marked as shared.
return _resourceDatabase.CreateShared(_fallbackTexture.AsResource()).AsTexture();
}
private static TextureFormat GetTextureFormat(uint depth, uint colorComponents) private static TextureFormat GetTextureFormat(uint depth, uint colorComponents)
{ {
return colorComponents switch return colorComponents switch
@@ -44,10 +40,10 @@ public partial class AssetManager
}; };
} }
private unsafe Result UploadTexture(AssetEntry entry) private unsafe Result RecordTextureUpload(ICommandBuffer commandBuffer)
{ {
var pData = (byte*)entry.rawData.GetUnsafePtr(); var pData = (byte*)_rawData.GetUnsafePtr();
var reader = new BufferReader(pData, entry.rawData.Size); var reader = new BufferReader(pData, _rawData.Size);
var header = reader.Read<TextureContentHeader>(); var header = reader.Read<TextureContentHeader>();
@@ -63,11 +59,12 @@ public partial class AssetManager
}; };
var newHandle = RenderingUtility.CreateTexture( var newHandle = RenderingUtility.CreateTexture(
_resourceManager, ResourceManager,
_resourceDatabase, ResourceDatabase,
_resourceAllocator, ResourceAllocator,
_uploadedBatch.CommandBuffer, commandBuffer,
reader.Position, reader.CurrentAddress,
reader.RemainingBytes,
in textureDesc); in textureDesc);
if (newHandle.IsInvalid) if (newHandle.IsInvalid)
@@ -75,16 +72,33 @@ public partial class AssetManager
return Result.Failure("Failed to create GPU texture."); 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 = GetStorage<Handle<GPUTexture>>();
var oldHandle = entry.GetStorage<Handle<GPUTexture>>(); SetStorage((oldHandle, newHandle));
_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(); 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.
// Everything in the slot will have the same value as the fallback texture, expect the slot will be marked as shared.
return _resourceDatabase.CreateShared(_fallbackTexture.AsResource()).AsTexture();
}
public Handle<GPUTexture> ResolveTexture(Guid assetID) public Handle<GPUTexture> ResolveTexture(Guid assetID)
{ {
if (assetID == Guid.Empty) if (assetID == Guid.Empty)
@@ -93,7 +107,7 @@ public partial class AssetManager
} }
var entry = GetOrCreateEntry(assetID); var entry = GetOrCreateEntry(assetID);
Logger.DebugAssert(entry.assetType == AssetType.Texture); Logger.DebugAssert(entry.AssetType == AssetType.Texture);
return entry.GetStorage<Handle<GPUTexture>>(); return entry.GetStorage<Handle<GPUTexture>>();
} }

View File

@@ -1,13 +1,12 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Utilities; using Ghost.Core.Utilities;
using Ghost.Engine.AssetLoader;
using Ghost.Graphics.Core; using Ghost.Graphics.Core;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Services; using Ghost.Graphics.Services;
using Ghost.Graphics.Utilities;
using Misaki.HighPerformance.Buffer;
using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
@@ -21,8 +20,9 @@ public enum AssetState : byte
Scheduled = 1, Scheduled = 1,
Loading = 2, Loading = 2,
Loaded = 3, Loaded = 3,
Ready = 4, Uploading = 4,
Failed = 5, Ready = 5,
Failed = 6,
} }
public enum AssetType : byte public enum AssetType : byte
@@ -52,59 +52,177 @@ internal interface IContentProvider
// TODO: Support DirectStorage. // TODO: Support DirectStorage.
public partial class AssetManager : IDisposable 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 struct __storage
{ {
public fixed byte data[64]; public fixed byte data[64];
} }
public Guid assetId; private readonly AssetManager _assetManager;
public __storage storage;
public MemoryBlock rawData;
public JobHandle loadJobHandle; private Guid _assetId;
public AssetType assetType; private __storage _storage;
public int state; private MemoryBlock _rawData;
public int refCount;
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;
_assetId = assetId;
_assetType = assetType;
_refCount = 1;
switch (assetType)
{
case AssetType.Texture:
SetStorage(manager.AllocateTextureHandle());
break;
case AssetType.Mesh:
break;
case AssetType.Material:
break;
case AssetType.Audio:
break;
case AssetType.Scene:
break;
case AssetType.Video:
break;
case AssetType.Json:
break;
case AssetType.Unknown:
default:
break;
}
} }
private void Reset() [MethodImpl(MethodImplOptions.AggressiveInlining)]
{
assetId = Guid.Empty;
assetType = AssetType.Unknown;
storage = default;
rawData = default;
state = (int)AssetState.Unloaded;
refCount = 0;
loadJobHandle = default;
}
public void SetStorage<T>(T asset) public void SetStorage<T>(T asset)
where T : unmanaged where T : unmanaged
{ {
Unsafe.WriteUnaligned(ref storage.data[0], asset); Unsafe.WriteUnaligned(ref _storage.data[0], asset);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T GetStorage<T>() public T GetStorage<T>()
where T : unmanaged where T : unmanaged
{ {
return Unsafe.ReadUnaligned<T>(ref storage.data[0]); 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() public void Dispose()
{ {
s_pool.Return(this); var handle = GetStorage<Handle<GPUTexture>>();
ResourceDatabase.ReleaseResource(handle.AsResource());
_assetManager.RemoveEntry(_assetId);
} }
} }
@@ -113,189 +231,11 @@ public partial class AssetManager : IDisposable
public Guid assetID; public Guid assetID;
public AssetType assetType; public AssetType assetType;
public void Execute(ref readonly JobExecutionContext ctx) private static Result LoadRawData(IContentProvider contentProvider, AssetEntry entry)
{
var assetManager = ctx.State as AssetManager;
Debug.Assert(assetManager is not null);
Debug.Assert(assetManager._contentProvider.GetAssetType(assetID) == assetType);
if (!assetManager._entries.TryGetValue(assetID, out var entry))
{
Logger.Error($"Asset entry not found for {assetID}");
return;
}
var result = assetManager.LoadRawData(entry);
if (result.IsFailure)
{
Volatile.Write(ref entry.state, (int)AssetState.Failed);
Logger.Error($"Failed to load asset {assetID}: {result.Message}");
return;
}
Volatile.Write(ref entry.state, (int)AssetState.Loaded);
// Ensure the buffer inside the resource database does not move.
assetManager._resourceDatabase.EnterParallelRead();
try
{
switch (assetType)
{
case AssetType.Texture:
result = assetManager.UploadTexture(entry);
break;
case AssetType.Mesh:
break;
case AssetType.Material:
break;
case AssetType.Audio:
break;
case AssetType.Scene:
break;
case AssetType.Video:
break;
case AssetType.Json:
break;
case AssetType.Unknown:
default:
break;
}
if (result.IsFailure)
{
Logger.Error($"Failed to upload asset {assetID}: {result.Message}");
return;
}
}
finally
{
assetManager._resourceDatabase.ExitParallelRead();
}
}
}
private readonly IContentProvider _contentProvider;
private readonly ResourceManager _resourceManager;
private readonly IResourceAllocator _resourceAllocator;
private readonly IResourceDatabase _resourceDatabase;
private readonly ResourceUploadBatch _uploadedBatch; // Upload via copy queue.
private readonly JobScheduler _jobScheduler;
private readonly ConcurrentDictionary<Guid, AssetEntry> _entries;
private readonly ConcurrentQueue<Guid> _pendingUploads;
// TODO
private Handle<GPUTexture> _fallbackTexture;
private Handle<GPUTexture> _fallbackNormalMap;
private Handle<Mesh> _fallbackMesh;
private Handle<Material> _fallbackMaterial;
internal AssetManager(IContentProvider contentProvider, ResourceManager resourceManager, IResourceAllocator resourceAllocator, IResourceDatabase resourceDatabase, ResourceUploadBatch uploadBatch)
{
_contentProvider = contentProvider;
_resourceManager = resourceManager;
_resourceAllocator = resourceAllocator;
_resourceDatabase = resourceDatabase;
_uploadedBatch = uploadBatch;
// Ideally we should use a single JobScheduler across the entire engine, and schedule the streaming jobs to that scheduler as low priority background jobs.
// But how can we get the reference to the AssetManager? Is force job types to be unmanaged a wrong decision? Because we don't have burst compiler at all.
var threadCount = Environment.ProcessorCount < 8 ? 1 : 2;
_jobScheduler = new JobScheduler(threadCount, ThreadPriority.BelowNormal, this);
_entries = new ConcurrentDictionary<Guid, AssetEntry>();
_pendingUploads = new ConcurrentQueue<Guid>();
}
private JobHandle EnsureScheduled(Guid assetID)
{
if (_entries.TryGetValue(assetID, out var existing) && existing.state >= (int)AssetState.Scheduled)
{
return existing.loadJobHandle;
}
// Resolve dependencies (in-memory manifest/catalog lookup — instant)
var deps = _contentProvider.GetDependencies(assetID);
// Schedule all dependencies first (recursive, depth-first)
JobHandle dependency = default;
if (deps.Length > 0)
{
var depHandles = deps.Length <= 8
? stackalloc JobHandle[deps.Length]
: new JobHandle[deps.Length];
for (int i = 0; i < deps.Length; i++)
{
var depEntry = GetOrCreateEntry(deps[i]);
depHandles[i] = depEntry.loadJobHandle;
}
dependency = _jobScheduler.CombineDependencies(depHandles);
}
if (_entries.TryGetValue(assetID, out var entry))
{
var job = new LoadAssetJob
{
assetID = assetID,
assetType = entry.assetType,
};
entry.loadJobHandle = _jobScheduler.Schedule(ref job, dependency);
return entry.loadJobHandle;
}
// This should not happen, because GetOrCreateEntry should have created the entry and scheduled the job.
Debug.Fail($"Entry for {assetID} should have been created by GetOrCreateEntry");
return JobHandle.Invalid;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private AssetEntry GetOrCreateEntry(Guid guid)
{
return _entries.GetOrAdd(guid, static (id, self) =>
{
var entry = AssetEntry.Create();
entry.assetId = id;
entry.assetType = self._contentProvider.GetAssetType(id);
entry.state = (int)AssetState.Scheduled;
switch (entry.assetType)
{
case AssetType.Texture:
entry.SetStorage(self.AllocateTextureHandle());
break;
case AssetType.Mesh:
break;
case AssetType.Material:
break;
case AssetType.Audio:
break;
case AssetType.Scene:
break;
case AssetType.Video:
break;
case AssetType.Json:
break;
case AssetType.Unknown:
default:
break;
}
entry.loadJobHandle = self.EnsureScheduled(entry.assetId);
return entry;
}, this);
}
private Result LoadRawData(AssetEntry entry)
{ {
try try
{ {
using var stream = _contentProvider.OpenRead(entry.assetId).GetValueOrThrow(); using var stream = contentProvider.OpenRead(entry.AssetId).GetValueOrThrow();
var data = new MemoryBlock((nuint)stream.Length, MemoryUtility.AlignOf<IntPtr>(), AllocationHandle.Persistent); var data = new MemoryBlock((nuint)stream.Length, MemoryUtility.AlignOf<IntPtr>(), AllocationHandle.Persistent);
@@ -310,7 +250,7 @@ public partial class AssetManager : IDisposable
offset += (uint)mem.Memory.Length; offset += (uint)mem.Memory.Length;
} }
entry.rawData = data; entry.SetRawData(ref data);
return Result.Success(); return Result.Success();
} }
@@ -320,106 +260,221 @@ public partial class AssetManager : IDisposable
} }
} }
// ── Render thread only — single-threaded by design ── public void Execute(ref readonly JobExecutionContext ctx)
// 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(); var assetManager = ctx.State as AssetManager;
for (var i = 0; i < maxPerFrame; i++) Debug.Assert(assetManager is not null);
Debug.Assert(assetManager._contentProvider.GetAssetType(assetID) == assetType);
if (!assetManager._entries.TryGetValue(assetID, out var entry))
{ {
if (!_pendingUploads.TryDequeue(out var guid)) Logger.Error($"Asset entry not found for {assetID}");
{ return;
break;
} }
if (!_entries.TryGetValue(guid, out var entry) || entry.asset is null) 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; continue;
} }
var error = Error.Success; visited.Add(guid);
switch (entry.assetType) list.Add(guid);
var depss = _contentProvider.GetDependencies(guid);
foreach (var d in depss)
{ {
case AssetType.Texture: if (!visited.Contains(d))
var textureDesc = new TextureDesc
{ {
Width = textureAsset.Width, stack.Push(d);
Height = textureAsset.Height, }
MipLevels = textureAsset.MipLevels, }
Slice = 1, }
Format = GetTextureFormat(textureAsset.Depth, textureAsset.ColorComponents),
Dimension = GetTextureDimension(textureAsset.Dimension), using var depHandles = new UnsafeList<JobHandle>(list.Count, scope.AllocationHandle);
Usage = TextureUsage.ShaderResource,
// 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,
}; };
// 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. entry.SetLoadJobHandle(_jobScheduler.Schedule(ref job, dependency));
// Internal method will cast the data to void* so the type does not matter as long as the format and size are correct.
var handle = RenderingUtility.CreateTexture(
ctx.ResourceManager,
ctx.ResourceDatabase,
ctx.ResourceAllocator,
_uploadedBatch.CommandBuffer,
textureAsset.GeData<Color128>(),
in textureDesc);
textureAsset.SetTextureHandle(handle);
break;
default:
error = Error.NotSupported;
break;
} }
if (error.IsSuccess) [MethodImpl(MethodImplOptions.AggressiveInlining)]
private AssetEntry GetOrCreateEntry(Guid guid)
{ {
Volatile.Write(ref entry.state, (int)AssetState.Ready); return _entries.GetOrAdd(guid, static (id, self) =>
}
else
{ {
_pendingUploads.Enqueue(guid); // retry next frame var entry = new AssetEntry(self, id, self._contentProvider.GetAssetType(id))
}
}
_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) State = AssetState.Scheduled
{ };
return null;
}
var entry = _entries.GetOrAdd(assetRef.guid, static (guid, self) => self.EnsureScheduled(entry);
{ return entry;
var e = new AssetEntryOld { assetId = guid, state = (int)AssetState.Loading };
e.loadTask = Task.Run(() => self.ExecuteLoadAsync(e));
return e;
}, this); }, this);
if (Volatile.Read(ref entry.state) >= (int)AssetState.Loaded)
{
return entry.asset as T;
} }
var loadTask = entry.loadTask; // NOTE: Render thread only.
if (loadTask is not null) internal void ProcessPendingUploads()
{ {
await loadTask.WaitAsync(token).ConfigureAwait(false); // 1. If there's a pending copy batch from last frame, check its fence
if (_pendingCopyFenceValue > 0 && _copyPipeline.CurrentFenceValue() >= _pendingCopyFenceValue)
{
while (_pendingFinalize.TryDequeue(out var item))
{
item.OnUploadComplete();
} }
return _entries.TryGetValue(assetRef.guid, out var e) ? e.asset as T : null; _pendingCopyFenceValue = 0;
}
if (_pendingCopyFenceValue > 0)
{
return;
}
// 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();
var cmdCopy = _copyPipeline.GetCommandBuffer();
var uploadCount = 0;
foreach (var (guid, entry) in _entries)
{
if (entry.State != AssetState.Loaded)
{
continue;
}
if (uploadCount >= _MAX_UPLOADS_PER_FRAME)
{
break;
}
// 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)
{
_pendingCopyFenceValue = _copyPipeline.SignaledFenceValue();
}
}
} }
public void Dispose() public void Dispose()

View File

@@ -224,6 +224,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
} }
ref var record = ref r.Value; ref var record = ref r.Value;
if (!record.Allocated)
{
return;
}
var accessBefore = desc.IsAliasing ? BarrierAccess.NoAccess : record.barrierData.access; var accessBefore = desc.IsAliasing ? BarrierAccess.NoAccess : record.barrierData.access;
if (record.barrierData.sync == desc.SyncAfter && accessBefore == desc.AccessAfter) if (record.barrierData.sync == desc.SyncAfter && accessBefore == desc.AccessAfter)
@@ -256,6 +261,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
} }
ref var record = ref r.Value; ref var record = ref r.Value;
if (!record.Allocated)
{
return;
}
var accessBefore = desc.IsAliasing ? BarrierAccess.NoAccess : record.barrierData.access; var accessBefore = desc.IsAliasing ? BarrierAccess.NoAccess : record.barrierData.access;
var layoutBefore = desc.IsAliasing ? BarrierLayout.Undefined : record.barrierData.layout; var layoutBefore = desc.IsAliasing ? BarrierLayout.Undefined : record.barrierData.layout;
@@ -364,7 +374,13 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
continue; 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); pRtvHandles[i] = _descriptorAllocator.GetCpuHandle(viewGroup.rtv);
rtvCount++; rtvCount++;
@@ -380,7 +396,13 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
return; 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); pDsvHandle[0] = _descriptorAllocator.GetCpuHandle(viewGroup.dsv);
} }
@@ -407,6 +429,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
} }
ref var record = ref recordResult.Value; ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.rtv); var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.rtv);
pNativeObject->ClearRenderTargetView(cpuHandle, (float*)&clearColor, 0, null); pNativeObject->ClearRenderTargetView(cpuHandle, (float*)&clearColor, 0, null);
@@ -432,6 +459,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
} }
ref var record = ref recordResult.Value; ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv); var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv);
var flag = (inlcudeDepth ? D3D12_CLEAR_FLAG_DEPTH : 0) | (includeStencil ? D3D12_CLEAR_FLAG_STENCIL : 0); 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; ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.rtv); var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.rtv);
var format = record.desc.TextureDescriptor.Format.ToDXGIFormat(); var format = record.desc.TextureDescriptor.Format.ToDXGIFormat();
var clearColor = rtDesc.ClearColor; var clearColor = rtDesc.ClearColor;
@@ -527,6 +564,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
} }
ref var record = ref recordResult.Value; ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv); var cpuHandle = _descriptorAllocator.GetCpuHandle(record.viewGroup.dsv);
var format = record.desc.TextureDescriptor.Format.ToDXGIFormat(); var format = record.desc.TextureDescriptor.Format.ToDXGIFormat();
@@ -675,6 +717,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
IncrementCommandCount(); IncrementCommandCount();
var resource = _resourceDatabase.GetResource(buffer.AsResource()); var resource = _resourceDatabase.GetResource(buffer.AsResource());
if (resource == null)
{
return;
}
pNativeObject->SetGraphicsRootConstantBufferView(slot, resource.Get()->GetGPUVirtualAddress()); pNativeObject->SetGraphicsRootConstantBufferView(slot, resource.Get()->GetGPUVirtualAddress());
} }
@@ -698,6 +745,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
} }
ref var record = ref recordResult.Value; ref var record = ref recordResult.Value;
if (!record.Allocated)
{
return;
}
var vbView = new D3D12_VERTEX_BUFFER_VIEW var vbView = new D3D12_VERTEX_BUFFER_VIEW
{ {
BufferLocation = record.ResourcePtr.Get()->GetGPUVirtualAddress() + offset, BufferLocation = record.ResourcePtr.Get()->GetGPUVirtualAddress() + offset,
@@ -721,6 +773,11 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
IncrementCommandCount(); IncrementCommandCount();
var resource = _resourceDatabase.GetResource(buffer.AsResource()); var resource = _resourceDatabase.GetResource(buffer.AsResource());
if (resource == null)
{
return;
}
var ibView = new D3D12_INDEX_BUFFER_VIEW var ibView = new D3D12_INDEX_BUFFER_VIEW
{ {
BufferLocation = resource.Get()->GetGPUVirtualAddress() + offset, BufferLocation = resource.Get()->GetGPUVirtualAddress() + offset,
@@ -891,7 +948,6 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
var pSrcResource = _resourceDatabase.GetResource(src.AsResource()); var pSrcResource = _resourceDatabase.GetResource(src.AsResource());
if (pSrcResource == null || pDstResource == null) if (pSrcResource == null || pDstResource == null)
{ {
RecordError(nameof(CopyBuffer), Error.InvalidArgument);
return; return;
} }
@@ -922,7 +978,6 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
var d3d12Intermediate = _resourceDatabase.GetResource(intermediate); var d3d12Intermediate = _resourceDatabase.GetResource(intermediate);
if (d3d12Intermediate == null || d3d12Resource == null) if (d3d12Intermediate == null || d3d12Resource == null)
{ {
RecordError(nameof(UpdateSubResources), Error.InvalidArgument);
return; return;
} }
@@ -992,7 +1047,6 @@ internal unsafe class D3D12CommandBuffer : D3D12Object<ID3D12GraphicsCommandList
var pSrcResource = _resourceDatabase.GetResource(src.AsResource()); var pSrcResource = _resourceDatabase.GetResource(src.AsResource());
if (pSrcResource == null || pDstResource == null) if (pSrcResource == null || pDstResource == null)
{ {
RecordError(nameof(CopyTexture), Error.InvalidArgument);
return; return;
} }

View File

@@ -1,8 +1,6 @@
using Ghost.Graphics.D3D12.Utilities;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel; using System.Diagnostics;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Ghost.Graphics.D3D12; namespace Ghost.Graphics.D3D12;
@@ -11,11 +9,6 @@ namespace Ghost.Graphics.D3D12;
/// </summary> /// </summary>
internal unsafe class D3D12CommandQueue : D3D12Object<ID3D12CommandQueue>, ICommandQueue internal unsafe class D3D12CommandQueue : D3D12Object<ID3D12CommandQueue>, ICommandQueue
{ {
private UniquePtr<ID3D12Fence> _fence;
private readonly AutoResetEvent _fenceEvent;
private ulong _fenceValue;
public CommandQueueType Type public CommandQueueType Type
{ {
get; get;
@@ -39,13 +32,6 @@ internal unsafe class D3D12CommandQueue : D3D12Object<ID3D12CommandQueue>, IComm
: base(CreateCommandQueue(device.NativeObject, type)) : base(CreateCommandQueue(device.NativeObject, type))
{ {
Type = 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) 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); pNativeObject->ExecuteCommandLists((uint)currentIndex, ppCommandLists);
} }
public ulong Signal(ulong value) public ulong Signal(IFence fence, ulong value)
{ {
AssertNotDisposed(); AssertNotDisposed();
_fenceValue = value; var d3d12Fence = fence as D3D12Fence;
ThrowIfFailed(pNativeObject->Signal(_fence.Get(), _fenceValue)); Debug.Assert(d3d12Fence != null, "Fence must be a D3D12Fence");
return _fenceValue;
ThrowIfFailed(pNativeObject->Signal(d3d12Fence.NativeObject, value));
return value;
} }
public void WaitForValue(ulong value) public void Wait(IFence fence, ulong value)
{ {
AssertNotDisposed(); AssertNotDisposed();
if (_fence.Get()->GetCompletedValue() < value) var d3d12Fence = fence as D3D12Fence;
{ Debug.Assert(d3d12Fence != null, "Fence must be a D3D12Fence");
var handle = new HANDLE((void*)_fenceEvent.SafeWaitHandle.DangerousGetHandle());
if (_fence.Get()->SetEventOnCompletion(value, handle).SUCCEEDED) pNativeObject->Wait(d3d12Fence.NativeObject, value);
{
_fenceEvent.WaitOne();
}
}
}
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();
} }
} }

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); 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) public ICommandSignature CreateCommandSignature(ref readonly CommandSignatureDesc desc, Key128<PipelineState> pipelineKey)
{ {
Logger.DebugAssert(!_disposed); Logger.DebugAssert(!_disposed);

View File

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

View File

@@ -7,7 +7,6 @@ using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Ghost.Graphics.D3D12; namespace Ghost.Graphics.D3D12;
@@ -115,7 +114,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
private UnsafeQueue<ReleaseEntry> _releaseQueue; private UnsafeQueue<ReleaseEntry> _releaseQueue;
private int _writeLock; private readonly Lock _writeLock;
private ulong _cpuFrame; private ulong _cpuFrame;
private bool _disposed; private bool _disposed;
@@ -133,6 +132,7 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
#endif #endif
_releaseQueue = new UnsafeQueue<ReleaseEntry>(32, AllocationHandle.Persistent); _releaseQueue = new UnsafeQueue<ReleaseEntry>(32, AllocationHandle.Persistent);
_writeLock = new Lock();
} }
~D3D12ResourceDatabase() ~D3D12ResourceDatabase()
@@ -152,13 +152,10 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
return Handle<GPUResource>.Invalid; return Handle<GPUResource>.Invalid;
} }
var spinner = new SpinWait(); // 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.
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0) // 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.
spinner.SpinOnce(); lock (_writeLock)
}
try
{ {
var id = _resources.Add(new ResourceRecord(pResource, initialBarrierData, viewGroup, desc), out var generation); var id = _resources.Add(new ResourceRecord(pResource, initialBarrierData, viewGroup, desc), out var generation);
var handle = new Handle<GPUResource>(id, generation); var handle = new Handle<GPUResource>(id, generation);
@@ -173,10 +170,6 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
return handle; return handle;
} }
finally
{
Interlocked.Exchange(ref _writeLock, 0);
}
} }
internal Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null) 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; return Handle<GPUResource>.Invalid;
} }
var spinner = new SpinWait(); lock (_writeLock)
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
{ {
var id = _resources.Add(new ResourceRecord(allocation, initialBarrierData, resourceDescriptor, desc), out var generation); var id = _resources.Add(new ResourceRecord(allocation, initialBarrierData, resourceDescriptor, desc), out var generation);
var handle = new Handle<GPUResource>(id, generation); var handle = new Handle<GPUResource>(id, generation);
@@ -217,22 +204,6 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
return handle; 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) public bool HasResource(Handle<GPUResource> handle)
@@ -416,30 +387,20 @@ internal unsafe class D3D12ResourceDatabase : IResourceDatabase
return Handle<GPUResource>.Invalid; return Handle<GPUResource>.Invalid;
} }
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
{
var (srcRecord, error) = GetResourceRecord(src); var (srcRecord, error) = GetResourceRecord(src);
if (error.IsFailure) if (error.IsFailure)
{ {
return Handle<GPUResource>.Invalid; return Handle<GPUResource>.Invalid;
} }
lock (_writeLock)
{
var newRecord = srcRecord.Get(); var newRecord = srcRecord.Get();
newRecord.isShared = true; newRecord.isShared = true;
var id = _resources.Add(newRecord, out var generation); var id = _resources.Add(newRecord, out var generation);
return new Handle<GPUResource>(id, generation); return new Handle<GPUResource>(id, generation);
} }
finally
{
Interlocked.Exchange(ref _writeLock, 0);
}
} }
public void* MapResource(Handle<GPUResource> handle, uint subResource, ResourceRange? readRange) public void* MapResource(Handle<GPUResource> handle, uint subResource, ResourceRange? readRange)

View File

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

View File

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

View File

@@ -28,30 +28,18 @@ public interface ICommandQueue : IRHIObject
/// <summary> /// <summary>
/// Signals a fence with the specified Value /// Signals a fence with the specified Value
/// </summary> /// </summary>
/// <param name="fence">Fence to signal</param>
/// <param name="value">Value to signal</param> /// <param name="value">Value to signal</param>
/// <returns>The fence Value that was signaled</returns> /// <returns>The fence Value that was signaled</returns>
ulong Signal(ulong value); ulong Signal(IFence fence, ulong value);
/// <summary> /// <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> /// </summary>
/// <remarks>
/// CPU will return immediately.
/// </remarks>
/// <param name="fence">Fence to wait on</param>
/// <param name="value">Value to wait for</param> /// <param name="value">Value to wait for</param>
void WaitForValue(ulong value); void Wait(IFence fence, 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();
} }

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> /// <returns>A new swap chain instance</returns>
ISwapChain CreateSwapChain(SwapChainDesc desc); 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> /// <summary>
/// Begin the current frame. /// Begin the current frame.
/// </summary> /// </summary>

View File

@@ -33,16 +33,6 @@ public enum BindlessAccess
public unsafe interface IResourceDatabase : IDisposable 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> /// <summary>
/// Checks if a resource with the specified handle exists in the database. /// Checks if a resource with the specified handle exists in the database.
/// </summary> /// </summary>

View File

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

View File

@@ -92,6 +92,7 @@ public class RenderSystem : IDisposable
private readonly SwapChainManager _swapChainManager; private readonly SwapChainManager _swapChainManager;
private readonly ShaderLibrary _shaderLibrary; private readonly ShaderLibrary _shaderLibrary;
private readonly IFence _fence;
private readonly FrameResource[] _frameResources; private readonly FrameResource[] _frameResources;
private readonly Thread _renderThread; private readonly Thread _renderThread;
private readonly AutoResetEvent _shutdownEvent; private readonly AutoResetEvent _shutdownEvent;
@@ -181,6 +182,7 @@ public class RenderSystem : IDisposable
_swapChainManager = new SwapChainManager(_graphicsEngine); _swapChainManager = new SwapChainManager(_graphicsEngine);
_shaderLibrary = new ShaderLibrary(desc.ShaderCompilationBridge, desc.ShaderCacheDirectory); _shaderLibrary = new ShaderLibrary(desc.ShaderCompilationBridge, desc.ShaderCacheDirectory);
_fence = _graphicsEngine.CreateFence(0);
// Create frame resources for synchronization // Create frame resources for synchronization
_frameResources = new FrameResource[desc.FrameBufferCount]; _frameResources = new FrameResource[desc.FrameBufferCount];
for (var i = 0; i < desc.FrameBufferCount; i++) for (var i = 0; i < desc.FrameBufferCount; i++)
@@ -259,7 +261,7 @@ public class RenderSystem : IDisposable
continue; continue;
} }
_graphicsEngine.Device.GraphicsQueue.WaitForValue(frameResource.FenceValue); _fence.WaitForValue(frameResource.FenceValue);
if (!_resizeRequest.IsEmpty) if (!_resizeRequest.IsEmpty)
{ {
@@ -275,7 +277,7 @@ public class RenderSystem : IDisposable
} }
} }
var completedFrame = _graphicsEngine.Device.GraphicsQueue.GetCompletedValue(); var completedFrame = _fence.CompletedValue;
if (_submittedFenceValue < completedFrame) if (_submittedFenceValue < completedFrame)
{ {
_submittedFenceValue = completedFrame; _submittedFenceValue = completedFrame;
@@ -318,10 +320,10 @@ public class RenderSystem : IDisposable
} }
_submittedFenceValue++; _submittedFenceValue++;
frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(_submittedFenceValue); frameResource.FenceValue = _graphicsEngine.Device.GraphicsQueue.Signal(_fence, _submittedFenceValue);
frameResource.GpuReadyEvent.Set(); frameResource.GpuReadyEvent.Set();
completedFrame = _graphicsEngine.Device.GraphicsQueue.GetCompletedValue(); completedFrame = _fence.CompletedValue;
// End the frame and retire resources based on the freshest observed GPU progress. // End the frame and retire resources based on the freshest observed GPU progress.
_resourceManager.EndFrame(completedFrame); _resourceManager.EndFrame(completedFrame);
@@ -386,7 +388,7 @@ public class RenderSystem : IDisposable
var requiredGpuFence = _cpuFenceValue < _config.FrameBufferCount ? 0 : _cpuFenceValue - _config.FrameBufferCount + 1; 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; return false;
} }
@@ -418,7 +420,7 @@ public class RenderSystem : IDisposable
{ {
if (frameResource.FenceValue > 0) 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; public ulong retireFrame;
} }
private UnsafeList<Page> _activePages = new UnsafeList<Page>(4, AllocationHandle.Persistent); private UnsafeList<Page> _activePages = new UnsafeList<Page>(8, AllocationHandle.Persistent);
private UnsafeQueue<Page> _freePages = new UnsafeQueue<Page>(4, AllocationHandle.Persistent); private UnsafeQueue<Page> _freePages = new UnsafeQueue<Page>(8, AllocationHandle.Persistent);
private UnsafeQueue<RetiringPage> _retiringPages = new UnsafeQueue<RetiringPage>(4, 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) private static bool IsHeapFlagsCompatible(HeapFlags pageHeapFlags, HeapFlags requiredHeapFlags)
{ {
@@ -96,6 +98,9 @@ public partial class ResourceManager
var isRTOrDS = desc.Usage.HasFlag(TextureUsage.DepthStencil) || desc.Usage.HasFlag(TextureUsage.RenderTarget); var isRTOrDS = desc.Usage.HasFlag(TextureUsage.DepthStencil) || desc.Usage.HasFlag(TextureUsage.RenderTarget);
var size = _resourceAllocator.GetSizeInfo(ResourceDesc.Texture(desc)); var size = _resourceAllocator.GetSizeInfo(ResourceDesc.Texture(desc));
// TODO: Any better way?
lock (_transientWriteLock)
{
if (size.Size > DEFAULT_TRANSIENT_PAGE_SIZE) if (size.Size > DEFAULT_TRANSIENT_PAGE_SIZE)
{ {
var texHandle = _resourceAllocator.CreateTexture(in desc, name); var texHandle = _resourceAllocator.CreateTexture(in desc, name);
@@ -168,10 +173,14 @@ public partial class ResourceManager
return handle; return handle;
} }
}
public Handle<GPUBuffer> CreateTransientBuffer(ref readonly BufferDesc desc, string? name = null) public Handle<GPUBuffer> CreateTransientBuffer(ref readonly BufferDesc desc, string? name = null)
{ {
var size = _resourceAllocator.GetSizeInfo(ResourceDesc.Buffer(desc)); var size = _resourceAllocator.GetSizeInfo(ResourceDesc.Buffer(desc));
lock (_transientWriteLock)
{
if (size.Size > DEFAULT_TRANSIENT_PAGE_SIZE) if (size.Size > DEFAULT_TRANSIENT_PAGE_SIZE)
{ {
var bufHandle = _resourceAllocator.CreateBuffer(in desc, name); var bufHandle = _resourceAllocator.CreateBuffer(in desc, name);
@@ -250,6 +259,7 @@ public partial class ResourceManager
return handle; return handle;
} }
}
private void EndFramePool(ulong completedFrame) private void EndFramePool(ulong completedFrame)
{ {

View File

@@ -33,8 +33,15 @@ public sealed partial class ResourceManager : IDisposable
private readonly MaterialPaletteStore _materialPalettes; 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 ulong _submittedFrame;
private int _writeLock;
private bool _disposed; private bool _disposed;
@@ -50,6 +57,11 @@ public sealed partial class ResourceManager : IDisposable
_computeShaders = new UnsafeSlotMap<ComputeShader>(16, AllocationHandle.Persistent); _computeShaders = new UnsafeSlotMap<ComputeShader>(16, AllocationHandle.Persistent);
_materialPalettes = new MaterialPaletteStore(); _materialPalettes = new MaterialPaletteStore();
_meshWriteLock = new Lock();
_materialWriteLock = new Lock();
_shaderWriteLock = new Lock();
_computeShaderWriteLock = new Lock();
} }
~ResourceManager() ~ResourceManager()
@@ -69,18 +81,6 @@ public sealed partial class ResourceManager : IDisposable
EndFramePool(completedFrame); 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> /// <summary>
/// Creates a new mesh from the specified vertex and index data. /// Creates a new mesh from the specified vertex and index data.
/// </summary> /// </summary>
@@ -91,13 +91,7 @@ public sealed partial class ResourceManager : IDisposable
{ {
Logger.DebugAssert(!_disposed); Logger.DebugAssert(!_disposed);
var spinner = new SpinWait(); lock (_meshWriteLock)
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
{ {
var vertexBufferDesc = new BufferDesc var vertexBufferDesc = new BufferDesc
{ {
@@ -139,10 +133,6 @@ public sealed partial class ResourceManager : IDisposable
var id = _meshes.Add(mesh, out var generation); var id = _meshes.Add(mesh, out var generation);
return new Handle<Mesh>(id, generation); return new Handle<Mesh>(id, generation);
} }
finally
{
Volatile.Write(ref _writeLock, 0);
}
} }
/// <summary> /// <summary>
@@ -154,15 +144,10 @@ public sealed partial class ResourceManager : IDisposable
{ {
Logger.DebugAssert(!_disposed); Logger.DebugAssert(!_disposed);
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
{
var material = new Material(); var material = new Material();
lock (_materialWriteLock)
{
if (material.SetShader(shader, this, _resourceDatabase, _resourceAllocator) != Error.None) if (material.SetShader(shader, this, _resourceDatabase, _resourceAllocator) != Error.None)
{ {
return Handle<Material>.Invalid; return Handle<Material>.Invalid;
@@ -171,10 +156,6 @@ public sealed partial class ResourceManager : IDisposable
var id = _materials.Add(material, out var generation); var id = _materials.Add(material, out var generation);
return new Handle<Material>(id, generation); return new Handle<Material>(id, generation);
} }
finally
{
Volatile.Write(ref _writeLock, 0);
}
} }
/// <summary> /// <summary>
@@ -186,44 +167,26 @@ public sealed partial class ResourceManager : IDisposable
{ {
Logger.DebugAssert(!_disposed); Logger.DebugAssert(!_disposed);
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
{
var shader = new Shader(descriptor); var shader = new Shader(descriptor);
lock (_shaderWriteLock)
{
var id = _shaders.Add(shader, out var generation); var id = _shaders.Add(shader, out var generation);
return new Handle<Shader>(id, generation); return new Handle<Shader>(id, generation);
} }
finally
{
Volatile.Write(ref _writeLock, 0);
}
} }
public Handle<ComputeShader> CreateComputeShader(ComputeShaderDescriptor descriptor) public Handle<ComputeShader> CreateComputeShader(ComputeShaderDescriptor descriptor)
{ {
Logger.DebugAssert(!_disposed); Logger.DebugAssert(!_disposed);
var spinner = new SpinWait();
while (Interlocked.CompareExchange(ref _writeLock, 1, 0) != 0)
{
spinner.SpinOnce();
}
try
{
var computeShader = new ComputeShader(descriptor); var computeShader = new ComputeShader(descriptor);
lock (_computeShaderWriteLock)
{
var id = _computeShaders.Add(computeShader, out var generation); var id = _computeShaders.Add(computeShader, out var generation);
return new Handle<ComputeShader>(id, generation); return new Handle<ComputeShader>(id, generation);
} }
finally
{
Volatile.Write(ref _writeLock, 0);
}
} }
/// <summary> /// <summary>
@@ -261,6 +224,8 @@ public sealed partial class ResourceManager : IDisposable
{ {
Logger.DebugAssert(!_disposed); Logger.DebugAssert(!_disposed);
lock (_meshWriteLock)
{
ref var mesh = ref _meshes.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var mesh = ref _meshes.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist) if (!exist)
{ {
@@ -270,6 +235,7 @@ public sealed partial class ResourceManager : IDisposable
_meshes.Remove(handle.ID, handle.Generation); _meshes.Remove(handle.ID, handle.Generation);
mesh.ReleaseResource(_resourceDatabase); mesh.ReleaseResource(_resourceDatabase);
} }
}
/// <summary> /// <summary>
/// Determines whether a material with the specified handle exists in the collection. /// Determines whether a material with the specified handle exists in the collection.
@@ -306,6 +272,8 @@ public sealed partial class ResourceManager : IDisposable
{ {
Logger.DebugAssert(!_disposed); Logger.DebugAssert(!_disposed);
lock (_materialWriteLock)
{
ref var material = ref _materials.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var material = ref _materials.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist) if (!exist)
{ {
@@ -315,6 +283,7 @@ public sealed partial class ResourceManager : IDisposable
_materials.Remove(handle.ID, handle.Generation); _materials.Remove(handle.ID, handle.Generation);
material.ReleaseResource(_resourceDatabase); material.ReleaseResource(_resourceDatabase);
} }
}
/// <summary> /// <summary>
/// Returns an existing material palette index for the specified material sequence or creates a new one. /// Returns an existing material palette index for the specified material sequence or creates a new one.
@@ -412,6 +381,8 @@ public sealed partial class ResourceManager : IDisposable
{ {
Logger.DebugAssert(!_disposed); Logger.DebugAssert(!_disposed);
lock (_shaderWriteLock)
{
ref var shader = ref _shaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var shader = ref _shaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist) if (!exist)
{ {
@@ -421,6 +392,7 @@ public sealed partial class ResourceManager : IDisposable
_shaders.Remove(handle.ID, handle.Generation); _shaders.Remove(handle.ID, handle.Generation);
shader.ReleaseResource(_resourceDatabase); shader.ReleaseResource(_resourceDatabase);
} }
}
/// <summary> /// <summary>
/// Determines whether a compute shader with the specified identifier exists in the collection. /// Determines whether a compute shader with the specified identifier exists in the collection.
@@ -457,6 +429,8 @@ public sealed partial class ResourceManager : IDisposable
{ {
Logger.DebugAssert(!_disposed); Logger.DebugAssert(!_disposed);
lock (_computeShaderWriteLock)
{
ref var computeShader = ref _computeShaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); ref var computeShader = ref _computeShaders.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist) if (!exist)
{ {
@@ -466,6 +440,7 @@ public sealed partial class ResourceManager : IDisposable
_computeShaders.Remove(handle.ID, handle.Generation); _computeShaders.Remove(handle.ID, handle.Generation);
computeShader.ReleaseResource(_resourceDatabase); computeShader.ReleaseResource(_resourceDatabase);
} }
}
public void Dispose() 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()); var (desc, error) = resourceDatabase.GetResourceDescription(texture.AsResource());
if (error.IsFailure) 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 _); 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); var requiredSize = resourceDatabase.GetIntermediateResourceSize(texture.AsResource(), 0, 1);
if (sizeInBytes < requiredSize)
{
return Error.InvalidArgument;
}
var uploadDesc = new BufferDesc var uploadDesc = new BufferDesc
{ {
Size = requiredSize, Size = requiredSize,
@@ -133,18 +138,18 @@ public static unsafe class RenderingUtility
{ {
fixed (T* pData = data) 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 error = Error.UnknownError;
var textureHandle = resourceAllocator.CreateTexture(in desc, name); var textureHandle = resourceAllocator.CreateTexture(in desc, name);
if (!textureHandle.IsInvalid) if (!textureHandle.IsInvalid)
{ {
error = UploadTexture(resourceManager, resourceDatabase, cmd, textureHandle, pData); error = UploadTexture(resourceManager, resourceDatabase, cmd, textureHandle, pData, sizeInBytes);
} }
if (error.IsSuccess) if (error.IsSuccess)
@@ -161,7 +166,7 @@ public static unsafe class RenderingUtility
{ {
fixed (T* pData = data) 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);
} }
} }
} }