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 allocation; [FieldOffset(0)] public UniquePtr 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 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 _resources; private UnsafeHashMap> _samplers; #if GHOST_EDITOR private readonly Dictionary, string> _resourceName; #endif private UnsafeQueue _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(64, AllocationHandle.Persistent, AllocationOption.Clear); _samplers = new UnsafeHashMap>(32, AllocationHandle.Persistent); #if GHOST_EDITOR _resourceName = new Dictionary, string>(64); #endif _releaseQueue = new UnsafeQueue(32, AllocationHandle.Persistent); _writeLock = new Lock(); } ~D3D12ResourceDatabase() { Dispose(); } internal Handle 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.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(id, generation); #if GHOST_EDITOR if (!string.IsNullOrEmpty(name)) { pResource->SetName(name); _resourceName[handle] = name; } #endif return handle; } } internal Handle 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.Invalid; } lock (_writeLock) { var id = _resources.Add(new ResourceRecord(allocation, initialBarrierData, resourceDescriptor, desc), out var generation); var handle = new Handle(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 handle) { Logger.DebugAssert(!_disposed); return _resources.Contains(handle.ID, handle.Generation); } public RefResult GetResourceRecord(Handle handle) { Logger.DebugAssert(!_disposed); ref var info = ref _resources.GetElementReferenceAt(handle.ID, handle.Generation, out var exist); if (!exist) { return Error.NotFound; } return RefResult.Success(ref info); } public SharedPtr GetResource(Handle handle) { var r = GetResourceRecord(handle); if (r.IsFailure) { return null; } return r.Value.ResourcePtr; } public Result GetResourceBarrierData(Handle handle) { var r = GetResourceRecord(handle); if (r.IsFailure) { return r.Error; } return r.Value.barrierData; } public Error SetResourceBarrierData(Handle handle, ResourceBarrierData data) { var r = GetResourceRecord(handle); if (r.IsFailure) { return r.Error; } r.Value.barrierData = data; return Error.None; } public Result GetResourceDescription(Handle handle) { var r = GetResourceRecord(handle); if (r.IsFailure) { return r.Error; } return r.Value.desc; } public uint GetBindlessIndex(Handle 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 handle) { Logger.DebugAssert(!_disposed); #if GHOST_EDITOR if (_resourceName.TryGetValue(handle, out var name)) { return name; } #endif return null; } public void ReleaseResource(Handle 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 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 AddSampler(ref readonly SamplerDesc desc, int id) { Logger.DebugAssert(!_disposed); if (_samplers.ContainsKey(desc)) { throw new InvalidOperationException("Sampler already exists."); } var identifier = new Identifier(id); _samplers.Add(desc, identifier); return identifier; } public bool TryGetSampler(ref readonly SamplerDesc desc, out Identifier id) { Logger.DebugAssert(!_disposed); return _samplers.TryGetValue(desc, out id); } public void ReleaseSampler(Identifier 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(id.Value)); } public Error Swap(Handle handleA, Handle 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 Replace(Handle dst, Handle 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.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 CreateShared(Handle src) { if (src.IsInvalid) { return Handle.Invalid; } var (srcRecord, error) = GetResourceRecord(src); if (error.IsFailure) { return Handle.Invalid; } lock (_writeLock) { var newRecord = srcRecord.Get(); newRecord.isShared = true; var id = _resources.Add(newRecord, out var generation); return new Handle(id, generation); } } public Handle CreateEmpty() { lock (_writeLock) { var id = _resources.Add(default, out var generation); return new Handle(id, generation); } } public void* MapResource(Handle 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 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 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); } }