Files
GhostEngine/src/Runtime/Ghost.Graphics.D3D12/D3D12ResourceDatabase.cs

548 lines
16 KiB
C#

using Ghost.Core;
using Ghost.Graphics.D3D12.Utilities;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Diagnostics;
using System.Runtime.InteropServices;
using TerraFX.Interop.DirectX;
namespace Ghost.Graphics.D3D12;
internal unsafe class D3D12ResourceDatabase : IResourceDatabase
{
internal unsafe record struct ResourceRecord
{
[StructLayout(LayoutKind.Explicit)]
public struct __resource_union
{
[FieldOffset(0)]
public UniquePtr<D3D12MA_Allocation> allocation;
[FieldOffset(0)]
public UniquePtr<ID3D12Resource> resource;
public __resource_union(D3D12MA_Allocation* allocation)
{
this.allocation = allocation;
}
public __resource_union(ID3D12Resource* resource)
{
this.resource = resource;
}
}
public ResourceDesc desc;
public ResourceViewGroup viewGroup;
public __resource_union resource;
public ResourceBarrierData barrierData;
public bool isExternal;
public bool isShared;
public readonly bool Allocated => isExternal ? resource.resource.Get() != null : resource.allocation.Get() != null;
public readonly SharedPtr<ID3D12Resource> ResourcePtr => isExternal ? resource.resource.Get() : resource.allocation.Get()->GetResource();
public ResourceRecord(D3D12MA_Allocation* allocation, ResourceBarrierData barrierData, ResourceViewGroup viewGroup, ResourceDesc desc)
{
this.resource = new __resource_union(allocation);
this.isExternal = false;
this.isShared = false;
this.viewGroup = viewGroup;
this.barrierData = barrierData;
this.desc = desc;
}
public ResourceRecord(ID3D12Resource* resource, ResourceBarrierData barrierData, ResourceViewGroup viewGroup, ResourceDesc desc)
{
this.resource = new __resource_union(resource);
this.isExternal = true;
this.isShared = false;
this.viewGroup = viewGroup;
this.barrierData = barrierData;
this.desc = desc;
}
public readonly uint Release(D3D12DescriptorAllocator descriptorAllocator)
{
if (isShared)
{
return 0;
}
var refCount = 0u;
if (Allocated)
{
if (isExternal)
{
refCount = resource.resource.Get()->Release();
}
else
{
refCount = resource.allocation.Get()->Release();
}
}
descriptorAllocator.Release(viewGroup);
return refCount;
}
}
private readonly struct ReleaseEntry
{
public readonly ResourceRecord record;
public readonly ulong fenceValue;
public ReleaseEntry(ResourceRecord record, ulong fenceValue)
{
this.record = record;
this.fenceValue = fenceValue;
}
}
private readonly D3D12RenderDevice _device;
private readonly D3D12DescriptorAllocator _descriptorAllocator;
private UnsafeSlotMap<ResourceRecord> _resources;
private UnsafeHashMap<SamplerDesc, Identifier<Sampler>> _samplers;
#if GHOST_EDITOR
private readonly Dictionary<Handle<GPUResource>, string> _resourceName;
#endif
private UnsafeQueue<ReleaseEntry> _releaseQueue;
private readonly Lock _writeLock;
private ulong _cpuFrame;
private bool _disposed;
public ResourceBarrierData globalBarrier;
public D3D12ResourceDatabase(D3D12RenderDevice device, D3D12DescriptorAllocator descriptorAllocator)
{
_device = device;
_descriptorAllocator = descriptorAllocator;
_resources = new UnsafeSlotMap<ResourceRecord>(64, AllocationHandle.Persistent, AllocationOption.Clear);
_samplers = new UnsafeHashMap<SamplerDesc, Identifier<Sampler>>(32, AllocationHandle.Persistent);
#if GHOST_EDITOR
_resourceName = new Dictionary<Handle<GPUResource>, string>(64);
#endif
_releaseQueue = new UnsafeQueue<ReleaseEntry>(32, AllocationHandle.Persistent);
_writeLock = new Lock();
}
~D3D12ResourceDatabase()
{
Dispose();
}
internal Handle<GPUResource> ImportExternalResource(ID3D12Resource* pResource, ResourceBarrierData initialBarrierData, ResourceViewGroup viewGroup, ResourceDesc desc, string? name = null)
{
Logger.DebugAssert(!_disposed);
if (pResource == null)
{
#if DEBUG
Debugger.Break();
#endif
return Handle<GPUResource>.Invalid;
}
// 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);
#if GHOST_EDITOR
if (!string.IsNullOrEmpty(name))
{
pResource->SetName(name);
_resourceName[handle] = name;
}
#endif
return handle;
}
}
internal Handle<GPUResource> AddAllocation(D3D12MA_Allocation* allocation, ResourceBarrierData initialBarrierData, ResourceViewGroup resourceDescriptor, ResourceDesc desc, string? name = null)
{
Logger.DebugAssert(!_disposed);
if (allocation == null)
{
#if DEBUG
Debugger.Break();
#endif
return Handle<GPUResource>.Invalid;
}
lock (_writeLock)
{
var id = _resources.Add(new ResourceRecord(allocation, initialBarrierData, resourceDescriptor, desc), out var generation);
var handle = new Handle<GPUResource>(id, generation);
#if GHOST_EDITOR
if (!string.IsNullOrEmpty(name))
{
allocation->SetName(name);
var pResource = allocation->GetResource();
if (pResource != null)
{
pResource->SetName(name);
}
_resourceName[handle] = name;
}
#endif
return handle;
}
}
public bool HasResource(Handle<GPUResource> handle)
{
Logger.DebugAssert(!_disposed);
return _resources.Contains(handle.ID, handle.Generation);
}
public RefResult<ResourceRecord, Error> GetResourceRecord(Handle<GPUResource> handle)
{
Logger.DebugAssert(!_disposed);
ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist)
{
return Error.NotFound;
}
return RefResult<ResourceRecord, Error>.Success(ref info);
}
public SharedPtr<ID3D12Resource> GetResource(Handle<GPUResource> handle)
{
var r = GetResourceRecord(handle);
if (r.IsFailure)
{
return null;
}
return r.Value.ResourcePtr;
}
public Result<ResourceBarrierData, Error> GetResourceBarrierData(Handle<GPUResource> handle)
{
var r = GetResourceRecord(handle);
if (r.IsFailure)
{
return r.Error;
}
return r.Value.barrierData;
}
public Error SetResourceBarrierData(Handle<GPUResource> handle, ResourceBarrierData data)
{
var r = GetResourceRecord(handle);
if (r.IsFailure)
{
return r.Error;
}
r.Value.barrierData = data;
return Error.None;
}
public Result<ResourceDesc, Error> GetResourceDescription(Handle<GPUResource> handle)
{
var r = GetResourceRecord(handle);
if (r.IsFailure)
{
return r.Error;
}
return r.Value.desc;
}
public uint GetBindlessIndex(Handle<GPUResource> handle, BindlessAccess access = BindlessAccess.ShaderResource)
{
var r = GetResourceRecord(handle);
if (r.IsFailure || !r.Value.Allocated)
{
return uint.MaxValue;
}
return access switch
{
BindlessAccess.ShaderResource => (uint)r.Value.viewGroup.srv.Value,
BindlessAccess.ConstantBuffer => (uint)r.Value.viewGroup.cbv.Value,
BindlessAccess.UnorderedAccess => (uint)r.Value.viewGroup.uav.Value,
_ => uint.MaxValue,
};
}
public string? GetResourceName(Handle<GPUResource> handle)
{
Logger.DebugAssert(!_disposed);
#if GHOST_EDITOR
if (_resourceName.TryGetValue(handle, out var name))
{
return name;
}
#endif
return null;
}
public void ReleaseResource(Handle<GPUResource> handle)
{
Logger.DebugAssert(!_disposed);
if (!_resources.TryGetElementAt(handle.ID, handle.Generation, out var record))
{
return;
}
var entry = new ReleaseEntry(record, _cpuFrame);
_releaseQueue.Enqueue(entry);
_resources.Remove(handle.ID, handle.Generation);
#if GHOST_EDITOR
_resourceName.Remove(handle, out _);
#endif
}
public void ReleaseResourceImmediately(Handle<GPUResource> handle)
{
Logger.DebugAssert(!_disposed);
ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist);
if (!exist || !info.Allocated)
{
return;
}
info.Release(_descriptorAllocator);
_resources.Remove(handle.ID, handle.Generation);
}
public Identifier<Sampler> AddSampler(ref readonly SamplerDesc desc, int id)
{
Logger.DebugAssert(!_disposed);
if (_samplers.ContainsKey(desc))
{
throw new InvalidOperationException("Sampler already exists.");
}
var identifier = new Identifier<Sampler>(id);
_samplers.Add(desc, identifier);
return identifier;
}
public bool TryGetSampler(ref readonly SamplerDesc desc, out Identifier<Sampler> id)
{
Logger.DebugAssert(!_disposed);
return _samplers.TryGetValue(desc, out id);
}
public void ReleaseSampler(Identifier<Sampler> id)
{
Logger.DebugAssert(!_disposed);
// NOTE: We almost never release samplers individually, because they are cheap and can be reused.
// Ideally we would release all samplers at once when disposing the ResourceDatabase.
_descriptorAllocator.Release(new Identifier<SamplerDescriptor>(id.Value));
}
public Error Swap(Handle<GPUResource> handleA, Handle<GPUResource> handleB)
{
ref var recordA = ref _resources.GetElementReferenceAt(handleA.ID, handleA.Generation, out var existA);
ref var recordB = ref _resources.GetElementReferenceAt(handleB.ID, handleB.Generation, out var existB);
if (!existA || !existB)
{
return Error.NotFound;
}
// ViewGroups are pinned to their slots — save before swap
var viewA = recordA.viewGroup;
var viewB = recordB.viewGroup;
var temp = recordA;
recordA = recordB;
recordB = temp;
// Restore viewGroups to their original slots
recordA.viewGroup = viewA;
recordB.viewGroup = viewB;
D3D12Utility.CreateResourceDescriptor(_device, _descriptorAllocator, recordA.desc, recordA.ResourcePtr, viewA);
D3D12Utility.CreateResourceDescriptor(_device, _descriptorAllocator, recordB.desc, recordB.ResourcePtr, viewB);
return Error.None;
}
public Handle<GPUResource> Replace(Handle<GPUResource> dst, Handle<GPUResource> src)
{
ref var recordDst = ref _resources.GetElementReferenceAt(dst.ID, dst.Generation, out var existDst);
ref var recordSrc = ref _resources.GetElementReferenceAt(src.ID, src.Generation, out var existSrc);
if (!existDst || !existSrc)
{
return Handle<GPUResource>.Invalid;
}
var dstView = recordDst.viewGroup;
var srcView = recordSrc.viewGroup;
var temp = recordDst;
recordDst = recordSrc;
recordSrc = temp;
// Pin viewGroups back
recordDst.viewGroup = dstView;
recordSrc.viewGroup = srcView;
// Update dst's descriptor to point to the new resource
D3D12Utility.CreateResourceDescriptor(_device, _descriptorAllocator, recordDst.desc, recordDst.ResourcePtr, dstView);
ReleaseResource(src);
return dst;
}
public Handle<GPUResource> CreateShared(Handle<GPUResource> src)
{
if (src.IsInvalid)
{
return Handle<GPUResource>.Invalid;
}
var (srcRecord, error) = GetResourceRecord(src);
if (error.IsFailure)
{
return Handle<GPUResource>.Invalid;
}
lock (_writeLock)
{
var newRecord = srcRecord.Get();
newRecord.isShared = true;
var id = _resources.Add(newRecord, out var generation);
return new Handle<GPUResource>(id, generation);
}
}
public Handle<GPUResource> CreateEmpty()
{
lock (_writeLock)
{
var id = _resources.Add(default, out var generation);
return new Handle<GPUResource>(id, generation);
}
}
public void* MapResource(Handle<GPUResource> handle, uint subResource, ResourceRange? readRange)
{
var r = GetResourceRecord(handle);
if (r.IsFailure)
{
return null;
}
var resource = r.Value.ResourcePtr;
var rRange = readRange.HasValue ? new D3D12_RANGE { Begin = readRange.Value.Start, End = readRange.Value.End } : default;
void* mappedData = null;
resource.Get()->Map(subResource, readRange.HasValue ? &rRange : null, &mappedData);
return mappedData;
}
public Error UnmapResource(Handle<GPUResource> handle, uint subResource, ResourceRange? writtenRange)
{
var r = GetResourceRecord(handle);
if (r.IsFailure)
{
return r.Error;
}
var resource = r.Value.ResourcePtr;
var wRange = writtenRange.HasValue ? new D3D12_RANGE { Begin = writtenRange.Value.Start, End = writtenRange.Value.End } : default;
resource.Get()->Unmap(subResource, writtenRange.HasValue ? &wRange : null);
return Error.None;
}
public ulong GetIntermediateResourceSize(Handle<GPUResource> resource, uint firstSubResource, uint numSubResources)
{
var r = GetResourceRecord(resource);
if (r.IsFailure)
{
return 0;
}
return GetRequiredIntermediateSize(r.Value.ResourcePtr.Get(), firstSubResource, numSubResources);
}
internal void BeginFrame(ulong cpuFrame)
{
Logger.DebugAssert(!_disposed);
_cpuFrame = cpuFrame;
}
internal void EndFrame(ulong gpuFrame)
{
Logger.DebugAssert(!_disposed);
while (_releaseQueue.TryPeek(out var toRelease) && toRelease.fenceValue < gpuFrame)
{
_releaseQueue.Dequeue();
toRelease.record.Release(_descriptorAllocator);
}
}
internal void ReleaseAllResourcesImmediately()
{
Logger.DebugAssert(!_disposed);
foreach (ref var entry in _releaseQueue)
{
entry.record.Release(_descriptorAllocator);
}
foreach (ref var record in _resources)
{
record.Release(_descriptorAllocator);
}
_resources.Clear();
}
public void Dispose()
{
if (_disposed)
{
return;
}
_resources.Dispose();
_samplers.Dispose();
_releaseQueue.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}