using System.Runtime.CompilerServices; using Win32; using Win32.Graphics.Direct3D12; namespace Ghost.Graphics.D3D12; /// /// Resource upload batch for efficiently uploading resources to GPU memory /// internal unsafe class ResourceUploadBatch : IDisposable { private ComPtr _commandAllocator; private ComPtr _commandList; private ComPtr _fence; private readonly GraphicsDevice _device = GraphicsPipeline.GraphicsDevice; private readonly AutoResetEvent _fenceEvent = new(false); private ulong _fenceValue; private bool _isRecording; private bool _disposed; /// /// Gets whether the batch is currently recording commands /// public bool IsRecording => _isRecording; /// /// Creates a new ResourceUploadBatch /// internal ResourceUploadBatch() { Initialize(); } ~ResourceUploadBatch() { Dispose(); } private void Initialize() { ThrowIfFailed(_device.NativeDevice.Ptr->CreateCommandAllocator( CommandListType.Direct, __uuidof(), _commandAllocator.GetVoidAddressOf())); ThrowIfFailed(_device.NativeDevice.Ptr->CreateCommandList1( 0, CommandListType.Direct, CommandListFlags.None, __uuidof(), _commandList.GetVoidAddressOf())); ThrowIfFailed(_device.NativeDevice.Ptr->CreateFence(0, FenceFlags.None, __uuidof(), _fence.GetVoidAddressOf())); } /// /// Begins recording upload commands /// public void Begin() { if (_isRecording) { throw new InvalidOperationException("Upload batch is already recording"); } ThrowIfFailed(_commandAllocator.Get()->Reset()); ThrowIfFailed(_commandList.Get()->Reset(_commandAllocator.Get(), null)); _isRecording = true; } /// /// Uploads buffer data to a resource /// /// Type of data to upload /// Destination resource /// Source data public void Upload(ID3D12Resource* resource, ReadOnlySpan data) where T : unmanaged { if (!_isRecording) { throw new InvalidOperationException("Upload batch is not recording"); } var sizeInBytes = (uint)(data.Length * sizeof(T)); var uploadBuffer = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer(sizeInBytes, true); void* mappedData; var uploadResource = uploadBuffer.ResourceHandle.GetAllocation().Resource; uploadResource->Map(0, null, &mappedData); fixed (T* dataPtr = data) { Unsafe.CopyBlock(mappedData, dataPtr, sizeInBytes); } uploadResource->Unmap(0, null); // Copy from upload buffer to destination _commandList.Get()->CopyBufferRegion( resource, 0, uploadResource, 0, sizeInBytes); } /// /// Uploads subresource data to a texture /// /// Destination texture resource /// First subresource index /// Subresource data array /// Number of subresources public void Upload(ID3D12Resource* resource, uint firstSubresource, SubresourceData* subresources, uint numSubresources) { if (!_isRecording) { throw new InvalidOperationException("Upload batch is not recording"); } var resourceDesc = resource->GetDesc(); var requiredSize = GetRequiredIntermediateSize(resource, firstSubresource, numSubresources); var uploadBuffer = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer((uint)requiredSize, true); UpdateSubresources( resource, uploadBuffer.ResourceHandle.GetAllocation().Resource, 0, firstSubresource, numSubresources, subresources); } /// /// Adds a resource transition barrier /// /// Resource to transition /// State before transition /// State after transition public void Transition(ID3D12Resource* resource, ResourceStates stateBefore, ResourceStates stateAfter) { if (!_isRecording) { throw new InvalidOperationException("Upload batch is not recording"); } _commandList.Get()->ResourceBarrierTransition(resource, stateBefore, stateAfter); } /// /// Generates mipmaps for a texture (if supported) /// /// Texture resource public void GenerateMips(GraphicsResource resource) { if (!_isRecording) { throw new InvalidOperationException("Upload batch is not recording"); } // This would require compute shader implementation for mipmap generation // For now, this is a placeholder - DirectXTK12 uses a compute shader approach throw new NotImplementedException("Mipmap generation not yet implemented"); } /// /// Ends recording and submits the batch for execution /// /// Future that completes when upload is finished public ulong End() { if (!_isRecording) { throw new InvalidOperationException("Upload batch is not recording"); } ThrowIfFailed(_commandList.Get()->Close()); _device.CommandQueue.Ptr->ExecuteCommandLists(1, (ID3D12CommandList**)_commandList.GetAddressOf()); ThrowIfFailed(_device.CommandQueue.Ptr->Signal(_fence.Get(), ++_fenceValue)); _isRecording = false; return _fenceValue; } /// /// Waits for the upload batch to complete /// /// Fence value to wait for public void WaitForCompletion(ulong fenceValue) { if (_fence.Get()->GetCompletedValue() < fenceValue) { var handle = new Handle((void*)_fenceEvent.SafeWaitHandle.DangerousGetHandle()); ThrowIfFailed(_fence.Get()->SetEventOnCompletion(fenceValue, handle)); _fenceEvent.WaitOne(); } } private ulong GetRequiredIntermediateSize(ID3D12Resource* destinationResource, uint firstSubresource, uint numSubresources) { var resourceDesc = destinationResource->GetDesc(); ulong requiredSize = 0; var numRows = stackalloc uint[(int)numSubresources]; var rowSizeInBytes = stackalloc ulong[(int)numSubresources]; _device.NativeDevice.Ptr->GetCopyableFootprints( &resourceDesc, firstSubresource, numSubresources, 0, null, numRows, rowSizeInBytes, &requiredSize); return requiredSize; } private void UpdateSubresources(ID3D12Resource* destinationResource, ID3D12Resource* intermediate, ulong intermediateOffset, uint firstSubresource, uint numSubresources, SubresourceData* pSubresourceData) { var destDesc = destinationResource->GetDesc(); var layouts = stackalloc PlacedSubresourceFootprint[(int)numSubresources]; var numRows = stackalloc uint[(int)numSubresources]; var rowSizeInBytes = stackalloc ulong[(int)numSubresources]; ulong requiredSize = 0; _device.NativeDevice.Ptr->GetCopyableFootprints( &destDesc, firstSubresource, numSubresources, intermediateOffset, layouts, numRows, rowSizeInBytes, &requiredSize); void* pMappedData; ThrowIfFailed(intermediate->Map(0, null, &pMappedData)); for (uint i = 0; i < numSubresources; i++) { var srcData = pSubresourceData[i]; var destLayout = layouts[i]; var pDestSlice = (byte*)pMappedData + destLayout.Offset; var pSrcSlice = (byte*)srcData.pData; for (uint y = 0; y < numRows[i]; y++) { var pDestRow = pDestSlice + (y * destLayout.Footprint.RowPitch); var pSrcRow = pSrcSlice + (y * srcData.RowPitch); Unsafe.CopyBlockUnaligned(pDestRow, pSrcRow, (uint)rowSizeInBytes[i]); } } intermediate->Unmap(0, null); if (destDesc.Dimension == ResourceDimension.Buffer) { _commandList.Get()->CopyBufferRegion(destinationResource, 0, intermediate, intermediateOffset, requiredSize); } else { for (uint i = 0; i < numSubresources; i++) { var destLocation = new TextureCopyLocation(destinationResource, firstSubresource + i); var srcLocation = new TextureCopyLocation(intermediate, in layouts[i]); var box = new Box { left = 0, top = 0, front = 0, right = (uint)destDesc.Width, bottom = destDesc.Height, back = destDesc.DepthOrArraySize }; _commandList.Get()->CopyTextureRegion(&destLocation, 0, 0, 0, &srcLocation, &box); } } } public void Dispose() { if (_disposed) { return; } if (_isRecording) { _commandList.Get()->Close(); _isRecording = false; } _fence.Dispose(); _commandList.Dispose(); _commandAllocator.Dispose(); _fenceEvent.Dispose(); _disposed = true; GC.SuppressFinalize(this); } }