diff --git a/Ghost.Core/Ghost.Core.csproj b/Ghost.Core/Ghost.Core.csproj index 65d5c8c..489a212 100644 --- a/Ghost.Core/Ghost.Core.csproj +++ b/Ghost.Core/Ghost.Core.csproj @@ -9,7 +9,7 @@ - ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll + ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.LowLevel\bin\Release\net9.0\Misaki.HighPerformance.LowLevel.dll diff --git a/Ghost.Editor/Ghost.Editor.csproj b/Ghost.Editor/Ghost.Editor.csproj index a28fcaf..a825bb6 100644 --- a/Ghost.Editor/Ghost.Editor.csproj +++ b/Ghost.Editor/Ghost.Editor.csproj @@ -102,7 +102,7 @@ - ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll + ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.LowLevel\bin\Release\net9.0\Misaki.HighPerformance.LowLevel.dll diff --git a/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml.cs b/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml.cs index a3d3fb7..e8cb3fb 100644 --- a/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml.cs +++ b/Ghost.Editor/View/Pages/EngineEditor/ScenePage.xaml.cs @@ -1,6 +1,7 @@ using Ghost.Editor.Controls.Internal; using Ghost.Graphics; using Ghost.Graphics.Contracts; +using Ghost.Graphics.D3D12; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using WinRT; @@ -9,7 +10,7 @@ namespace Ghost.Editor.View.Pages.EngineEditor; internal sealed partial class ScenePage : NavigationTabPage { - private IRenderer? _renderView; + private Renderer? _renderView; private ISwapChainPanelNative _swapChainPanelNative; public ScenePage() diff --git a/Ghost.Engine/EngineCore.cs b/Ghost.Engine/EngineCore.cs index 9bbaf75..fadca99 100644 --- a/Ghost.Engine/EngineCore.cs +++ b/Ghost.Engine/EngineCore.cs @@ -1,7 +1,6 @@ using Ghost.Engine.Models; using Ghost.Engine.Services; using Ghost.Graphics; -using Ghost.Graphics.Data; namespace Ghost.Engine; @@ -11,7 +10,7 @@ internal class EngineCore { ActivationHandler.Handle(args); - GraphicsPipeline.Initialize(GraphicsAPI.D3D12); + GraphicsPipeline.Initialize(); GraphicsPipeline.Start(); Logger.LogInfo("Engine started successfully."); diff --git a/Ghost.Engine/Services/Logger.cs b/Ghost.Engine/Services/Logger.cs index 61a3c83..bc93783 100644 --- a/Ghost.Engine/Services/Logger.cs +++ b/Ghost.Engine/Services/Logger.cs @@ -1,5 +1,6 @@ using Ghost.Engine.Models; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Ghost.Engine.Services; @@ -89,7 +90,7 @@ public static class Logger LogExceptionInternal(ex); } - public static void Assert(bool condition, object? message = null) + public static void Assert([DoesNotReturnIf(false)] bool condition, object? message = null) { if (!condition) { diff --git a/Ghost.Graphics/Contracts/ICommandBuffer.cs b/Ghost.Graphics/Contracts/ICommandBuffer.cs deleted file mode 100644 index 6757fed..0000000 --- a/Ghost.Graphics/Contracts/ICommandBuffer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Ghost.Graphics.Data; -using Win32.Graphics.Direct3D12; - -namespace Ghost.Graphics.Contracts; - -public interface ICommandBuffer -{ - // TODO: They should be internal, maybe an interface ICommandBufferInternal? - public void BarrierTransition(IResource resource, ResourceStates beforeState, ResourceStates afterState); - public void SetGraphicsRootConstantBufferView(uint slot, ulong gpuAddress); - - public void DrawMesh(Mesh mesh, Material material); - public void CopyResource(IResource dstResource, uint dstOffset, IResource srcResource, uint srcOffset, uint size); -} \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IDebugLayer.cs b/Ghost.Graphics/Contracts/IDebugLayer.cs deleted file mode 100644 index d8ddafb..0000000 --- a/Ghost.Graphics/Contracts/IDebugLayer.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Ghost.Graphics.Contracts; - -internal interface IDebugLayer : IDisposable -{ -} \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IGraphicsDevice.cs b/Ghost.Graphics/Contracts/IGraphicsDevice.cs deleted file mode 100644 index 55fa65f..0000000 --- a/Ghost.Graphics/Contracts/IGraphicsDevice.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Ghost.Graphics.Data; - -namespace Ghost.Graphics.Contracts; - -internal interface IGraphicsDevice : IDisposable -{ - public static abstract GraphicsAPI TargetAPI - { - get; - } - - public ReadOnlySpan Renderers - { - get; - } - - public IRenderer CreateRenderer(in SwapChainPresenter swapChainSurface); - public void RemoveRenderer(IRenderer renderer); - public void InitializePendingRenderers(); -} \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IPipelineResource.cs b/Ghost.Graphics/Contracts/IPipelineResource.cs deleted file mode 100644 index 9068741..0000000 --- a/Ghost.Graphics/Contracts/IPipelineResource.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Ghost.Graphics.Contracts; - -internal interface IPipelineResource : IDisposable -{ -} \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IRenderPass.cs b/Ghost.Graphics/Contracts/IRenderPass.cs index 184e21e..d3b4fd5 100644 --- a/Ghost.Graphics/Contracts/IRenderPass.cs +++ b/Ghost.Graphics/Contracts/IRenderPass.cs @@ -1,7 +1,9 @@ -namespace Ghost.Graphics.Contracts; +using Ghost.Graphics.D3D12; + +namespace Ghost.Graphics.Contracts; internal interface IRenderPass : IDisposable { - void Initialize(ICommandBuffer cmb); - void Execute(ICommandBuffer cmb); + void Initialize(CommandList cmd); + void Execute(CommandList cmd); } \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IRenderer.cs b/Ghost.Graphics/Contracts/IRenderer.cs deleted file mode 100644 index 52a4dd7..0000000 --- a/Ghost.Graphics/Contracts/IRenderer.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Ghost.Graphics.Contracts; - -/// -/// Defines the contract for a render view in the graphics pipeline. -/// -internal interface IRenderer : IDisposable -{ - public ReadOnlySpan RenderPasses - { - get; - } - - /// - /// Requests a resize of the render view. - /// - /// The new width of the render view. - /// The new height of the render view. - /// This only submits a resize request without executing it. May overwrite last request if next request issued before next frame. - public void RequestResize(uint width, uint height); - /// - /// Executes any pending resize operations for the current context. - /// - public void ExecutePendingResize(); - - public void Initialize(); - /// - /// Renders the current content to the output target. - /// - public void Render(); - - /// - /// Waits for the next frame to be ready for rendering. - /// - public void WaitNextFrame(); - /// - /// Waits for the render view to become idle, ensuring all previous commands have been executed and resources are ready for the next frame. - /// - public void WaitIdle(); -} \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IResource.cs b/Ghost.Graphics/Contracts/IResource.cs deleted file mode 100644 index c062e34..0000000 --- a/Ghost.Graphics/Contracts/IResource.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Misaki.HighPerformance.LowLevel.Collections; - -namespace Ghost.Graphics.Contracts; - -public unsafe interface IResource : IDisposable -{ - public ulong GPUAddress - { - get; - } - - public string Name - { - get; - set; - } - - public bool TempResource - { - get; - } - - public void SetData(Span data) - where T : unmanaged; - - public void SetData(T* data, uint length) - where T : unmanaged; - - public void SetData(void* data, uint size); - - public UnsafeArray ReadData(Allocator allocator) - where T : unmanaged; - - public void ReadData(T* ppData, uint* size) - where T : unmanaged; - - public void ReadData(void* ppData, uint* size); -} \ No newline at end of file diff --git a/Ghost.Graphics/Contracts/IResourceAllocator.cs b/Ghost.Graphics/Contracts/IResourceAllocator.cs deleted file mode 100644 index 5df866c..0000000 --- a/Ghost.Graphics/Contracts/IResourceAllocator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Win32.Graphics.Direct3D12; - -namespace Ghost.Graphics.Contracts; - -internal unsafe interface IResourceAllocator : IDisposable -{ - public IResource CreateUploadBuffer(uint sizeInBytes, bool tempResource = false, ResourceFlags flags = ResourceFlags.None); - public IResource CreateCopyDestinationBuffer(uint sizeInBytes, bool tempResource = false, ResourceFlags flags = ResourceFlags.None); - - public void ReleaseTempResource(); -} \ No newline at end of file diff --git a/Ghost.Graphics/D3D12/CommandList.cs b/Ghost.Graphics/D3D12/CommandList.cs new file mode 100644 index 0000000..3816838 --- /dev/null +++ b/Ghost.Graphics/D3D12/CommandList.cs @@ -0,0 +1,59 @@ +using Ghost.Core; +using Ghost.Graphics.Data; +using Win32.Graphics.Direct3D; +using Win32.Graphics.Direct3D12; + +namespace Ghost.Graphics.D3D12; + +public unsafe class CommandList +{ + private readonly ConstPtr _commandList; + + internal ConstPtr NativeCommandList => _commandList; + + public CommandList(ID3D12GraphicsCommandList10* commandList) + { + _commandList = commandList; + } + + public void BarrierTransition(GraphicsResource resource, ResourceStates beforeState, ResourceStates afterState) + { + _commandList.Ptr->ResourceBarrierTransition(resource.NativeResource.Ptr, beforeState, afterState); + } + + public void SetGraphicsRootConstantBufferView(uint slot, ulong gpuAddress) + { + _commandList.Ptr->SetGraphicsRootConstantBufferView(slot, gpuAddress); + } + + public void DrawMesh(Mesh mesh, Material material) + { + _commandList.Ptr->SetGraphicsRootSignature(material.Shader.RootSignature); + _commandList.Ptr->SetPipelineState(material.Shader.PipelineState); + + // Bind shader-visible descriptor heaps before setting descriptor tables + if (material.Shader.Textures.Count > 0) + { + var shaderVisibleHeaps = GraphicsPipeline.DescriptorAllocator.GetShaderVisibleHeaps(); + var heapPtrs = stackalloc ID3D12DescriptorHeap*[shaderVisibleHeaps.Length]; + for (var i = 0; i < shaderVisibleHeaps.Length; i++) + { + heapPtrs[i] = shaderVisibleHeaps[i].Ptr; + } + _commandList.Ptr->SetDescriptorHeaps((uint)shaderVisibleHeaps.Length, heapPtrs); + } + + material.Bind(this); + + _commandList.Ptr->IASetPrimitiveTopology(PrimitiveTopology.TriangleList); + _commandList.Ptr->IASetVertexBuffers(0, 1, mesh.VertexBufferView); + _commandList.Ptr->IASetIndexBuffer(mesh.IndexBufferView); + + _commandList.Ptr->DrawIndexedInstanced(mesh.IndexCount, 1, 0, 0, 0); + } + + public void CopyResource(GraphicsResource dstResource, uint dstOffset, GraphicsResource srcResource, uint srcOffset, uint size) + { + _commandList.Ptr->CopyBufferRegion(dstResource.NativeResource, dstOffset, srcResource.NativeResource, srcOffset, size); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs b/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs deleted file mode 100644 index 4ee6a9c..0000000 --- a/Ghost.Graphics/D3D12/D3D12CommandBuffer.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Ghost.Core; -using Ghost.Graphics.Contracts; -using Ghost.Graphics.Data; -using Win32.Graphics.Direct3D; -using Win32.Graphics.Direct3D12; - -namespace Ghost.Graphics.D3D12; - -internal unsafe class D3D12CommandBuffer : ICommandBuffer -{ - private readonly ConstPtr _commandList; - - internal ConstPtr CommandList => _commandList; - - public D3D12CommandBuffer(ID3D12GraphicsCommandList10* commandList) - { - _commandList = commandList; - } - - public void BarrierTransition(IResource resource, ResourceStates beforeState, ResourceStates afterState) - { - var dxResource = (D3D12Resource)resource; - _commandList.Ptr->ResourceBarrierTransition(dxResource.NativeResource.Ptr, beforeState, afterState); - } - - public void SetGraphicsRootConstantBufferView(uint slot, ulong gpuAddress) - { - _commandList.Ptr->SetGraphicsRootConstantBufferView(slot, gpuAddress); - } - - public void DrawMesh(Mesh mesh, Material material) - { - _commandList.Ptr->SetGraphicsRootSignature(material.Shader.RootSignature); - _commandList.Ptr->SetPipelineState(material.Shader.PipelineState); - - material.UploadAndBind(this); - - _commandList.Ptr->IASetPrimitiveTopology(PrimitiveTopology.TriangleList); - _commandList.Ptr->IASetVertexBuffers(0, 1, mesh.VertexBufferView); - _commandList.Ptr->IASetIndexBuffer(mesh.IndexBufferView); - - _commandList.Ptr->DrawIndexedInstanced(mesh.IndexCount, 1, 0, 0, 0); - } - - public void CopyResource(IResource dstResource, uint dstOffset, IResource srcResource, uint srcOffset, uint size) - { - var dstDXResource = (D3D12Resource)dstResource; - var srcDXResource = (D3D12Resource)srcResource; - - _commandList.Ptr->CopyBufferRegion(dstDXResource.NativeResource, dstOffset, srcDXResource.NativeResource, srcOffset, size); - } -} \ No newline at end of file diff --git a/Ghost.Graphics/D3D12/D3D12DebugLayer.cs b/Ghost.Graphics/D3D12/DebugLayer.cs similarity index 89% rename from Ghost.Graphics/D3D12/D3D12DebugLayer.cs rename to Ghost.Graphics/D3D12/DebugLayer.cs index 35c145e..182854a 100644 --- a/Ghost.Graphics/D3D12/D3D12DebugLayer.cs +++ b/Ghost.Graphics/D3D12/DebugLayer.cs @@ -1,17 +1,16 @@ -using Ghost.Graphics.Contracts; -using Win32; +using Win32; using Win32.Graphics.Direct3D12; using Win32.Graphics.Dxgi; namespace Ghost.Graphics.D3D12; -internal unsafe class D3D12DebugLayer : IDebugLayer +internal unsafe class DebugLayer { private readonly ComPtr _d3d12Debug; private readonly ComPtr _dxgiDebug; private readonly ComPtr _dxgiInfoQueue; - public D3D12DebugLayer() + public DebugLayer() { D3D12GetDebugInterface(__uuidof(), _d3d12Debug.GetVoidAddressOf()); _d3d12Debug.Get()->EnableDebugLayer(); diff --git a/Ghost.Graphics/D3D12/DescriptorAllocator.cs b/Ghost.Graphics/D3D12/DescriptorAllocator.cs new file mode 100644 index 0000000..5ee7396 --- /dev/null +++ b/Ghost.Graphics/D3D12/DescriptorAllocator.cs @@ -0,0 +1,330 @@ +using Ghost.Core; +using Ghost.Graphics.D3D12.Utilities; +using Win32.Graphics.Direct3D12; + +namespace Ghost.Graphics.D3D12; + +/// +/// D3D12 implementation of descriptor allocator that manages different types of descriptor heaps. +/// +internal class DescriptorAllocator : IDisposable +{ + private readonly DescriptorHeapAllocator _rtvHeap; + private readonly DescriptorHeapAllocator _dsvHeap; + private readonly DescriptorHeapAllocator _srvHeap; + private readonly DescriptorHeapAllocator _samplerHeap; + + private bool _disposed; + + public DescriptorAllocator(uint initialRtvCount = 256, uint initialDsvCount = 256, uint initialSrvCount = 1024, uint initialSamplerCount = 256) + { + var device = GraphicsPipeline.GraphicsDevice; + + _rtvHeap = new DescriptorHeapAllocator(device.NativeDevice, DescriptorHeapType.Rtv, initialRtvCount); + _dsvHeap = new DescriptorHeapAllocator(device.NativeDevice, DescriptorHeapType.Dsv, initialDsvCount); + _srvHeap = new DescriptorHeapAllocator(device.NativeDevice, DescriptorHeapType.CbvSrvUav, initialSrvCount); + _samplerHeap = new DescriptorHeapAllocator(device.NativeDevice, DescriptorHeapType.Sampler, initialSamplerCount); + } + + #region RTV Methods + + public RenderTargetDescriptor AllocateRTV() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var index = _rtvHeap.AllocateDescriptor(); + if (index == uint.MaxValue) + { + throw new InvalidOperationException("Failed to allocate RTV descriptor"); + } + + var cpuHandle = _rtvHeap.GetCpuHandle(index); + return new RenderTargetDescriptor(index, cpuHandle); + } + + public RenderTargetDescriptor[] AllocateRTVs(uint count) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var baseIndex = _rtvHeap.AllocateDescriptors(count); + if (baseIndex == uint.MaxValue) + { + throw new InvalidOperationException($"Failed to allocate {count} RTV descriptors"); + } + + var descriptors = new RenderTargetDescriptor[count]; + for (uint i = 0; i < count; i++) + { + var index = baseIndex + i; + var cpuHandle = _rtvHeap.GetCpuHandle(index); + descriptors[i] = new RenderTargetDescriptor(index, cpuHandle); + } + + return descriptors; + } + + public void ReleaseRTV(RenderTargetDescriptor descriptor) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (descriptor is RenderTargetDescriptor d3d12Descriptor) + { + _rtvHeap.ReleaseDescriptor(d3d12Descriptor.Index); + } + } + + public void ReleaseRTVs(RenderTargetDescriptor[] descriptors) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + foreach (var descriptor in descriptors) + { + ReleaseRTV(descriptor); + } + } + + #endregion + + #region DSV Methods + + public DepthStencilDescriptor AllocateDSV() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var index = _dsvHeap.AllocateDescriptor(); + if (index == uint.MaxValue) + { + throw new InvalidOperationException("Failed to allocate DSV descriptor"); + } + + var cpuHandle = _dsvHeap.GetCpuHandle(index); + return new DepthStencilDescriptor(index, cpuHandle); + } + + public DepthStencilDescriptor[] AllocateDSVs(uint count) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var baseIndex = _dsvHeap.AllocateDescriptors(count); + if (baseIndex == uint.MaxValue) + { + throw new InvalidOperationException($"Failed to allocate {count} DSV descriptors"); + } + + var descriptors = new DepthStencilDescriptor[count]; + for (uint i = 0; i < count; i++) + { + var index = baseIndex + i; + var cpuHandle = _dsvHeap.GetCpuHandle(index); + descriptors[i] = new DepthStencilDescriptor(index, cpuHandle); + } + + return descriptors; + } + + public void ReleaseDSV(DepthStencilDescriptor descriptor) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (descriptor is DepthStencilDescriptor d3d12Descriptor) + { + _dsvHeap.ReleaseDescriptor(d3d12Descriptor.Index); + } + } + + public void ReleaseDSVs(DepthStencilDescriptor[] descriptors) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + foreach (var descriptor in descriptors) + { + ReleaseDSV(descriptor); + } + } + + #endregion + + #region SRV Methods + + public ShaderResourceDescriptor AllocateSRV() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var index = _srvHeap.AllocateDescriptor(); + if (index == uint.MaxValue) + { + throw new InvalidOperationException("Failed to allocate SRV descriptor"); + } + + var cpuHandle = _srvHeap.GetCpuHandle(index); + var gpuHandle = _srvHeap.GetGpuHandle(index); + + // Copy to shader visible heap + _srvHeap.CopyToShaderVisibleHeap(index); + + return new ShaderResourceDescriptor(index, cpuHandle, gpuHandle); + } + + public ShaderResourceDescriptor[] AllocateSRVs(uint count) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var baseIndex = _srvHeap.AllocateDescriptors(count); + if (baseIndex == uint.MaxValue) + { + throw new InvalidOperationException($"Failed to allocate {count} SRV descriptors"); + } + + var descriptors = new ShaderResourceDescriptor[count]; + for (uint i = 0; i < count; i++) + { + var index = baseIndex + i; + var cpuHandle = _srvHeap.GetCpuHandle(index); + var gpuHandle = _srvHeap.GetGpuHandle(index); + descriptors[i] = new ShaderResourceDescriptor(index, cpuHandle, gpuHandle); + } + + // Copy all descriptors to shader visible heap + _srvHeap.CopyToShaderVisibleHeap(baseIndex, count); + + return descriptors; + } + + public void ReleaseSRV(ShaderResourceDescriptor descriptor) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (descriptor is ShaderResourceDescriptor d3d12Descriptor) + { + _srvHeap.ReleaseDescriptor(d3d12Descriptor.Index); + } + } + + public void ReleaseSRVs(ShaderResourceDescriptor[] descriptors) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + foreach (var descriptor in descriptors) + { + ReleaseSRV(descriptor); + } + } + + #endregion + + #region Sampler Methods + + public SamplerDescriptor AllocateSampler() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var index = _samplerHeap.AllocateDescriptor(); + if (index == uint.MaxValue) + { + throw new InvalidOperationException("Failed to allocate Sampler descriptor"); + } + + var cpuHandle = _samplerHeap.GetCpuHandle(index); + var gpuHandle = _samplerHeap.GetGpuHandle(index); + + // Copy to shader visible heap + _samplerHeap.CopyToShaderVisibleHeap(index); + + return new SamplerDescriptor(index, cpuHandle, gpuHandle); + } + + public SamplerDescriptor[] AllocateSamplers(uint count) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var baseIndex = _samplerHeap.AllocateDescriptors(count); + if (baseIndex == uint.MaxValue) + { + throw new InvalidOperationException($"Failed to allocate {count} Sampler descriptors"); + } + + var descriptors = new SamplerDescriptor[count]; + for (uint i = 0; i < count; i++) + { + var index = baseIndex + i; + var cpuHandle = _samplerHeap.GetCpuHandle(index); + var gpuHandle = _samplerHeap.GetGpuHandle(index); + descriptors[i] = new SamplerDescriptor(index, cpuHandle, gpuHandle); + } + + // Copy all descriptors to shader visible heap + _samplerHeap.CopyToShaderVisibleHeap(baseIndex, count); + + return descriptors; + } + + public void ReleaseSampler(SamplerDescriptor descriptor) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (descriptor is SamplerDescriptor d3d12Descriptor) + { + _samplerHeap.ReleaseDescriptor(d3d12Descriptor.Index); + } + } + + public void ReleaseSamplers(SamplerDescriptor[] descriptors) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + foreach (var descriptor in descriptors) + { + ReleaseSampler(descriptor); + } + } + + #endregion + + #region Utility Methods + + /// + /// Gets the RTV heap for binding to the command list. + /// + public ConstPtr GetRTVHeap() => _rtvHeap.Heap; + + /// + /// Gets the DSV heap for binding to the command list. + /// + public ConstPtr GetDSVHeap() => _dsvHeap.Heap; + + /// + /// Gets the SRV heap for binding to the command list. + /// + public ConstPtr GetSRVHeap() => _srvHeap.ShaderVisibleHeap; + + /// + /// Gets the sampler heap for binding to the command list. + /// + public ConstPtr GetSamplerHeap() => _samplerHeap.ShaderVisibleHeap; + + /// + /// Gets the shader visible heaps that need to be bound to the command list. + /// + public ConstPtr[] GetShaderVisibleHeaps() + { + return [_srvHeap.ShaderVisibleHeap, _samplerHeap.ShaderVisibleHeap]; + } + + #endregion + + public void Dispose() + { + if (_disposed) + { + return; + } + + _rtvHeap.Dispose(); + _dsvHeap.Dispose(); + _srvHeap.Dispose(); + _samplerHeap.Dispose(); + + _disposed = true; + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/D3D12/Descriptors.cs b/Ghost.Graphics/D3D12/Descriptors.cs new file mode 100644 index 0000000..28e653a --- /dev/null +++ b/Ghost.Graphics/D3D12/Descriptors.cs @@ -0,0 +1,110 @@ +using Win32.Graphics.Direct3D12; + +namespace Ghost.Graphics.D3D12; + +/// +/// Base class for D3D12 descriptor implementations. +/// +internal abstract class Descriptor +{ + protected readonly uint index; + protected readonly bool isShaderVisible; + + protected Descriptor(uint index, bool isShaderVisible) + { + this.index = index; + this.isShaderVisible = isShaderVisible; + } + + public bool IsValid => index != uint.MaxValue; + public uint Index => index; + public bool IsShaderVisible => isShaderVisible; + + /// + /// Gets the CPU descriptor handle. + /// + public abstract CpuDescriptorHandle CpuHandle + { + get; + } + + /// + /// Gets the GPU descriptor handle (only valid for shader-visible descriptors). + /// + public abstract GpuDescriptorHandle GpuHandle + { + get; + } +} + +/// +/// D3D12 implementation of render target view (RTV) descriptor. +/// +internal sealed class RenderTargetDescriptor : Descriptor +{ + private readonly CpuDescriptorHandle _cpuHandle; + + public RenderTargetDescriptor(uint index, CpuDescriptorHandle cpuHandle) + : base(index, false) + { + _cpuHandle = cpuHandle; + } + + public override CpuDescriptorHandle CpuHandle => _cpuHandle; + public override GpuDescriptorHandle GpuHandle => default; +} + +/// +/// D3D12 implementation of depth stencil view (DSV) descriptor. +/// +internal sealed class DepthStencilDescriptor : Descriptor +{ + private readonly CpuDescriptorHandle _cpuHandle; + + public DepthStencilDescriptor(uint index, CpuDescriptorHandle cpuHandle) + : base(index, false) // DSVs are not shader visible + { + _cpuHandle = cpuHandle; + } + + public override CpuDescriptorHandle CpuHandle => _cpuHandle; + public override GpuDescriptorHandle GpuHandle => default; +} + +/// +/// D3D12 implementation of shader resource view (SRV) descriptor. +/// +internal sealed class ShaderResourceDescriptor : Descriptor +{ + private readonly CpuDescriptorHandle _cpuHandle; + private readonly GpuDescriptorHandle _gpuHandle; + + public ShaderResourceDescriptor(uint index, CpuDescriptorHandle cpuHandle, GpuDescriptorHandle gpuHandle) + : base(index, true) + { + _cpuHandle = cpuHandle; + _gpuHandle = gpuHandle; + } + + public override CpuDescriptorHandle CpuHandle => _cpuHandle; + public override GpuDescriptorHandle GpuHandle => _gpuHandle; +} + +/// +/// D3D12 implementation of sampler descriptor. +/// +internal sealed class SamplerDescriptor : Descriptor +{ + private readonly CpuDescriptorHandle _cpuHandle; + private readonly GpuDescriptorHandle _gpuHandle; + + public SamplerDescriptor(uint index, CpuDescriptorHandle cpuHandle, GpuDescriptorHandle gpuHandle) + : base(index, true) + { + _cpuHandle = cpuHandle; + _gpuHandle = gpuHandle; + } + + public override CpuDescriptorHandle CpuHandle => _cpuHandle; + public override GpuDescriptorHandle GpuHandle => _gpuHandle; +} \ No newline at end of file diff --git a/Ghost.Graphics/D3D12/D3D12GraphicsDevice.cs b/Ghost.Graphics/D3D12/GraphicsDevice.cs similarity index 76% rename from Ghost.Graphics/D3D12/D3D12GraphicsDevice.cs rename to Ghost.Graphics/D3D12/GraphicsDevice.cs index 4935dee..98f8739 100644 --- a/Ghost.Graphics/D3D12/D3D12GraphicsDevice.cs +++ b/Ghost.Graphics/D3D12/GraphicsDevice.cs @@ -1,5 +1,4 @@ using Ghost.Core; -using Ghost.Graphics.Contracts; using Ghost.Graphics.Data; using System.Collections.Immutable; using Win32; @@ -9,39 +8,38 @@ using Win32.Graphics.Dxgi; namespace Ghost.Graphics.D3D12; -internal unsafe class D3D12GraphicsDevice : IGraphicsDevice +internal unsafe class GraphicsDevice { #if DEBUG - private readonly D3D12DebugLayer _debugLayer; + private readonly DebugLayer _debugLayer; #endif private ComPtr _dxgiFactory; private ComPtr _device; private ComPtr _commandQueue; - private ImmutableArray _initializeQueue; - private ImmutableArray _renderers; + private ImmutableArray _initializeQueue; + private ImmutableArray _renderers; private bool _disposed; - public static GraphicsAPI TargetAPI => GraphicsAPI.D3D12; - public ReadOnlySpan InitializeQueue => _initializeQueue.AsSpan(); - public ReadOnlySpan Renderers => _renderers.AsSpan(); + public ReadOnlySpan InitializeQueue => _initializeQueue.AsSpan(); + public ReadOnlySpan Renderers => _renderers.AsSpan(); public ConstPtr NativeDevice => new(_device.Get()); public ConstPtr DXGIFactory => new(_dxgiFactory.Get()); public ConstPtr CommandQueue => new(_commandQueue.Get()); - public D3D12GraphicsDevice() + public GraphicsDevice() { #if DEBUG - _debugLayer = new D3D12DebugLayer(); + _debugLayer = new DebugLayer(); #endif InitializeDevice(); InitializeCommandQueue(); - _initializeQueue = ImmutableArray.Empty; - _renderers = ImmutableArray.Empty; + _initializeQueue = ImmutableArray.Empty; + _renderers = ImmutableArray.Empty; } private void InitializeDevice() @@ -67,7 +65,7 @@ internal unsafe class D3D12GraphicsDevice : IGraphicsDevice continue; } - if (D3D12CreateDevice((IUnknown*)adapter.Get(), FeatureLevel.Level_11_0, __uuidof(), _device.GetVoidAddressOf()).Success) + if (D3D12CreateDevice((IUnknown*)adapter.Get(), FeatureLevel.Level_12_0, __uuidof(), _device.GetVoidAddressOf()).Success) { break; } @@ -75,7 +73,7 @@ internal unsafe class D3D12GraphicsDevice : IGraphicsDevice if (_device.Get() == null) { - throw new PlatformNotSupportedException("Cannot create ID3D12Device"); + throw new PlatformNotSupportedException("Cannot create ID3D12Device with feature level 12.0"); } } @@ -94,17 +92,17 @@ internal unsafe class D3D12GraphicsDevice : IGraphicsDevice } } - public IRenderer CreateRenderer(in SwapChainPresenter presenter) + public Renderer CreateRenderer(in SwapChainPresenter presenter) { - var renderView = new D3D12Renderer(this, in presenter); + var renderView = new Renderer(this, in presenter); ImmutableInterlocked.Update(ref _initializeQueue, old => old.Add(renderView)); return renderView; } - public void RemoveRenderer(IRenderer renderer) + public void RemoveRenderer(Renderer renderer) { - if (renderer is D3D12Renderer dx12RenderView) + if (renderer is Renderer dx12RenderView) { dx12RenderView.Dispose(); @@ -148,13 +146,8 @@ internal unsafe class D3D12GraphicsDevice : IGraphicsDevice renderer.Dispose(); } - foreach (var renderView in _renderers) - { - renderView.Dispose(); - } - _commandQueue.Dispose(); - _device.Dispose(); + _device.Reset(); _dxgiFactory.Dispose(); #if DEBUG diff --git a/Ghost.Graphics/D3D12/D3D12Resource.cs b/Ghost.Graphics/D3D12/GraphicsResource.cs similarity index 87% rename from Ghost.Graphics/D3D12/D3D12Resource.cs rename to Ghost.Graphics/D3D12/GraphicsResource.cs index 17e7f46..4aebf72 100644 --- a/Ghost.Graphics/D3D12/D3D12Resource.cs +++ b/Ghost.Graphics/D3D12/GraphicsResource.cs @@ -1,5 +1,4 @@ using Ghost.Core; -using Ghost.Graphics.Contracts; using Misaki.HighPerformance.LowLevel.Collections; using System.Runtime.CompilerServices; using Win32; @@ -7,7 +6,7 @@ using Win32.Graphics.Direct3D12; namespace Ghost.Graphics.D3D12; -public unsafe class D3D12Resource : IResource +public unsafe class GraphicsResource { private ComPtr _nativeResource; private string _name = string.Empty; @@ -33,13 +32,13 @@ public unsafe class D3D12Resource : IResource get; } - internal D3D12Resource(ComPtr nativeResource, bool temp) + internal GraphicsResource(ComPtr nativeResource, bool temp = false) { _nativeResource = nativeResource; TempResource = temp; } - ~D3D12Resource() + ~GraphicsResource() { DisposeInternal(); } @@ -68,12 +67,7 @@ public unsafe class D3D12Resource : IResource var range = new Win32.Graphics.Direct3D12.Range(0, size); void* mappedPtr; - var hr = _nativeResource.Get()->Map(0, &range, &mappedPtr); - if (hr.Failure) - { - var message = hr.ToString(); - throw new InvalidOperationException($"Failed to map resource: {message}"); - } + ThrowIfFailed(_nativeResource.Get()->Map(0, &range, &mappedPtr)); Unsafe.CopyBlock(mappedPtr, data, size); _nativeResource.Get()->Unmap(0, &range); @@ -99,7 +93,7 @@ public unsafe class D3D12Resource : IResource public void ReadData(T* pData, uint* size) where T : unmanaged { - ReadData(pData, size); + ReadData((void*)pData, size); } public void ReadData(void* pData, uint* size) diff --git a/Ghost.Graphics/D3D12/D3D12Renderer.cs b/Ghost.Graphics/D3D12/Renderer.cs similarity index 88% rename from Ghost.Graphics/D3D12/D3D12Renderer.cs rename to Ghost.Graphics/D3D12/Renderer.cs index c0c404b..822fb80 100644 --- a/Ghost.Graphics/D3D12/D3D12Renderer.cs +++ b/Ghost.Graphics/D3D12/Renderer.cs @@ -14,7 +14,7 @@ namespace Ghost.Graphics.D3D12; // TODO: We should split the renderer and swap chain into different classes to allow for more flexibility in rendering pipelines. // Each renderer can have a render target (swap chain or texture). // When render target is null, skip the render pass execution. -internal unsafe class D3D12Renderer : IRenderer +internal unsafe class Renderer { private struct FrameResource : IDisposable { @@ -22,17 +22,17 @@ internal unsafe class D3D12Renderer : IRenderer public ComPtr commandList; public ComPtr backBuffer; - public ICommandBuffer commandBuffer; - public uint backBufferDescriptorIndexes; + public CommandList cmd; + public RenderTargetDescriptor rtvDescriptor; public ulong fenceValue; - public FrameResource(D3D12Renderer renderer, uint index) + public FrameResource(Renderer renderer, uint index) { renderer._graphicsDevice.NativeDevice.Ptr->CreateCommandAllocator(CommandListType.Direct, __uuidof(), commandAllocator.GetVoidAddressOf()); renderer._graphicsDevice.NativeDevice.Ptr->CreateCommandList(0u, CommandListType.Direct, commandAllocator.Get(), null, __uuidof(), commandList.GetVoidAddressOf()); - commandBuffer = new D3D12CommandBuffer(commandList.Get()); - backBufferDescriptorIndexes = renderer.CreateBackBufferResource(index, backBuffer.GetAddressOf()); + cmd = new(commandList.Get()); + rtvDescriptor = renderer.CreateBackBufferResource(index, backBuffer.GetAddressOf()); } public readonly void ResetCommandBuffer() @@ -58,13 +58,11 @@ internal unsafe class D3D12Renderer : IRenderer commandAllocator.Dispose(); commandList.Dispose(); backBuffer.Dispose(); + GraphicsPipeline.DescriptorAllocator.ReleaseRTV(rtvDescriptor); } } - private const int _RENDER_TARGET_VIEW_HEAP_SIZE = 1024; - private const int _DEPTH_STENCIL_VIEW_HEAP_SIZE = 256; - - private readonly D3D12GraphicsDevice _graphicsDevice; + private readonly GraphicsDevice _graphicsDevice; private readonly SwapChainPresenter _swapChainPresenter; private ComPtr _swapChain = default; @@ -74,8 +72,6 @@ internal unsafe class D3D12Renderer : IRenderer private readonly FrameResource[] _frameResources; private readonly AutoResetEvent _fenceEvent; - private D3D12DescriptorAllocator _rtvHeap; - private ImmutableArray _renderPasses; private readonly Lock _lock = new(); @@ -89,16 +85,15 @@ internal unsafe class D3D12Renderer : IRenderer public ReadOnlySpan RenderPasses => _renderPasses.AsSpan(); - public D3D12Renderer(D3D12GraphicsDevice graphicsDevice, in SwapChainPresenter swapChainSurface) + public Renderer(GraphicsDevice graphicsDevice, in SwapChainPresenter swapChainSurface) { _graphicsDevice = graphicsDevice; _swapChainPresenter = swapChainSurface; _viewPortWidth = swapChainSurface.Width; _viewPortHeight = swapChainSurface.Height; - _rtvHeap = new(_graphicsDevice.NativeDevice, DescriptorHeapType.Rtv, _RENDER_TARGET_VIEW_HEAP_SIZE); _fenceEvent = new(false); - _renderPasses = [new MeshRenderPass()]; + _renderPasses = [new BindlessMeshRenderPass()]; InitializeSwapChain(); InitializeFrameResource(out _frameResources); @@ -190,14 +185,14 @@ internal unsafe class D3D12Renderer : IRenderer } } - private uint CreateBackBufferResource(uint i, ID3D12Resource** backBuffer) + private RenderTargetDescriptor CreateBackBufferResource(uint i, ID3D12Resource** backBuffer) { _swapChain.Get()->GetBuffer(i, __uuidof(), (void**)backBuffer); (*backBuffer)->SetName($"BackBuffer_{i}"); - var index = _rtvHeap.AllocateDescriptor(); - var rtvHandle = _rtvHeap.GetCpuHandle(index); + var rtvDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateRTV(); + var rtvHandle = rtvDescriptor.CpuHandle; _graphicsDevice.NativeDevice.Ptr->CreateRenderTargetView(*backBuffer, null, rtvHandle); - return index; + return rtvDescriptor; } public void ExecutePendingResize() @@ -225,7 +220,7 @@ internal unsafe class D3D12Renderer : IRenderer if (frameResource.backBuffer.Get() is not null) { var c = frameResource.backBuffer.Reset(); - _rtvHeap.ReleaseDescriptor(frameResource.backBufferDescriptorIndexes); + GraphicsPipeline.DescriptorAllocator.ReleaseRTV(frameResource.rtvDescriptor); } frameResource.fenceValue = _frameResources[_backBufferIndex].fenceValue; @@ -239,7 +234,7 @@ internal unsafe class D3D12Renderer : IRenderer for (var i = 0u; i < GraphicsPipeline._FRAME_COUNT; i++) { var index = CreateBackBufferResource(i, _frameResources[i].backBuffer.GetAddressOf()); - _frameResources[i].backBufferDescriptorIndexes = index; + _frameResources[i].rtvDescriptor = index; } _backBufferIndex = _swapChain.Get()->GetCurrentBackBufferIndex(); @@ -253,7 +248,7 @@ internal unsafe class D3D12Renderer : IRenderer foreach (var pass in _renderPasses) { - pass.Initialize(frameResource.commandBuffer); + pass.Initialize(frameResource.cmd); } frameResource.ExecuteCommandBuffer(_graphicsDevice.CommandQueue); @@ -265,7 +260,7 @@ internal unsafe class D3D12Renderer : IRenderer _backBufferIndex = _swapChain.Get()->GetCurrentBackBufferIndex(); ref var frameResource = ref _frameResources[_backBufferIndex]; - var cpuHandle = _rtvHeap.GetCpuHandle(frameResource.backBufferDescriptorIndexes); + var cpuHandle = frameResource.rtvDescriptor.CpuHandle; frameResource.ResetCommandBuffer(); frameResource.commandList.Get()->ResourceBarrierTransition(frameResource.backBuffer.Get(), ResourceStates.Present, ResourceStates.RenderTarget); @@ -282,7 +277,7 @@ internal unsafe class D3D12Renderer : IRenderer foreach (var pass in _renderPasses) { - pass.Execute(frameResource.commandBuffer); + pass.Execute(frameResource.cmd); } frameResource.commandList.Get()->ResourceBarrierTransition(frameResource.backBuffer.Get(), ResourceStates.RenderTarget, ResourceStates.Present); @@ -353,8 +348,6 @@ internal unsafe class D3D12Renderer : IRenderer _fence.Dispose(); _fenceEvent.Dispose(); - _rtvHeap.Dispose(); - _backBufferIndex = 0; _disposed = true; diff --git a/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs b/Ghost.Graphics/D3D12/ResourceAllocator.cs similarity index 90% rename from Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs rename to Ghost.Graphics/D3D12/ResourceAllocator.cs index c324fcd..ab5dc7a 100644 --- a/Ghost.Graphics/D3D12/D3D12ResourceAllocator.cs +++ b/Ghost.Graphics/D3D12/ResourceAllocator.cs @@ -1,23 +1,22 @@ -using Ghost.Graphics.Contracts; -using Win32; +using Win32; using Win32.Graphics.Direct3D12; namespace Ghost.Graphics.D3D12; -internal unsafe class D3D12ResourceAllocator : IResourceAllocator +internal unsafe class ResourceAllocator { private readonly struct TempResourceAllocInfo { - public readonly D3D12Resource resource; + public readonly GraphicsResource resource; public readonly uint cpuFenceValue; - public TempResourceAllocInfo(D3D12Resource resource, uint cpuFenceValue) + public TempResourceAllocInfo(GraphicsResource resource, uint cpuFenceValue) { this.resource = resource; this.cpuFenceValue = cpuFenceValue; } - public TempResourceAllocInfo(D3D12Resource resource) + public TempResourceAllocInfo(GraphicsResource resource) : this(resource, GraphicsPipeline.CPUFenceValue + 1) { } @@ -224,29 +223,28 @@ internal unsafe class D3D12ResourceAllocator : IResourceAllocator // } //} - public IResource CreateUploadBuffer(uint sizeInBytes, bool tempResource = false, ResourceFlags flags = ResourceFlags.None) + public GraphicsResource CreateUploadBuffer(uint sizeInBytes, bool tempResource = false, ResourceFlags flags = ResourceFlags.None) { if (sizeInBytes > _MAX_BYTES) { throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {sizeInBytes})"); } - var device = GraphicsPipeline.GetGraphicsDevice(); var heapProperties = new HeapProperties(HeapType.Upload); var resourceDescription = ResourceDescription.Buffer(sizeInBytes, flags); ComPtr buffer = default; - device.NativeDevice.Ptr->CreateCommittedResource( + GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateCommittedResource( &heapProperties, HeapFlags.None, &resourceDescription, - ResourceStates.Common, + ResourceStates.GenericRead, null, __uuidof(), buffer.GetVoidAddressOf() ); - var resource = new D3D12Resource(buffer.Move(), tempResource); + var resource = new GraphicsResource(buffer.Move(), tempResource); if (tempResource) { _temResources.Enqueue(new(resource)); @@ -255,19 +253,18 @@ internal unsafe class D3D12ResourceAllocator : IResourceAllocator return resource; } - public IResource CreateCopyDestinationBuffer(uint sizeInBytes, bool tempResource = false, ResourceFlags flags = ResourceFlags.None) + public GraphicsResource CreateCopyDestinationBuffer(uint sizeInBytes, bool tempResource = false, ResourceFlags flags = ResourceFlags.None) { if (sizeInBytes > _MAX_BYTES) { throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {sizeInBytes})"); } - var device = GraphicsPipeline.GetGraphicsDevice(); var heapProperties = new HeapProperties(HeapType.Default); var resourceDescription = ResourceDescription.Buffer(sizeInBytes, flags); ComPtr buffer = default; - device.NativeDevice.Ptr->CreateCommittedResource( + GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateCommittedResource( &heapProperties, HeapFlags.None, &resourceDescription, @@ -277,7 +274,7 @@ internal unsafe class D3D12ResourceAllocator : IResourceAllocator buffer.GetVoidAddressOf() ); - var resource = new D3D12Resource(buffer.Move(), tempResource); + var resource = new GraphicsResource(buffer.Move(), tempResource); if (tempResource) { _temResources.Enqueue(new(resource)); diff --git a/Ghost.Graphics/D3D12/ResourceUploadBatch.cs b/Ghost.Graphics/D3D12/ResourceUploadBatch.cs new file mode 100644 index 0000000..fdd774b --- /dev/null +++ b/Ghost.Graphics/D3D12/ResourceUploadBatch.cs @@ -0,0 +1,339 @@ +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 struct TrackedResource + { + public GraphicsResource resource; + public ResourceStates state; + + public TrackedResource(GraphicsResource resource, ResourceStates state) + { + this.resource = resource; + this.state = state; + } + } + + 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(GraphicsResource resource, ReadOnlySpan data) + where T : unmanaged + { + if (!_isRecording) + { + throw new InvalidOperationException("Upload batch is not recording"); + } + + var sizeInBytes = (uint)(data.Length * sizeof(T)); + + // Create upload buffer + var uploadBuffer = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer(sizeInBytes, true); + + // Copy data to upload buffer + fixed (T* dataPtr = data) + { + uploadBuffer.SetData(dataPtr, (uint)data.Length); + } + + // Copy from upload buffer to destination + _commandList.Get()->CopyBufferRegion( + resource.NativeResource.Ptr, + 0, + uploadBuffer.NativeResource.Ptr, + 0, + sizeInBytes); + } + + /// + /// Uploads subresource data to a texture + /// + /// Destination texture resource + /// First subresource index + /// Subresource data array + /// Number of subresources + public void Upload(GraphicsResource resource, uint firstSubresource, SubresourceData* subresources, uint numSubresources) + { + if (!_isRecording) + { + throw new InvalidOperationException("Upload batch is not recording"); + } + + var resourceDesc = resource.NativeResource.Ptr->GetDesc(); + + var requiredSize = GetRequiredIntermediateSize(resource, firstSubresource, numSubresources); + + var uploadBuffer = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer((uint)requiredSize, true); + + UpdateSubresources( + resource.NativeResource.Ptr, + uploadBuffer.NativeResource.Ptr, + 0, + firstSubresource, + numSubresources, + subresources); + } + + /// + /// Adds a resource transition barrier + /// + /// Resource to transition + /// State before transition + /// State after transition + public void Transition(GraphicsResource resource, ResourceStates stateBefore, ResourceStates stateAfter) + { + if (!_isRecording) + { + throw new InvalidOperationException("Upload batch is not recording"); + } + + // Apply the transition immediately + var barrier = new ResourceBarrier + { + Type = ResourceBarrierType.Transition, + Flags = ResourceBarrierFlags.None, + Anonymous = new ResourceBarrier._Anonymous_e__Union + { + Transition = new ResourceTransitionBarrier + { + pResource = resource.NativeResource.Ptr, + Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + StateBefore = stateBefore, + StateAfter = stateAfter + } + } + }; + + _commandList.Get()->ResourceBarrier(1, &barrier); + } + + /// + /// 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(GraphicsResource destinationResource, uint firstSubresource, uint numSubresources) + { + var resourceDesc = destinationResource.NativeResource.Ptr->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); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/D3D12/Utilities/D3D12DescriptorAllocator.cs b/Ghost.Graphics/D3D12/Utilities/DescriptorHeapAllocator.cs similarity index 97% rename from Ghost.Graphics/D3D12/Utilities/D3D12DescriptorAllocator.cs rename to Ghost.Graphics/D3D12/Utilities/DescriptorHeapAllocator.cs index 2cb4f80..88adf3a 100644 --- a/Ghost.Graphics/D3D12/Utilities/D3D12DescriptorAllocator.cs +++ b/Ghost.Graphics/D3D12/Utilities/DescriptorHeapAllocator.cs @@ -7,7 +7,7 @@ using DescriptorIndex = System.UInt32; namespace Ghost.Graphics.D3D12.Utilities; -internal unsafe struct D3D12DescriptorAllocator : IDisposable +internal unsafe struct DescriptorHeapAllocator : IDisposable { private const DescriptorIndex _INVALID_DESCRIPTOR_INDEX = ~0u; @@ -50,7 +50,7 @@ internal unsafe struct D3D12DescriptorAllocator : IDisposable public readonly ConstPtr Heap => new(_heap.Get()); public readonly ConstPtr ShaderVisibleHeap => new(_shaderVisibleHeap.Get()); - public D3D12DescriptorAllocator(ConstPtr device, DescriptorHeapType type, uint numDescriptors) + public DescriptorHeapAllocator(ConstPtr device, DescriptorHeapType type, uint numDescriptors) { _device = device; HeapType = type; diff --git a/Ghost.Graphics/Data/GraphicsAPI.cs b/Ghost.Graphics/Data/GraphicsAPI.cs deleted file mode 100644 index d75072f..0000000 --- a/Ghost.Graphics/Data/GraphicsAPI.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ghost.Graphics.Data; - -public enum GraphicsAPI -{ - None, - D3D12 -} \ No newline at end of file diff --git a/Ghost.Graphics/Data/Material.cs b/Ghost.Graphics/Data/Material.cs index 883f9f1..528e703 100644 --- a/Ghost.Graphics/Data/Material.cs +++ b/Ghost.Graphics/Data/Material.cs @@ -1,13 +1,15 @@ -using Ghost.Graphics.Contracts; +using Ghost.Graphics.D3D12; using Ghost.Graphics.Shading; using System.Numerics; using System.Runtime.CompilerServices; namespace Ghost.Graphics.Data; -public class Material : IDisposable +public unsafe class Material : IDisposable { - private readonly Dictionary _cbufferCaches; + private readonly CBufferCache[] _cbufferCaches; + private readonly Texture2D?[] _textures; + private readonly Dictionary _textureNameToSlotMap; private bool _disposed; @@ -21,10 +23,37 @@ public class Material : IDisposable { Shader = shader; - _cbufferCaches = new(); - foreach (var cbufferInfo in shader.ConstantBuffers.Values) + if (shader.ConstantBuffers.Count > 0) { - _cbufferCaches.Add(cbufferInfo.Name, new CBufferCache(cbufferInfo.Size)); + var maxSlot = shader.ConstantBuffers.Max(cb => cb.RegisterSlot); + _cbufferCaches = new CBufferCache[maxSlot + 1]; + + foreach (var cbufferInfo in shader.ConstantBuffers) + { + _cbufferCaches[cbufferInfo.RegisterSlot] = new CBufferCache(cbufferInfo.Size); + } + } + else + { + _cbufferCaches = Array.Empty(); + } + + // Initialize texture storage + if (shader.Textures.Count > 0) + { + var maxTextureSlot = shader.Textures.Max(t => t.RegisterSlot); + _textures = new Texture2D?[maxTextureSlot + 1]; + _textureNameToSlotMap = new Dictionary(); + + foreach (var textureInfo in shader.Textures) + { + _textureNameToSlotMap.Add(textureInfo.Name, (int)textureInfo.RegisterSlot); + } + } + else + { + _textures = Array.Empty(); + _textureNameToSlotMap = new Dictionary(); } } @@ -33,52 +62,191 @@ public class Material : IDisposable Dispose(); } + /// + /// Sets a float property in the material's constant buffer. + /// + /// The ID of the property to set. + /// The value to set for the property. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetFloat(int propertyId, in float value) + { + WriteToCache(propertyId, in value); + } + + /// + /// Sets a float property in the material's constant buffer. + /// + /// The name of the property to set. + /// The value to set for the property. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetFloat(string propertyName, in float value) { - WriteToCache(propertyName, in value); + SetFloat(Shader.GetPropertyId(propertyName), in value); } + /// + /// Sets a Vector property in the material's constant buffer. + /// + /// The ID of the property to set. + /// The value to set for the property. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetVector(int propertyId, in Vector4 value) + { + WriteToCache(propertyId, in value); + } + + /// + /// Sets a Vector property in the material's constant buffer. + /// + /// The name of the property to set. + /// The value to set for the property. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetVector(string propertyName, in Vector4 value) { - WriteToCache(propertyName, in value); + SetVector(Shader.GetPropertyId(propertyName), in value); } + /// + /// Sets a Matrix property in the material's constant buffer. + /// + /// The ID of the property to set. + /// The value to set for the property. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetMatrix(int propertyId, in Matrix4x4 value) + { + WriteToCache(propertyId, in value); + } + + /// + /// Sets a Matrix property in the material's constant buffer. + /// + /// The name of the property to set. + /// The value to set for the property. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetMatrix(string propertyName, in Matrix4x4 value) { - WriteToCache(propertyName, in value); + SetMatrix(Shader.GetPropertyId(propertyName), in value); } - private unsafe void WriteToCache(string propertyName, in T value) where T : unmanaged + /// + /// Sets a texture property in the material. + /// + /// The ID of the texture to set. + /// The texture to set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetTexture(int textureId, Texture2D? texture) { - if (!Shader.Properties.TryGetValue(propertyName, out var propInfo)) + if (textureId == -1) { - throw new ArgumentException($"Property '{propertyName}' does not exist in the shader.", nameof(propertyName)); + throw new ArgumentException("Texture ID is invalid."); } + if (textureId >= _textures.Length) + { + throw new ArgumentException($"Texture ID {textureId} is out of range."); + } + + _textures[textureId] = texture; + } + + /// + /// Sets a texture property in the material. + /// + /// The name of the texture to set. + /// The texture to set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetTexture(string textureName, Texture2D? texture) + { + if (!_textureNameToSlotMap.TryGetValue(textureName, out var slot)) + { + throw new ArgumentException($"Texture '{textureName}' not found in shader."); + } + + _textures[slot] = texture; + } + + /// + /// Gets a texture property from the material. + /// + /// The ID of the texture to get. + /// The texture, or null if not set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Texture2D? GetTexture(int textureId) + { + if (textureId == -1 || textureId >= _textures.Length) + { + return null; + } + + return _textures[textureId]; + } + + /// + /// Gets a texture property from the material. + /// + /// The name of the texture to get. + /// The texture, or null if not set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Texture2D? GetTexture(string textureName) + { + if (!_textureNameToSlotMap.TryGetValue(textureName, out var slot)) + { + return null; + } + + return _textures[slot]; + } + + private unsafe void WriteToCache(int propertyId, in T value) where T : unmanaged + { + if (propertyId == -1) + { + throw new ArgumentException("Property ID is invalid."); + } + + var propInfo = Shader.Properties[propertyId]; if (propInfo.Size != sizeof(T)) { - throw new ArgumentException($"Property '{propertyName}' has a size mismatch. Expected {sizeof(T)} bytes, but got {propInfo.Size} bytes.", nameof(propertyName)); + throw new ArgumentException($"Property '{propInfo.Name}' has a size mismatch. Expected {sizeof(T)} bytes, but got {propInfo.Size} bytes."); } - var cache = _cbufferCaches[propInfo.CBufferName]; + var cache = _cbufferCaches[propInfo.CBufferIndex]; Unsafe.WriteUnaligned(ref cache.CpuData[(int)propInfo.ByteOffset], value); } - public void UploadAndBind(ICommandBuffer cmb) + /// + /// Uploads the material data to the GPU. + /// + public void UploadMaterialData() { - foreach (var cache in _cbufferCaches.Values) + foreach (var cache in _cbufferCaches) { cache.UploadToGpu(); } + } - foreach (var (name, cache) in _cbufferCaches) + internal void Bind(CommandList cmd) + { + // Bind constant buffers + foreach (var cbufferInfo in Shader.ConstantBuffers) { - var cbufferInfo = Shader.ConstantBuffers[name]; - cmb.SetGraphicsRootConstantBufferView(cbufferInfo.RegisterSlot, cache.GpuResource.GPUAddress); + var cache = _cbufferCaches[cbufferInfo.RegisterSlot]; + cmd.SetGraphicsRootConstantBufferView(cbufferInfo.RegisterSlot, cache.GpuResource.GPUAddress); + } + + // Bind textures using descriptor table + if (Shader.Textures.Count > 0) + { + // Get the first texture info to determine the root parameter index + var textureInfo = Shader.Textures[0]; + var texture = _textures[0]; // Get the first texture + + if (texture != null) + { + // Set descriptor table for SRVs + cmd.NativeCommandList.Ptr->SetGraphicsRootDescriptorTable(textureInfo.RootParameterIndex, texture.SRVDescriptor.GpuHandle); + } } } @@ -89,13 +257,11 @@ public class Material : IDisposable return; } - foreach (var cache in _cbufferCaches.Values) + foreach (var cache in _cbufferCaches) { cache.Dispose(); } - _cbufferCaches.Clear(); - GC.SuppressFinalize(this); _disposed = true; diff --git a/Ghost.Graphics/Data/Mesh.cs b/Ghost.Graphics/Data/Mesh.cs index c08f4d1..30cd4a0 100644 --- a/Ghost.Graphics/Data/Mesh.cs +++ b/Ghost.Graphics/Data/Mesh.cs @@ -1,5 +1,5 @@ using Ghost.Core; -using Ghost.Graphics.Contracts; +using Ghost.Graphics.D3D12; using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Helpers; using System.Numerics; @@ -16,8 +16,8 @@ public unsafe sealed class Mesh(int initialVertexCapacity = 256, int initialInde private Bounds _boundingBox; - private IResource? _vertexBuffer; - private IResource? _indexBuffer; + private GraphicsResource? _vertexBuffer; + private GraphicsResource? _indexBuffer; private VertexBufferView _vertexBufferView; private IndexBufferView _indexBufferView; @@ -204,8 +204,7 @@ public unsafe sealed class Mesh(int initialVertexCapacity = 256, int initialInde /// /// Uploads the mesh data to GPU resources. /// - /// The command buffer to record the upload commands. - public unsafe void UploadMeshData(ICommandBuffer cmb) + public unsafe void UploadMeshData() { if (VertexCount == 0 || IndexCount == 0) { @@ -220,17 +219,13 @@ public unsafe sealed class Mesh(int initialVertexCapacity = 256, int initialInde _vertexBuffer = GraphicsPipeline.ResourceAllocator.CreateCopyDestinationBuffer(vertexBufferSize); _indexBuffer = GraphicsPipeline.ResourceAllocator.CreateCopyDestinationBuffer(indexBufferSize); - var vertexUploadBuffer = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer(vertexBufferSize, false); - var indexUploadBuffer = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer(indexBufferSize, false); - - vertexUploadBuffer.SetData(_vertices.AsSpan()); - indexUploadBuffer.SetData(_indices.AsSpan()); - - cmb.CopyResource(_vertexBuffer, 0, vertexUploadBuffer, 0, vertexBufferSize); - cmb.CopyResource(_indexBuffer, 0, indexUploadBuffer, 0, indexBufferSize); - - cmb.BarrierTransition(_vertexBuffer, ResourceStates.CopyDest, ResourceStates.VertexAndConstantBuffer); - cmb.BarrierTransition(_indexBuffer, ResourceStates.CopyDest, ResourceStates.IndexBuffer); + using var uploadBatch = new ResourceUploadBatch(); + uploadBatch.Begin(); + uploadBatch.Upload(_vertexBuffer, _vertices.AsSpan()); + uploadBatch.Upload(_indexBuffer, _indices.AsSpan()); + uploadBatch.Transition(_vertexBuffer, ResourceStates.CopyDest, ResourceStates.VertexAndConstantBuffer); + uploadBatch.Transition(_indexBuffer, ResourceStates.CopyDest, ResourceStates.IndexBuffer); + uploadBatch.WaitForCompletion(uploadBatch.End()); _vertexBufferView = new VertexBufferView { diff --git a/Ghost.Graphics/Data/Texture2D.cs b/Ghost.Graphics/Data/Texture2D.cs new file mode 100644 index 0000000..c77766b --- /dev/null +++ b/Ghost.Graphics/Data/Texture2D.cs @@ -0,0 +1,357 @@ +using Ghost.Graphics.D3D12; +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.LowLevel.Helpers; +using System.Runtime.InteropServices; +using Win32; +using Win32.Graphics.Direct3D12; +using Win32.Graphics.Dxgi.Common; + +namespace Ghost.Graphics.Data; + +public unsafe class Texture2D : IDisposable +{ + private UnsafeArray _cpuData; + + private readonly GraphicsResource _resource; + private readonly ShaderResourceDescriptor _srvDescriptor; + + private bool _disposed; + + public uint Width + { + get; + } + + public uint Height + { + get; + } + + public Format Format + { + get; + } + + public uint BytesPerPixel + { + get; + } + + public uint Pitch + { + get; + } + + public uint DataSize + { + get; + } + + internal ShaderResourceDescriptor SRVDescriptor => _srvDescriptor; + + public GraphicsResource? Resource => _resource; + public ReadOnlySpan CpuData => _cpuData.AsSpan(); + + public Texture2D(uint width, uint height, Span data, Format format) + { + Width = width; + Height = height; + Format = format; + BytesPerPixel = GetBytesPerPixel(format); + Pitch = width * BytesPerPixel; + DataSize = Pitch * height; + + // Initialize CPU-side data + _cpuData = new((int)DataSize, Allocator.Persistent); + + if (!data.IsEmpty) + { + if (data.Length != DataSize) + { + throw new ArgumentException($"Data size mismatch. Expected {DataSize} bytes, got {data.Length} bytes."); + } + + data.CopyTo(_cpuData.AsSpan()); + } + + _resource = CreateGpuResource(); + _srvDescriptor = CreateShaderResourceView(); + } + private static uint GetBytesPerPixel(Format format) + { + return format switch + { + Format.R8G8B8A8Unorm => 4, + Format.R8G8B8A8UnormSrgb => 4, + Format.B8G8R8A8Unorm => 4, + Format.B8G8R8A8UnormSrgb => 4, + Format.R8G8B8A8Uint => 4, + Format.R8G8B8A8Sint => 4, + Format.R8G8B8A8Snorm => 4, + Format.R8G8Unorm => 2, + Format.R8G8Uint => 2, + Format.R8G8Sint => 2, + Format.R8G8Snorm => 2, + Format.R8Unorm => 1, + Format.R8Uint => 1, + Format.R8Sint => 1, + Format.R8Snorm => 1, + Format.A8Unorm => 1, + Format.R16G16B16A16Float => 8, + Format.R16G16B16A16Unorm => 8, + Format.R16G16B16A16Uint => 8, + Format.R16G16B16A16Sint => 8, + Format.R16G16B16A16Snorm => 8, + Format.R32G32B32A32Float => 16, + Format.R32G32B32A32Uint => 16, + Format.R32G32B32A32Sint => 16, + Format.R32G32B32Float => 12, + Format.R32G32B32Uint => 12, + Format.R32G32B32Sint => 12, + Format.R32G32Float => 8, + Format.R32G32Uint => 8, + Format.R32G32Sint => 8, + Format.R32Float => 4, + Format.R32Uint => 4, + Format.R32Sint => 4, + Format.R16G16Float => 4, + Format.R16G16Unorm => 4, + Format.R16G16Uint => 4, + Format.R16G16Sint => 4, + Format.R16G16Snorm => 4, + Format.R16Float => 2, + Format.R16Unorm => 2, + Format.R16Uint => 2, + Format.R16Sint => 2, + Format.R16Snorm => 2, + _ => throw new NotSupportedException($"Format {format} is not supported.") + }; + } + + private GraphicsResource CreateGpuResource() + { + var heapProperties = new HeapProperties(HeapType.Default); + var resourceDesc = ResourceDescription.Tex2D(Format, Width, Height); + + ComPtr textureResource = default; + GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateCommittedResource( + &heapProperties, + HeapFlags.None, + &resourceDesc, + ResourceStates.Common, + null, + __uuidof(), + textureResource.GetVoidAddressOf() + ); + + return new(textureResource.Move()); + } + + private ShaderResourceDescriptor CreateShaderResourceView() + { + var srvDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateSRV(); + + // Create the actual SRV + var srvDesc = new ShaderResourceViewDescription + { + Format = Format, + ViewDimension = SrvDimension.Texture2D, + Texture2D = new Texture2DSrv { MipLevels = 1 }, + Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING + }; + + GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateShaderResourceView(_resource.NativeResource.Ptr, &srvDesc, srvDescriptor.CpuHandle); + + return srvDescriptor; + } + + private void CopyBufferToTexture(CommandList cmd, GraphicsResource srcBuffer, GraphicsResource dstTexture) + { + // Calculate the proper footprint for the placed subresource + var resourceDesc = dstTexture.NativeResource.Ptr->GetDesc(); + + PlacedSubresourceFootprint footprint = new() + { + Footprint = new SubresourceFootprint + { + Format = Format, + Width = Width, + Height = Height, + Depth = 1, + RowPitch = (uint)((Pitch + 255) & ~255) // Align to 256 bytes + } + }; + + var srcLocation = new TextureCopyLocation(srcBuffer.NativeResource.Ptr, footprint); + var dstLocation = new TextureCopyLocation(dstTexture.NativeResource.Ptr, 0); + + cmd.NativeCommandList.Ptr->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, null); + } + + /// + /// Sets the entire texture data on the CPU side. + /// + /// The texture data to set + public void SetData(ReadOnlySpan data) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (data.Length != DataSize) + { + throw new ArgumentException($"Data size mismatch. Expected {DataSize} bytes, got {data.Length} bytes."); + } + + data.CopyTo(_cpuData.AsSpan()); + } + + /// + /// Sets the texture data for a specific region on the CPU side. + /// + /// The texture data to set + /// Starting X coordinate + /// Starting Y coordinate + /// Width of the region + /// Height of the region + public void SetData(ReadOnlySpan data, uint x, uint y, uint width, uint height) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (x + width > Width || y + height > Height) + { + throw new ArgumentException("Region extends beyond texture bounds."); + } + + var expectedSize = width * height * BytesPerPixel; + if (data.Length != expectedSize) + { + throw new ArgumentException($"Data size mismatch. Expected {expectedSize} bytes, got {data.Length} bytes."); + } + + for (uint row = 0; row < height; row++) + { + var srcOffset = (int)(row * width * BytesPerPixel); + var dstOffset = (int)((y + row) * Pitch + x * BytesPerPixel); + var rowSize = (int)(width * BytesPerPixel); + + data.Slice(srcOffset, rowSize).CopyTo(_cpuData.AsSpan().Slice(dstOffset, rowSize)); + } + } + + /// + /// Sets a single pixel value on the CPU side. + /// + /// X coordinate of the pixel + /// Y coordinate of the pixel + /// The color data for the pixel + public void SetPixel(uint x, uint y, ReadOnlySpan color) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (x >= Width || y >= Height) + { + throw new ArgumentException("Pixel coordinates are outside texture bounds."); + } + + if (color.Length != BytesPerPixel) + { + throw new ArgumentException($"Color data size mismatch. Expected {BytesPerPixel} bytes, got {color.Length} bytes."); + } + + var offset = (int)(y * Pitch + x * BytesPerPixel); + color.CopyTo(_cpuData.AsSpan().Slice(offset, (int)BytesPerPixel)); + } + + /// + /// Sets a single pixel value using generic color types. + /// + /// The color type (e.g., uint for RGBA32) + /// X coordinate of the pixel + /// Y coordinate of the pixel + /// The color value + public void SetPixel(uint x, uint y, T color) where T : unmanaged + { + if (sizeof(T) != BytesPerPixel) + { + throw new ArgumentException($"Color type size mismatch. Expected {BytesPerPixel} bytes, got {sizeof(T)} bytes."); + } + + var colorSpan = new ReadOnlySpan(&color, sizeof(T)); + SetPixel(x, y, colorSpan); + } + + /// + /// Gets a single pixel value from the CPU side data. + /// + /// X coordinate of the pixel + /// Y coordinate of the pixel + /// The pixel color data + public ReadOnlySpan GetPixel(uint x, uint y) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (x >= Width || y >= Height) + { + throw new ArgumentException("Pixel coordinates are outside texture bounds."); + } + + var offset = (int)(y * Pitch + x * BytesPerPixel); + return _cpuData.AsSpan().Slice(offset, (int)BytesPerPixel); + } + + /// + /// Gets a single pixel value as a generic color type. + /// + /// The color type (e.g., uint for RGBA32) + /// X coordinate of the pixel + /// Y coordinate of the pixel + /// The pixel color value + public T GetPixel(uint x, uint y) where T : unmanaged + { + if (sizeof(T) != BytesPerPixel) + { + throw new ArgumentException($"Color type size mismatch. Expected {BytesPerPixel} bytes, got {sizeof(T)} bytes."); + } + + var pixelData = GetPixel(x, y); + return MemoryMarshal.Read(pixelData); + } + + /// + /// Uploads the CPU-side texture data to the GPU resource. + /// + public void UploadTextureData() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + Format.GetSurfaceInfo((int)Width, (int)Height, out var rowPitch, out var slicePitch); + var initData = new SubresourceData() + { + pData = _cpuData.GetUnsafePtr(), + RowPitch = rowPitch, + SlicePitch = slicePitch + }; + + using var uploadBatch = new ResourceUploadBatch(); + uploadBatch.Begin(); + uploadBatch.Transition(_resource, ResourceStates.Common, ResourceStates.CopyDest); + uploadBatch.Upload(_resource, 0, &initData, 1); + uploadBatch.Transition(_resource, ResourceStates.CopyDest, ResourceStates.PixelShaderResource); + uploadBatch.WaitForCompletion(uploadBatch.End()); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _cpuData.Dispose(); + _resource.Dispose(); + + GraphicsPipeline.DescriptorAllocator.ReleaseSRV(_srvDescriptor); + + _disposed = true; + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/Data/Vertex.cs b/Ghost.Graphics/Data/Vertex.cs index 77e31c9..66d6935 100644 --- a/Ghost.Graphics/Data/Vertex.cs +++ b/Ghost.Graphics/Data/Vertex.cs @@ -17,7 +17,7 @@ public struct Vertex(Vector4 position, Vector4 normal, Vector4 tangent, Color128 private static readonly byte[] s_normalBytes = Encoding.UTF8.GetBytes("NORMAL"); private static readonly byte[] s_tangentBytes = Encoding.UTF8.GetBytes("TANGENT"); private static readonly byte[] s_colorBytes = Encoding.UTF8.GetBytes("COLOR"); - private static readonly byte[] s_uvBytes = Encoding.UTF8.GetBytes("UV"); + private static readonly byte[] s_uvBytes = Encoding.UTF8.GetBytes("TEXCOORD"); public static byte* PositionName => (byte*)Unsafe.AsPointer(ref s_positionBytes[0]); public static byte* NormalName => (byte*)Unsafe.AsPointer(ref s_normalBytes[0]); diff --git a/Ghost.Graphics/Examples/DescriptorAllocatorExample.cs b/Ghost.Graphics/Examples/DescriptorAllocatorExample.cs new file mode 100644 index 0000000..a6e115b --- /dev/null +++ b/Ghost.Graphics/Examples/DescriptorAllocatorExample.cs @@ -0,0 +1,178 @@ +using Ghost.Graphics.D3D12; +using Win32.Graphics.Dxgi.Common; + +namespace Ghost.Graphics.Examples; + +/// +/// Example demonstrating how to use the IDescriptorAllocator interface. +/// +public static class DescriptorAllocatorExample +{ + /// + /// Example showing basic descriptor allocation and release. + /// + public static void BasicDescriptorUsage() + { + // Note: In a real application, you would get the descriptor allocator from the graphics device + // var device = GraphicsPipeline.GetGraphicsDevice(); + // var descriptorAllocator = device.DescriptorAllocator; + + // For demonstration purposes, we'll show the interface usage + DescriptorAllocator descriptorAllocator = null!; // Would be obtained from graphics device + + // Allocate a single RTV descriptor + var rtvDescriptor = descriptorAllocator.AllocateRTV(); + + // Allocate multiple SRV descriptors + var srvDescriptors = descriptorAllocator.AllocateSRVs(4); + + // Allocate a DSV descriptor + var dsvDescriptor = descriptorAllocator.AllocateDSV(); + + // Allocate sampler descriptors + var samplerDescriptors = descriptorAllocator.AllocateSamplers(2); + + // Use the descriptors... + ProcessDescriptors(rtvDescriptor, srvDescriptors, dsvDescriptor, samplerDescriptors); + + // Release descriptors when done + descriptorAllocator.ReleaseRTV(rtvDescriptor); + descriptorAllocator.ReleaseSRVs(srvDescriptors); + descriptorAllocator.ReleaseDSV(dsvDescriptor); + descriptorAllocator.ReleaseSamplers(samplerDescriptors); + } + + /// + /// Example showing how to work with D3D12-specific descriptor handles. + /// + public static void D3D12SpecificUsage() + { + // In D3D12-specific code, you can cast to get the underlying handles + DescriptorAllocator descriptorAllocator = null!; // Would be obtained from graphics device + + var srvDescriptor = descriptorAllocator.AllocateSRV(); + + // For D3D12, you can cast to get the specific handles + if (srvDescriptor is ShaderResourceDescriptor d3d12Srv) + { + var cpuHandle = d3d12Srv.CpuHandle; + var gpuHandle = d3d12Srv.GpuHandle; + + // Use the handles for D3D12 API calls + // device.CreateShaderResourceView(resource, &srvDesc, cpuHandle); + // commandList.SetGraphicsRootDescriptorTable(0, gpuHandle); + } + + descriptorAllocator.ReleaseSRV(srvDescriptor); + } + + /// + /// Example showing descriptor management in a texture class. + /// + public static void TextureDescriptorUsage() + { + // This shows how you might integrate descriptor allocation in a texture class + DescriptorAllocator descriptorAllocator = null!; // Would be obtained from graphics device + + // Create a texture with SRV + var texture = new ExampleTexture2D(512, 512, Format.R8G8B8A8Unorm, descriptorAllocator); + + // The texture automatically manages its SRV descriptor + var srvDescriptor = texture.GetShaderResourceView(); + + // Use the texture... + + // Dispose will automatically release the descriptor + texture.Dispose(); + } + + private static void ProcessDescriptors( + RenderTargetDescriptor rtv, + ShaderResourceDescriptor[] srvs, + DepthStencilDescriptor dsv, + SamplerDescriptor[] samplers) + { + // Process the descriptors - check validity, shader visibility, etc. + if (rtv.IsValid) + { + // Use RTV descriptor + } + + foreach (var srv in srvs) + { + if (srv.IsValid && srv.IsShaderVisible) + { + // Use SRV descriptor in shaders + } + } + + if (dsv.IsValid) + { + // Use DSV descriptor + } + + foreach (var sampler in samplers) + { + if (sampler.IsValid && sampler.IsShaderVisible) + { + // Use sampler descriptor in shaders + } + } + } +} + +/// +/// Example texture class showing descriptor integration. +/// +internal class ExampleTexture2D : IDisposable +{ + private readonly DescriptorAllocator _descriptorAllocator; + private readonly ShaderResourceDescriptor _srvDescriptor; + private readonly uint _width; + private readonly uint _height; + private readonly Format _format; + private bool _disposed; + + public ExampleTexture2D(uint width, uint height, Format format, DescriptorAllocator descriptorAllocator) + { + _width = width; + _height = height; + _format = format; + _descriptorAllocator = descriptorAllocator; + + // Allocate SRV descriptor for this texture + _srvDescriptor = _descriptorAllocator.AllocateSRV(); + + // Create the actual texture resource and SRV... + CreateTextureResource(); + } + + public ShaderResourceDescriptor GetShaderResourceView() => _srvDescriptor; + + private void CreateTextureResource() + { + // Create D3D12 texture resource + // Create SRV using the allocated descriptor + + // For D3D12: + if (_srvDescriptor is ShaderResourceDescriptor d3d12Srv) + { + var cpuHandle = d3d12Srv.CpuHandle; + // device.CreateShaderResourceView(textureResource, &srvDesc, cpuHandle); + } + } + + public void Dispose() + { + if (_disposed) + return; + + // Release the descriptor + _descriptorAllocator.ReleaseSRV(_srvDescriptor); + + // Dispose texture resource... + + _disposed = true; + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/Ghost.Graphics.csproj b/Ghost.Graphics/Ghost.Graphics.csproj index 33f5a54..dbf9d82 100644 --- a/Ghost.Graphics/Ghost.Graphics.csproj +++ b/Ghost.Graphics/Ghost.Graphics.csproj @@ -17,6 +17,8 @@ + + @@ -27,14 +29,20 @@ - - ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Image\bin\Release\net9.0\Misaki.HighPerformance.Image.dll - ..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.LowLevel\bin\Release\net9.0\Misaki.HighPerformance.LowLevel.dll + + True diff --git a/Ghost.Graphics/GraphicsPipeline.cs b/Ghost.Graphics/GraphicsPipeline.cs index da3ed0e..70c1720 100644 --- a/Ghost.Graphics/GraphicsPipeline.cs +++ b/Ghost.Graphics/GraphicsPipeline.cs @@ -1,196 +1,154 @@ -using Ghost.Core; -using Ghost.Graphics.Contracts; -using Ghost.Graphics.D3D12; -using Ghost.Graphics.Data; -using System.Runtime.CompilerServices; +using Ghost.Graphics.D3D12; namespace Ghost.Graphics; public static class GraphicsPipeline { - internal const int _FRAME_COUNT = 2; + internal const uint _FRAME_COUNT = 2; - private static IGraphicsDevice? _graphicsDevice; - private static IResourceAllocator? _resourceAllocator; - - private static Thread? _renderThread; - private static AutoResetEvent[]? _cpuReadyEvent; - private static AutoResetEvent[]? _gpuReadyEvent; - - private static uint _cpuFenceValue; - private static uint _gpuFenceValue; - - private static bool _initialized; - private static bool _isRunning; - - internal static uint CPUFenceValue => _cpuFenceValue; - internal static uint GPUFenceValue => _gpuFenceValue; - internal static bool IsRunning => _isRunning; - - internal static IGraphicsDevice GraphicsDevice + private readonly struct FrameResource : IDisposable { - get - { - if (_graphicsDevice == null) - { - throw new InvalidOperationException("Graphics pipeline is not initialized."); - } + public readonly AutoResetEvent cpuReadyEvent; + public readonly AutoResetEvent gpuReadyEvent; - return _graphicsDevice; + public FrameResource() + { + cpuReadyEvent = new(false); + gpuReadyEvent = new(true); + } + + public void Dispose() + { + cpuReadyEvent?.Dispose(); + gpuReadyEvent?.Dispose(); } } - internal static IResourceAllocator ResourceAllocator + private static GraphicsDevice? s_graphicsDevice; + private static DescriptorAllocator? s_descriptorAllocator; + private static ResourceAllocator? s_resourceAllocator; + + private static Thread? s_renderThread; + private static FrameResource[]? s_frameResources; + + private static uint s_frameIndex; + private static uint s_cpuFenceValue; + private static uint s_gpuFenceValue; + + private static bool s_initialized; + private static bool s_isRunning; + + internal static uint CPUFenceValue => s_cpuFenceValue; + internal static uint GPUFenceValue => s_gpuFenceValue; + internal static bool IsRunning => s_isRunning; + + internal static GraphicsDevice GraphicsDevice => s_graphicsDevice ?? throw new InvalidOperationException("Graphics device is not initialized."); + internal static ResourceAllocator ResourceAllocator => s_resourceAllocator ?? throw new InvalidOperationException("Resource allocator is not initialized."); + internal static DescriptorAllocator DescriptorAllocator => s_descriptorAllocator ?? throw new InvalidOperationException("Descriptor allocator is not initialized."); + + internal static void Initialize() { - get - { - if (_resourceAllocator == null) - { - throw new InvalidOperationException("Resource allocator is not initialized."); - } + s_graphicsDevice = new GraphicsDevice(); + s_descriptorAllocator = new DescriptorAllocator(); + s_resourceAllocator = new ResourceAllocator(); - return _resourceAllocator; - } - } - - public static GraphicsAPI CurrentAPI - { - get; - private set; - } - - internal static void Initialize(GraphicsAPI api) - { - switch (api) - { - case GraphicsAPI.D3D12: - _graphicsDevice = new D3D12GraphicsDevice(); - _resourceAllocator = new D3D12ResourceAllocator(); - break; - default: - throw new NotSupportedException($"Graphics API {api} is not supported."); - } - - _renderThread = new Thread(RenderLoop) + s_renderThread = new Thread(RenderLoop) { IsBackground = true, Name = "Graphics Render Thread", Priority = ThreadPriority.Normal }; - _cpuReadyEvent = new AutoResetEvent[_FRAME_COUNT]; - _gpuReadyEvent = new AutoResetEvent[_FRAME_COUNT]; + s_frameResources = new FrameResource[_FRAME_COUNT]; for (var i = 0; i < _FRAME_COUNT; i++) { - _cpuReadyEvent[i] = new(false); - _gpuReadyEvent[i] = new(true); + s_frameResources[i] = new FrameResource(); } - CurrentAPI = api; - - _initialized = true; + s_initialized = true; } private static void RenderLoop() { - while (_isRunning) + while (s_isRunning) { - if (_graphicsDevice == null) - { - throw new ArgumentException("Renderer has been disposed or is not initialized."); - } + s_frameIndex = s_gpuFenceValue % _FRAME_COUNT; + var frameResource = s_frameResources![s_frameIndex]; - var eventIndex = (int)(_gpuFenceValue % _FRAME_COUNT); - _cpuReadyEvent![eventIndex].WaitOne(); + frameResource.cpuReadyEvent.WaitOne(); - _graphicsDevice.InitializePendingRenderers(); + s_graphicsDevice!.InitializePendingRenderers(); - foreach (var renderer in _graphicsDevice.Renderers) + foreach (var renderer in s_graphicsDevice.Renderers) { renderer.ExecutePendingResize(); renderer.Render(); } - _gpuFenceValue++; - _gpuReadyEvent![eventIndex].Set(); + s_gpuFenceValue++; + frameResource.gpuReadyEvent.Set(); - _resourceAllocator!.ReleaseTempResource(); + s_resourceAllocator!.ReleaseTempResource(); } } - internal static bool WaitForGPUReady(int timeOut = -1) { - if (_gpuReadyEvent == null) + if (s_frameResources == null) { throw new InvalidOperationException("Graphics pipeline is not initialized."); } - var eventIndex = (int)(_cpuFenceValue % _FRAME_COUNT); - return _gpuReadyEvent[eventIndex].WaitOne(timeOut); + var eventIndex = (int)(s_cpuFenceValue % _FRAME_COUNT); + return s_frameResources[eventIndex].gpuReadyEvent.WaitOne(timeOut); } internal static void SignalCPUReady() { - if (_cpuReadyEvent == null) + if (s_frameResources == null) { throw new InvalidOperationException("Graphics pipeline is not initialized."); } - var eventIndex = (int)(_cpuFenceValue % _FRAME_COUNT); - _cpuFenceValue++; - _cpuReadyEvent[eventIndex].Set(); + var eventIndex = (int)(s_cpuFenceValue % _FRAME_COUNT); + s_cpuFenceValue++; + s_frameResources[eventIndex].cpuReadyEvent.Set(); } internal static void Start() { - if (_isRunning || !_initialized) + if (s_isRunning || !s_initialized) { return; } - _isRunning = true; - _renderThread!.Start(); + s_isRunning = true; + s_renderThread!.Start(); } internal static void Stop() { - _isRunning = false; - _renderThread?.Join(); + s_isRunning = false; + s_renderThread?.Join(); } internal static void Shutdown() { Stop(); - _graphicsDevice?.Dispose(); - _resourceAllocator?.Dispose(); + s_resourceAllocator?.Dispose(); + s_descriptorAllocator?.Dispose(); + s_graphicsDevice?.Dispose(); - _graphicsDevice = null; - _renderThread = null; - _initialized = false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static T GetGraphicsDevice() - where T : class, IGraphicsDevice - { - if (T.TargetAPI != CurrentAPI) + if (s_frameResources != null) { - throw new InvalidOperationException($"No graphics device of type {typeof(T)} available for the current API."); + foreach (var frameResource in s_frameResources) + { + frameResource.Dispose(); + } + s_frameResources = null; } - return Unsafe.As(GraphicsDevice); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Result CheckAPI(GraphicsAPI expectedAPI) - { - if (CurrentAPI != expectedAPI) - { - return Result.Failure($"Expected API {expectedAPI}, but got {CurrentAPI}."); - } - - return Result.Success(); + s_initialized = false; } } \ No newline at end of file diff --git a/Ghost.Graphics/RenderPasses/BindlessMeshRenderPass.cs b/Ghost.Graphics/RenderPasses/BindlessMeshRenderPass.cs new file mode 100644 index 0000000..6eb35fb --- /dev/null +++ b/Ghost.Graphics/RenderPasses/BindlessMeshRenderPass.cs @@ -0,0 +1,677 @@ +using Ghost.Graphics.Contracts; +using Ghost.Graphics.D3D12; +using Ghost.Graphics.D3D12.Utilities; +using Ghost.Graphics.Data; +using Ghost.Graphics.Utilities; +using StbImageSharp; +using System.Runtime.InteropServices; +using Win32; +using Win32.Graphics.Direct3D; +using Win32.Graphics.Direct3D.Dxc; +using Win32.Graphics.Direct3D12; +using Win32.Graphics.Dxgi.Common; + +namespace Ghost.Graphics.RenderPasses; + +/// +/// Modern bindless texture implementation using SM 6.6 with ResourceDescriptorHeap +/// and D3D12_ROOT_SIGNATURE_FLAG_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED +/// +internal unsafe class BindlessMeshRenderPass : IRenderPass +{ + private const string _HLSL_SOURCE = @" +cbuffer ConstantBuffer : register(b0) +{ + float4 _Color; + uint _TextureIndex1; + uint _TextureIndex2; + uint _TextureIndex3; + uint _TextureIndex4; +}; + +// SM 6.6 approach - no need for explicit texture arrays +// ResourceDescriptorHeap is a global descriptor heap access mechanism +// This allows direct indexing into any texture in the bound descriptor heap +SamplerState _MainSampler : register(s0); + +struct VertexInput +{ + float4 position : POSITION; + float4 color : COLOR; + float4 uv : TEXCOORD0; +}; + +struct PixelInput +{ + float4 position : SV_POSITION; + float4 color : COLOR; + float4 uv : TEXCOORD0; +}; + +PixelInput VSMain(VertexInput input) +{ + PixelInput output; + output.position = input.position; + output.color = input.color; + output.uv = input.uv; + return output; +} + +float4 PSMain(PixelInput input) : SV_TARGET +{ + // SM 6.6 Modern Bindless Approach: + // ResourceDescriptorHeap[index] directly accesses any texture in the heap + // No need for explicit texture arrays or descriptor table binding + // This is enabled by D3D12_ROOT_SIGNATURE_FLAG_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED + Texture2D tex1 = ResourceDescriptorHeap[_TextureIndex1]; + Texture2D tex2 = ResourceDescriptorHeap[_TextureIndex2]; + Texture2D tex3 = ResourceDescriptorHeap[_TextureIndex3]; + Texture2D tex4 = ResourceDescriptorHeap[_TextureIndex4]; + + // Sample the textures normally + float4 color1 = tex1.Sample(_MainSampler, input.uv.xy); + float4 color2 = tex2.Sample(_MainSampler, input.uv.xy); + float4 color3 = tex3.Sample(_MainSampler, input.uv.xy); + float4 color4 = tex4.Sample(_MainSampler, input.uv.xy); + + // Blend all textures together (simple average) + float4 blendedColor = (color1 + color2 + color3 + color4) * 0.25f; + + return blendedColor * _Color; +} +"; + + private Mesh? _mesh; + + // Direct D3D12 resources for modern bindless implementation + private ComPtr _pipelineState; + private ComPtr _rootSignature; + private ComPtr _constantBuffer; + private void* _constantBufferMappedData; + + // Bindless texture resources + private ComPtr[] _textureResources; + private ComPtr _bindlessHeap; + private ComPtr _samplerHeap; + private CpuDescriptorHandle _bindlessHeapStart; + private GpuDescriptorHandle _bindlessHeapGpuStart; + private uint _descriptorSize; + + // Texture upload resources + private ComPtr[] _uploadBuffers; + private uint[] _textureWidths; + private uint[] _textureHeights; + private uint[] _texturePitches; + private bool _texturesUploaded = false; + + // Texture file paths for this demo + private readonly string[] _textureFiles = [ + "C:/Users/Misaki/Downloads/Im/Icon.png", + "C:/Users/Misaki/Downloads/Im/Backdrop.jpg", + "C:/Users/Misaki/Downloads/Im/101167591_p0.png", + "C:/Users/Misaki/Downloads/Im/yande.re 1134666 blue_archive nakamasa_ichika sugarhigh.jpg" + ]; + + // Constant buffer data structure + private struct ConstantBufferData + { + public System.Numerics.Vector4 Color; + public uint TextureIndex1; + public uint TextureIndex2; + public uint TextureIndex3; + public uint TextureIndex4; + } + + public void Initialize(CommandList cmd) + { + _mesh = MeshBuilder.CreateCube(0.75f); + _mesh.UploadMeshData(); + + CreateModernBindlessShader(); + CreateConstantBuffer(); + CreateBindlessTextures(); + UploadTextureData(cmd); + UpdateConstantBuffer(); + } + + private void CreateModernBindlessShader() + { + var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr; + + var vsCode = CompileShaderDXC(_HLSL_SOURCE, "VSMain", "vs_6_6"); + var psCode = CompileShaderDXC(_HLSL_SOURCE, "PSMain", "ps_6_6"); + + CreateModernBindlessRootSignature(); + CreatePipelineState(vsCode, psCode); + } + + private byte[] CompileShaderDXC(string source, string entryPoint, string profile) + { + using ComPtr compiler = default; + using ComPtr utils = default; + + // Create DXC compiler and utils + DxcCreateInstance(CLSID_DxcCompiler, __uuidof(), compiler.GetVoidAddressOf()); + DxcCreateInstance(CLSID_DxcUtils, __uuidof(), utils.GetVoidAddressOf()); + + // Create source blob + using ComPtr sourceBlob = default; + var sourceBytes = System.Text.Encoding.UTF8.GetBytes(source); + fixed (byte* sourceBytesPtr = sourceBytes) + { + utils.Get()->CreateBlob(sourceBytesPtr, (uint)sourceBytes.Length, DXC_CP_UTF8, sourceBlob.GetAddressOf()); + } + + // Prepare compilation arguments + var argsArray = new string[] + { + "-T", profile, // Target profile (vs_6_6, ps_6_6) + "-E", entryPoint, // Entry point + "-HV", "2021", // HLSL version 2021 (required for SM 6.6) + "-enable-16bit-types", // Enable 16-bit types + "-O3", // Optimization level + "-Qstrip_debug", // Strip debug info for release + "-Qstrip_reflect" // Strip reflection info for release + }; + + // Convert to wide strings (DXC expects LPCWSTR) + var wideArgs = new nuint[argsArray.Length]; + var argPointers = new IntPtr[argsArray.Length]; + + for (var i = 0; i < argsArray.Length; i++) + { + argPointers[i] = Marshal.StringToHGlobalUni(argsArray[i]); + wideArgs[i] = (nuint)argPointers[i]; + } + + try + { + // Compile shader + using ComPtr result = default; + fixed (nuint* argsPtr = wideArgs) + { + var buffer = new DxcBuffer + { + Ptr = sourceBlob.Get()->GetBufferPointer(), + Size = sourceBlob.Get()->GetBufferSize(), + Encoding = DXC_CP_UTF8 + }; + + compiler.Get()->Compile(&buffer, (char**)argsPtr, (uint)argsArray.Length, null, __uuidof(), result.GetVoidAddressOf()); + } + + // Check compilation result + HResult hrStatus; + result.Get()->GetStatus(&hrStatus); + if (hrStatus.Failure) + { + // Get error messages + using ComPtr errorBlob = default; + result.Get()->GetErrorBuffer(errorBlob.GetAddressOf()); + + if (errorBlob.Get() != null) + { + var errorMessage = Marshal.PtrToStringUni((IntPtr)errorBlob.Get()->GetBufferPointer()); + throw new Exception($"DXC shader compilation failed: {errorMessage}"); + } + else + { + throw new Exception("DXC shader compilation failed with unknown error"); + } + } + + // Get compiled bytecode + using ComPtr bytecodeBlob = default; + result.Get()->GetResult(bytecodeBlob.GetAddressOf()); + + if (bytecodeBlob.Get() == null) + { + throw new Exception("DXC compilation succeeded but no bytecode was produced"); + } + + // Copy bytecode to managed array + var bytecodeSize = (int)bytecodeBlob.Get()->GetBufferSize(); + var bytecode = new byte[bytecodeSize]; + + fixed (byte* bytecodePtr = bytecode) + { + Buffer.MemoryCopy(bytecodeBlob.Get()->GetBufferPointer(), bytecodePtr, bytecodeSize, bytecodeSize); + } + + return bytecode; + } + finally + { + // Free allocated wide strings + for (var i = 0; i < argPointers.Length; i++) + { + Marshal.FreeHGlobal(argPointers[i]); + } + } + } + + private void CreateModernBindlessRootSignature() + { + var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr; + + // Modern approach: Only CBV + Sampler, no descriptor tables needed + // The CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED flag allows direct heap access + var rootParameters = new RootParameter1[2]; + + // CBV for constant buffer + rootParameters[0] = new RootParameter1 + { + ParameterType = RootParameterType.Cbv, + ShaderVisibility = ShaderVisibility.All, + Descriptor = new RootDescriptor1(0, 0), // b0 + }; + + // Sampler descriptor table (still needed for samplers) + var samplerRanges = new DescriptorRange1[1]; + samplerRanges[0] = new DescriptorRange1 + { + RangeType = DescriptorRangeType.Sampler, + NumDescriptors = 1, + BaseShaderRegister = 0, // s0 + RegisterSpace = 0, + Flags = DescriptorRangeFlags.None, + OffsetInDescriptorsFromTableStart = 0 + }; + + fixed (DescriptorRange1* samplerRangesPtr = samplerRanges) + { + rootParameters[1] = new RootParameter1 + { + ParameterType = RootParameterType.DescriptorTable, + ShaderVisibility = ShaderVisibility.All, + DescriptorTable = new RootDescriptorTable1(1, samplerRangesPtr) + }; + } + + // Create root signature with the modern flag + fixed (RootParameter1* rootParamsPtr = rootParameters) + { + var rootSignatureDesc = new RootSignatureDescription1 + { + NumParameters = (uint)rootParameters.Length, + pParameters = rootParamsPtr, + NumStaticSamplers = 0, + pStaticSamplers = null, + // Key difference: Use the modern flag for direct heap indexing + Flags = RootSignatureFlags.AllowInputAssemblerInputLayout | + RootSignatureFlags.CbvSrvUavHeapDirectlyIndexed + }; + + var versionedDesc = new VersionedRootSignatureDescription + { + Version = RootSignatureVersion.V1_1, + Desc_1_1 = rootSignatureDesc + }; + + using ComPtr signature = default; + using ComPtr error = default; + + D3D12SerializeVersionedRootSignature(&versionedDesc, signature.GetAddressOf(), error.GetAddressOf()); + + device->CreateRootSignature(0, signature.Get()->GetBufferPointer(), signature.Get()->GetBufferSize(), __uuidof(), _rootSignature.GetVoidAddressOf()); + } + } + + private void CreatePipelineState(byte[] vsCode, byte[] psCode) + { + var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr; + + fixed (byte* vsPtr = vsCode) + fixed (byte* psPtr = psCode) + { + + var psoDesc = new GraphicsPipelineStateDescription + { + pRootSignature = _rootSignature.Get(), + VS = new ShaderBytecode(vsPtr, (nuint)vsCode.Length), + PS = new ShaderBytecode(psPtr, (nuint)psCode.Length), + InputLayout = D3D12PipelineResource.InputLayoutDescription, + RasterizerState = RasterizerDescription.CullNone, + BlendState = BlendDescription.Opaque, + DepthStencilState = DepthStencilDescription.Default, + SampleMask = uint.MaxValue, + PrimitiveTopologyType = PrimitiveTopologyType.Triangle, + NumRenderTargets = 1, + SampleDesc = new SampleDescription(1, 0), + DSVFormat = Format.Unknown, + }; + + // Fix: Use the correct swap chain back buffer format + psoDesc.RTVFormats[0] = D3D12PipelineResource.SWAP_CHAIN_BACK_BUFFER_FORMAT; + + device->CreateGraphicsPipelineState(&psoDesc, __uuidof(), _pipelineState.GetVoidAddressOf()); + } + } + + private void CreateConstantBuffer() + { + var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr; + + // Create constant buffer + var cbSize = (uint)((sizeof(ConstantBufferData) + 255) & ~255); // Align to 256 bytes + var heapProps = new HeapProperties(HeapType.Upload); + var bufferDesc = ResourceDescription.Buffer(cbSize); + + device->CreateCommittedResource( + &heapProps, + HeapFlags.None, + &bufferDesc, + ResourceStates.GenericRead, + null, + __uuidof(), + _constantBuffer.GetVoidAddressOf() + ); + + // Map constant buffer + fixed (void** mappedDataPtr = &_constantBufferMappedData) + { + _constantBuffer.Get()->Map(0, null, mappedDataPtr); + } + } + + private void UpdateConstantBuffer() + { + var cbData = new ConstantBufferData + { + Color = new(1.0f, 1.0f, 1.0f, 1.0f), + TextureIndex1 = 0, + TextureIndex2 = 1, + TextureIndex3 = 2, + TextureIndex4 = 3 + }; + + *(ConstantBufferData*)_constantBufferMappedData = cbData; + } + + private void CreateBindlessTextures() + { + var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr; + var textureCount = _textureFiles.Length; + + // Initialize arrays + _textureResources = new ComPtr[textureCount]; + _uploadBuffers = new ComPtr[textureCount]; + _textureWidths = new uint[textureCount]; + _textureHeights = new uint[textureCount]; + _texturePitches = new uint[textureCount]; + + // Create bindless descriptor heap + var bindlessHeapDesc = new DescriptorHeapDescription + { + Type = DescriptorHeapType.CbvSrvUav, + NumDescriptors = 1000, + Flags = DescriptorHeapFlags.ShaderVisible + }; + + device->CreateDescriptorHeap(&bindlessHeapDesc, __uuidof(), _bindlessHeap.GetVoidAddressOf()); + + // Create sampler heap + var samplerHeapDesc = new DescriptorHeapDescription + { + Type = DescriptorHeapType.Sampler, + NumDescriptors = 1, + Flags = DescriptorHeapFlags.ShaderVisible + }; + + device->CreateDescriptorHeap(&samplerHeapDesc, __uuidof(), _samplerHeap.GetVoidAddressOf()); + + // Get descriptor handles + _bindlessHeapStart = _bindlessHeap.Get()->GetCPUDescriptorHandleForHeapStart(); + _bindlessHeapGpuStart = _bindlessHeap.Get()->GetGPUDescriptorHandleForHeapStart(); + _descriptorSize = device->GetDescriptorHandleIncrementSize(DescriptorHeapType.CbvSrvUav); + + // Create sampler + var samplerDesc = new SamplerDescription + { + Filter = Filter.MinMagMipLinear, + AddressU = TextureAddressMode.Wrap, + AddressV = TextureAddressMode.Wrap, + AddressW = TextureAddressMode.Wrap, + MipLODBias = 0, + MaxAnisotropy = 1, + MinLOD = 0, + MaxLOD = float.MaxValue + }; + + // Set border color manually + samplerDesc.BorderColor[0] = 0; + samplerDesc.BorderColor[1] = 0; + samplerDesc.BorderColor[2] = 0; + samplerDesc.BorderColor[3] = 0; + + var samplerHandle = _samplerHeap.Get()->GetCPUDescriptorHandleForHeapStart(); + device->CreateSampler(&samplerDesc, samplerHandle); + + // Load and create textures + for (var i = 0; i < textureCount; i++) + { + CreateTextureResource(i); + } + } + + private void CreateTextureResource(int index) + { + var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr; + + // Load image data + using var stream = new FileStream(_textureFiles[index], FileMode.Open, FileAccess.Read); + var image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); + + var width = (uint)image.Width; + var height = (uint)image.Height; + uint bytesPerPixel = 4; + var pitch = width * bytesPerPixel; + + // Store texture dimensions + _textureWidths[index] = width; + _textureHeights[index] = height; + _texturePitches[index] = (pitch + 255) & ~255u; + + // Create texture resource + var textureDesc = new ResourceDescription + { + Dimension = ResourceDimension.Texture2D, + Alignment = 0, + Width = width, + Height = height, + DepthOrArraySize = 1, + MipLevels = 1, + Format = Format.R8G8B8A8Unorm, + SampleDesc = new SampleDescription(1, 0), + Layout = TextureLayout.Unknown, + Flags = ResourceFlags.None + }; + + var heapProps = new HeapProperties(HeapType.Default); + device->CreateCommittedResource( + &heapProps, + HeapFlags.None, + &textureDesc, + ResourceStates.CopyDest, + null, + __uuidof(), + _textureResources[index].GetVoidAddressOf() + ); + + // Create upload buffer + var uploadBufferSize = GetRequiredIntermediateSize(_textureResources[index].Get(), 0, 1); + var uploadHeapProps = new HeapProperties(HeapType.Upload); + var uploadBufferDesc = ResourceDescription.Buffer(uploadBufferSize); + + device->CreateCommittedResource( + &uploadHeapProps, + HeapFlags.None, + &uploadBufferDesc, + ResourceStates.GenericRead, + null, + __uuidof(), + _uploadBuffers[index].GetVoidAddressOf() + ); + + // Map and copy texture data + void* mappedData = null; + _uploadBuffers[index].Get()->Map(0, null, &mappedData); + + var srcData = image.Data.AsSpan(); + var dstSpan = new Span(mappedData, (int)uploadBufferSize); + + // Copy row by row with proper alignment + for (var y = 0; y < height; y++) + { + var srcRow = srcData.Slice(y * (int)pitch, (int)pitch); + var dstRow = dstSpan.Slice(y * (int)_texturePitches[index], (int)pitch); + srcRow.CopyTo(dstRow); + } + + _uploadBuffers[index].Get()->Unmap(0, null); + + // Create SRV in the bindless heap + var srvDesc = new ShaderResourceViewDescription + { + Format = Format.R8G8B8A8Unorm, + ViewDimension = Win32.Graphics.Direct3D12.SrvDimension.Texture2D, + Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING, + Texture2D = new Texture2DSrv + { + MostDetailedMip = 0, + MipLevels = 1, + PlaneSlice = 0, + ResourceMinLODClamp = 0.0f + } + }; + + // Calculate SRV handle for this texture + var srvHandle = new CpuDescriptorHandle + { + ptr = _bindlessHeapStart.ptr + (nuint)(index * _descriptorSize) + }; + + device->CreateShaderResourceView(_textureResources[index].Get(), &srvDesc, srvHandle); + } + + private void UploadTextureData(CommandList cmd) + { + var commandList = cmd.NativeCommandList.Ptr; + + for (var i = 0; i < _textureResources.Length; i++) + { + // Copy from upload buffer to texture + var srcLocation = new TextureCopyLocation(_uploadBuffers[i].Get(), new PlacedSubresourceFootprint + { + Offset = 0, + Footprint = new SubresourceFootprint + { + Format = Format.R8G8B8A8Unorm, + Width = _textureWidths[i], + Height = _textureHeights[i], + Depth = 1, + RowPitch = _texturePitches[i] + } + }); + + var dstLocation = new TextureCopyLocation(_textureResources[i].Get(), 0); + + commandList->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, null); + + // Transition texture to shader resource + var barrier = new ResourceBarrier + { + Type = ResourceBarrierType.Transition, + Flags = ResourceBarrierFlags.None, + Transition = new ResourceTransitionBarrier + { + pResource = _textureResources[i].Get(), + StateBefore = ResourceStates.CopyDest, + StateAfter = ResourceStates.PixelShaderResource, + Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES + } + }; + commandList->ResourceBarrier(1, &barrier); + } + + _texturesUploaded = true; + } + + private static ulong GetRequiredIntermediateSize(ID3D12Resource* destinationResource, uint firstSubresource, uint numSubresources) + { + var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr; + var resourceDesc = destinationResource->GetDesc(); + + ulong requiredSize = 0; + device->GetCopyableFootprints(&resourceDesc, firstSubresource, numSubresources, 0, null, null, null, &requiredSize); + + return requiredSize; + } + + public void Execute(CommandList cmd) + { + var commandList = cmd.NativeCommandList.Ptr; + + // Set root signature and pipeline state + commandList->SetGraphicsRootSignature(_rootSignature.Get()); + commandList->SetPipelineState(_pipelineState.Get()); + + // Set descriptor heaps - this is crucial for modern bindless + var heaps = stackalloc ID3D12DescriptorHeap*[2]; + heaps[0] = _bindlessHeap.Get(); // CBV_SRV_UAV heap for direct indexing + heaps[1] = _samplerHeap.Get(); // Sampler heap + commandList->SetDescriptorHeaps(2, heaps); + + // Bind constant buffer + commandList->SetGraphicsRootConstantBufferView(0, _constantBuffer.Get()->GetGPUVirtualAddress()); + + // Bind sampler descriptor table + var samplerGpuHandle = _samplerHeap.Get()->GetGPUDescriptorHandleForHeapStart(); + commandList->SetGraphicsRootDescriptorTable(1, samplerGpuHandle); + + // No need to explicitly bind texture descriptor table! + // The textures are accessed directly via ResourceDescriptorHeap[index] + + // Draw mesh + commandList->IASetPrimitiveTopology(PrimitiveTopology.TriangleList); + commandList->IASetVertexBuffers(0, 1, _mesh!.VertexBufferView); + commandList->IASetIndexBuffer(_mesh.IndexBufferView); + commandList->DrawIndexedInstanced(_mesh.IndexCount, 1, 0, 0, 0); + } + + public void Dispose() + { + _mesh?.Dispose(); + + // Unmap constant buffer + if (_constantBuffer.Get() != null) + { + _constantBuffer.Get()->Unmap(0, null); + } + + _pipelineState.Dispose(); + _rootSignature.Dispose(); + _constantBuffer.Dispose(); + + // Dispose texture resources + if (_textureResources != null) + { + for (var i = 0; i < _textureResources.Length; i++) + { + _textureResources[i].Dispose(); + } + } + + // Dispose upload buffers + if (_uploadBuffers != null) + { + for (var i = 0; i < _uploadBuffers.Length; i++) + { + _uploadBuffers[i].Dispose(); + } + } + + _bindlessHeap.Dispose(); + _samplerHeap.Dispose(); + } +} \ No newline at end of file diff --git a/Ghost.Graphics/RenderPasses/MeshRenderPass.cs b/Ghost.Graphics/RenderPasses/MeshRenderPass.cs index dd2cc73..1d45f50 100644 --- a/Ghost.Graphics/RenderPasses/MeshRenderPass.cs +++ b/Ghost.Graphics/RenderPasses/MeshRenderPass.cs @@ -1,9 +1,13 @@ using Ghost.Graphics.Contracts; +using Ghost.Graphics.D3D12; using Ghost.Graphics.Data; using Ghost.Graphics.Shading; using Ghost.Graphics.Utilities; -using System.Drawing; -using System.Numerics; +using StbImageSharp; +using Win32; +using Win32.Graphics.Direct3D; +using Win32.Graphics.Direct3D12; +using Win32.Graphics.Dxgi.Common; namespace Ghost.Graphics.RenderPasses; @@ -15,29 +19,35 @@ cbuffer ConstantBuffer : register(b0) float4 _Color; }; +Texture2D _MainTex : register(t0); +SamplerState _MainSampler : register(s0); + struct VertexInput { - float3 position : POSITION; + float4 position : POSITION; float4 color : COLOR; + float4 uv : TEXCOORD0; }; struct PixelInput { float4 position : SV_POSITION; float4 color : COLOR; + float4 uv : TEXCOORD0; }; PixelInput VSMain(VertexInput input) { PixelInput output; - output.position = float4(input.position, 1.0f); + output.position = input.position; output.color = input.color; + output.uv = input.uv; return output; } float4 PSMain(PixelInput input) : SV_TARGET { - return float4(_Color.xyz, 1.0); + return _MainTex.Sample(_MainSampler, input.uv.xy); } "; @@ -45,21 +55,218 @@ float4 PSMain(PixelInput input) : SV_TARGET private Shader? _shader; private Material? _material; - public void Initialize(ICommandBuffer cmb) + // Direct D3D12 resources for texture + private ComPtr _textureResource; + private ComPtr _srvHeap; + private CpuDescriptorHandle _srvHandle; + private GpuDescriptorHandle _srvGpuHandle; + private uint _srvDescriptorSize; + + // Additional fields for deferred texture upload + private ComPtr _uploadBuffer; + private uint _textureWidth; + private uint _textureHeight; + private uint _texturePitch; + private bool _textureUploaded = false; + + public void Initialize(CommandList cmd) { _mesh = MeshBuilder.CreateCube(0.25f); - _mesh.UploadMeshData(cmb); + _mesh.UploadMeshData(); _shader = new(_HLSL_SOURCE); _material = new(_shader); - var color = new Vector4(Color.Brown.R / 255f, Color.Brown.G / 255f, Color.Brown.B / 255f, 1.0f); - _material.SetVector("_Color", ref color); + // Create direct D3D12 texture resources + CreateTextureDirectly(); + + _material.UploadMaterialData(); + + // Copy from upload buffer to texture + var srcLocation = new TextureCopyLocation(_uploadBuffer.Get(), new PlacedSubresourceFootprint + { + Offset = 0, + Footprint = new SubresourceFootprint + { + Format = Format.R8G8B8A8Unorm, + Width = _textureWidth, + Height = _textureHeight, + Depth = 1, + RowPitch = _texturePitch + } + }); + + var dstLocation = new TextureCopyLocation(_textureResource.Get(), 0); + + cmd.NativeCommandList.Ptr->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, null); + + // Transition texture to shader resource + var barrier = new ResourceBarrier + { + Type = ResourceBarrierType.Transition, + Flags = ResourceBarrierFlags.None, + Transition = new ResourceTransitionBarrier + { + pResource = _textureResource.Get(), + StateBefore = ResourceStates.CopyDest, + StateAfter = ResourceStates.PixelShaderResource, + Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES + } + }; + cmd.NativeCommandList.Ptr->ResourceBarrier(1, &barrier); } - public void Execute(ICommandBuffer cmb) + private void CreateTextureDirectly() { - cmb.DrawMesh(_mesh!, _material!); + var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr; + + // Load image data + using var stream = new FileStream("C:/Users/Misaki/Downloads/Im/Icon.png", FileMode.Open, FileAccess.Read); + var image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); + + var width = (uint)image.Width; + var height = (uint)image.Height; + uint bytesPerPixel = 4; // RGBA + var pitch = width * bytesPerPixel; + var textureSize = pitch * height; + + // Create the texture resource + var textureDesc = new ResourceDescription + { + Dimension = ResourceDimension.Texture2D, + Alignment = 0, + Width = width, + Height = height, + DepthOrArraySize = 1, + MipLevels = 1, + Format = Format.R8G8B8A8Unorm, + SampleDesc = new SampleDescription(1, 0), + Layout = TextureLayout.Unknown, + Flags = ResourceFlags.None + }; + + var heapProps = new HeapProperties(HeapType.Default); + device->CreateCommittedResource( + &heapProps, + HeapFlags.None, + &textureDesc, + ResourceStates.CopyDest, + null, + __uuidof(), + _textureResource.GetVoidAddressOf() + ); + + // Create upload buffer + var uploadBufferSize = GetRequiredIntermediateSize(_textureResource.Get(), 0, 1); + var uploadHeapProps = new HeapProperties(HeapType.Upload); + var uploadBufferDesc = ResourceDescription.Buffer(uploadBufferSize); + + ComPtr uploadBuffer = default; + device->CreateCommittedResource( + &uploadHeapProps, + HeapFlags.None, + &uploadBufferDesc, + ResourceStates.GenericRead, + null, + __uuidof(), + uploadBuffer.GetVoidAddressOf() + ); + + // Map and copy texture data + void* mappedData = null; + uploadBuffer.Get()->Map(0, null, &mappedData); + + // Copy image data to upload buffer + var srcData = image.Data.AsSpan(); + var dstSpan = new Span(mappedData, (int)uploadBufferSize); + + // Copy row by row with proper alignment + var alignedPitch = (pitch + 255) & ~255u; // Align to 256 bytes + for (var y = 0; y < height; y++) + { + var srcRow = srcData.Slice(y * (int)pitch, (int)pitch); + var dstRow = dstSpan.Slice(y * (int)alignedPitch, (int)pitch); + srcRow.CopyTo(dstRow); + } + + uploadBuffer.Get()->Unmap(0, null); + + // We'll copy the texture data during Execute phase when we have access to command list + // Store the upload buffer for later use + _uploadBuffer = uploadBuffer.Move(); + _textureWidth = width; + _textureHeight = height; + _texturePitch = alignedPitch; + + // Create SRV descriptor heap + var srvHeapDesc = new DescriptorHeapDescription + { + Type = DescriptorHeapType.CbvSrvUav, + NumDescriptors = 1, + Flags = DescriptorHeapFlags.ShaderVisible + }; + + device->CreateDescriptorHeap(&srvHeapDesc, __uuidof(), _srvHeap.GetVoidAddressOf()); + + // Get descriptor handles + _srvHandle = _srvHeap.Get()->GetCPUDescriptorHandleForHeapStart(); + _srvGpuHandle = _srvHeap.Get()->GetGPUDescriptorHandleForHeapStart(); + _srvDescriptorSize = device->GetDescriptorHandleIncrementSize(DescriptorHeapType.CbvSrvUav); + + // Create SRV + var srvDesc = new ShaderResourceViewDescription + { + Format = Format.R8G8B8A8Unorm, + ViewDimension = Win32.Graphics.Direct3D12.SrvDimension.Texture2D, + Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING, + Texture2D = new Texture2DSrv + { + MostDetailedMip = 0, + MipLevels = 1, + PlaneSlice = 0, + ResourceMinLODClamp = 0.0f + } + }; + + device->CreateShaderResourceView(_textureResource.Get(), &srvDesc, _srvHandle); + } + + private static ulong GetRequiredIntermediateSize(ID3D12Resource* destinationResource, uint firstSubresource, uint numSubresources) + { + var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr; + var resourceDesc = destinationResource->GetDesc(); + + ulong requiredSize = 0; + device->GetCopyableFootprints(&resourceDesc, firstSubresource, numSubresources, 0, null, null, null, &requiredSize); + + return requiredSize; + } + + public void Execute(CommandList cmd) + { + var commandList = cmd.NativeCommandList.Ptr; + + // Set root signature and pipeline state + commandList->SetGraphicsRootSignature(_material!.Shader.RootSignature); + commandList->SetPipelineState(_material.Shader.PipelineState); + + // Set descriptor heap + var heaps = stackalloc ID3D12DescriptorHeap*[1]; + heaps[0] = _srvHeap.Get(); + commandList->SetDescriptorHeaps(1, heaps); + + // Bind texture descriptor table directly + if (_material.Shader.Textures.Count > 0) + { + var textureInfo = _material.Shader.Textures[0]; + commandList->SetGraphicsRootDescriptorTable(textureInfo.RootParameterIndex, _srvGpuHandle); + } + + // Draw mesh + commandList->IASetPrimitiveTopology(PrimitiveTopology.TriangleList); + commandList->IASetVertexBuffers(0, 1, _mesh!.VertexBufferView); + commandList->IASetIndexBuffer(_mesh.IndexBufferView); + commandList->DrawIndexedInstanced(_mesh.IndexCount, 1, 0, 0, 0); } public void Dispose() @@ -67,5 +274,9 @@ float4 PSMain(PixelInput input) : SV_TARGET _mesh?.Dispose(); _shader?.Dispose(); _material?.Dispose(); + + _textureResource.Dispose(); + _uploadBuffer.Dispose(); + _srvHeap.Dispose(); } } diff --git a/Ghost.Graphics/Shading/CBufferCache.cs b/Ghost.Graphics/Shading/CBufferCache.cs index 0d8c895..bd6f11d 100644 --- a/Ghost.Graphics/Shading/CBufferCache.cs +++ b/Ghost.Graphics/Shading/CBufferCache.cs @@ -1,4 +1,4 @@ -using Ghost.Graphics.Contracts; +using Ghost.Graphics.D3D12; using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Helpers; using System.Runtime.CompilerServices; @@ -10,9 +10,10 @@ internal struct CBufferCache : IDisposable public UnsafeArray CpuData { get; + set; } - public IResource GpuResource + public GraphicsResource GpuResource { get; } @@ -21,9 +22,9 @@ internal struct CBufferCache : IDisposable public unsafe CBufferCache(uint bufferSize) { - CpuData = new((int)bufferSize, Allocator.Persistent); - _alignedSize = (bufferSize + 255u) & ~255u; + + CpuData = new((int)_alignedSize, Allocator.Persistent); GpuResource = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer(_alignedSize); GpuResource.Name = "Material_CBufferCache"; diff --git a/Ghost.Graphics/Shading/Shader.cs b/Ghost.Graphics/Shading/Shader.cs index 4b799bf..3262483 100644 --- a/Ghost.Graphics/Shading/Shader.cs +++ b/Ghost.Graphics/Shading/Shader.cs @@ -1,7 +1,7 @@ using Ghost.Core; -using Ghost.Graphics.D3D12; using Ghost.Graphics.D3D12.Utilities; using Misaki.HighPerformance.LowLevel.Helpers; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -13,6 +13,24 @@ using Win32.Graphics.Dxgi.Common; namespace Ghost.Graphics.Shading; +internal readonly struct TextureInfo +{ + public required string Name + { + get; init; + } + + public uint RegisterSlot + { + get; init; + } + + public uint RootParameterIndex + { + get; init; + } +} + internal readonly struct PropertyInfo { public required string Name @@ -20,7 +38,7 @@ internal readonly struct PropertyInfo get; init; } - public required string CBufferName + public uint CBufferIndex { get; init; } @@ -62,23 +80,28 @@ public unsafe class Shader : IDisposable private readonly byte[] _vertexShaderBytecode; private readonly byte[] _pixelShaderBytecode; - private readonly Dictionary _constantBuffers = new(); - private readonly Dictionary _properties = new(); + private readonly List _constantBuffers = new(); + private readonly List _properties = new(); + private readonly Dictionary _propertyNameToIdMap = new(); + + private readonly List _textures = new(); + private readonly Dictionary _textureNameToIdMap = new(); private bool _disposed; internal ConstPtr PipelineState => new(_pipelineState.Get()); internal ConstPtr RootSignature => new(_rootSignature.Get()); - internal IReadOnlyDictionary ConstantBuffers => _constantBuffers; - internal IReadOnlyDictionary Properties => _properties; + internal IReadOnlyList ConstantBuffers => _constantBuffers; + internal IReadOnlyList Properties => _properties; + internal IReadOnlyList Textures => _textures; //public Shader(string shaderPath) //{ //} - public Shader(string shaderCode) + internal Shader(string shaderCode) { _vertexShaderBytecode = CompileShader(Encoding.UTF8.GetBytes(shaderCode), "VSMain", "vs_5_0"); _pixelShaderBytecode = CompileShader(Encoding.UTF8.GetBytes(shaderCode), "PSMain", "ps_5_0"); @@ -103,7 +126,7 @@ public unsafe class Shader : IDisposable /// The shader model to target (e.g., "vs_5_0", "ps_5_0"). /// A byte array containing the compiled shader bytecode. /// Thrown if shader compilation fails. - public static unsafe byte[] CompileShader(byte[] sourceCodeBytes, string entryPoint, string shaderProfile) + public static unsafe byte[] CompileShader(ReadOnlySpan sourceCodeBytes, string entryPoint, string shaderProfile) { using ComPtr bytecodeBlob = default; using ComPtr errorBlob = default; @@ -111,29 +134,14 @@ public unsafe class Shader : IDisposable var entryPointBytes = Encoding.UTF8.GetBytes(entryPoint); var shaderProfileBytes = Encoding.UTF8.GetBytes(shaderProfile); - // Call the D3DCompile function - var hr = D3DCompile( - sourceCodeBytes.AsSpan(), + ThrowIfFailed(D3DCompile( + sourceCodeBytes, entryPointBytes.AsSpan(), shaderProfileBytes.AsSpan(), CompileFlags.EnableStrictness | CompileFlags.Debug, bytecodeBlob.GetAddressOf(), errorBlob.GetAddressOf() - ); - - if (hr.Failure) - { - var errorMessage = "Shader compilation failed."; - if (errorBlob.Get() is not null) - { - errorMessage += "\n" + Encoding.ASCII.GetString( - (byte*)errorBlob.Get()->GetBufferPointer(), - (int)errorBlob.Get()->GetBufferSize() - ); - } - - throw new Exception(errorMessage); - } + )); var bytecode = new byte[bytecodeBlob.Get()->GetBufferSize()]; Unsafe.CopyBlock(bytecode.AsSpan().GetPointer(), bytecodeBlob.Get()->GetBufferPointer(), (uint)bytecode.Length); @@ -143,11 +151,18 @@ public unsafe class Shader : IDisposable private void CreateRootSignature() { - var rootParameters = new RootParameter1[_constantBuffers.Values.Count]; + // Calculate total root parameters: CBVs + 1 descriptor table for SRVs (if any textures) + var totalRootParameters = _constantBuffers.Count + (_textures.Count > 0 ? 1 : 0); + var rootParameters = new RootParameter1[totalRootParameters]; - var i = 0; - foreach (var cbufferInfo in _constantBuffers.Values) + var parameterIndex = 0; + + // Add CBV root parameters + for (var i = 0; i < _constantBuffers.Count; i++) { + var cbufferInfo = _constantBuffers[i]; + Debug.Assert(i == cbufferInfo.RegisterSlot); + var rootParameter = new RootParameter1 { ParameterType = RootParameterType.Cbv, @@ -155,10 +170,68 @@ public unsafe class Shader : IDisposable Descriptor = new RootDescriptor1(cbufferInfo.RegisterSlot, 0), }; - rootParameters[i++] = rootParameter; + rootParameters[parameterIndex++] = rootParameter; } - var rootSignatureDesc = new RootSignatureDescription((uint)rootParameters.Length, (RootParameter*)Unsafe.AsPointer(ref rootParameters[0])) + // Add descriptor table for SRVs if we have textures + if (_textures.Count > 0) + { + var ranges = new DescriptorRange1[1]; + ranges[0] = new DescriptorRange1 + { + RangeType = DescriptorRangeType.Srv, + NumDescriptors = (uint)_textures.Count, + BaseShaderRegister = 0, // Start from t0 + RegisterSpace = 0, + Flags = DescriptorRangeFlags.None, + OffsetInDescriptorsFromTableStart = 0 + }; + + fixed (DescriptorRange1* rangesPtr = ranges) + { + var rootParameter = new RootParameter1 + { + ParameterType = RootParameterType.DescriptorTable, + ShaderVisibility = ShaderVisibility.All, + DescriptorTable = new RootDescriptorTable1(1, rangesPtr) + }; + + rootParameters[parameterIndex++] = rootParameter; + } + } + + // Create static samplers for textures + var staticSamplers = new StaticSamplerDescription[_textures.Count]; + for (var i = 0; i < _textures.Count; i++) + { + staticSamplers[i] = new StaticSamplerDescription + { + Filter = Filter.MinMagMipLinear, + AddressU = TextureAddressMode.Wrap, + AddressV = TextureAddressMode.Wrap, + AddressW = TextureAddressMode.Wrap, + MipLODBias = 0, + MaxAnisotropy = 1, + BorderColor = StaticBorderColor.OpaqueWhite, + MinLOD = 0, + MaxLOD = 0, + ShaderRegister = (uint)i, // s0, s1, etc. + RegisterSpace = 0, + ShaderVisibility = ShaderVisibility.All + }; + } + + var parameterCount = (uint)rootParameters.Length; + var parameters = parameterCount > 0 + ? (RootParameter*)Unsafe.AsPointer(ref rootParameters[0]) + : null; + + var samplerCount = (uint)staticSamplers.Length; + var samplers = samplerCount > 0 + ? (StaticSamplerDescription*)Unsafe.AsPointer(ref staticSamplers[0]) + : null; + + var rootSignatureDesc = new RootSignatureDescription(parameterCount, parameters, samplerCount, samplers) { Flags = RootSignatureFlags.AllowInputAssemblerInputLayout }; @@ -166,14 +239,9 @@ public unsafe class Shader : IDisposable using ComPtr signature = default; using ComPtr error = default; - var hr = D3D12SerializeRootSignature(&rootSignatureDesc, RootSignatureVersion.V1_0, signature.GetAddressOf(), error.GetAddressOf()); - if (hr.Failure) - { - var errorMessage = System.Text.Encoding.ASCII.GetString((byte*)error.Get()->GetBufferPointer(), (int)error.Get()->GetBufferSize()); - throw new InvalidOperationException($"Failed to serialize root signature: {errorMessage}"); - } + ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, RootSignatureVersion.V1_0, signature.GetAddressOf(), error.GetAddressOf())); - GraphicsPipeline.GetGraphicsDevice().NativeDevice.Ptr->CreateRootSignature(0, signature.Get()->GetBufferPointer(), signature.Get()->GetBufferSize(), __uuidof(), _rootSignature.GetVoidAddressOf()); + GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateRootSignature(0, signature.Get()->GetBufferPointer(), signature.Get()->GetBufferSize(), __uuidof(), _rootSignature.GetVoidAddressOf()); } private void CreatePipelineStateObject() @@ -202,7 +270,7 @@ public unsafe class Shader : IDisposable psoDesc.RTVFormats[0] = D3D12PipelineResource.SWAP_CHAIN_BACK_BUFFER_FORMAT; // Create the PSO - GraphicsPipeline.GetGraphicsDevice().NativeDevice.Ptr->CreateGraphicsPipelineState(&psoDesc, __uuidof(), _pipelineState.GetVoidAddressOf()); + GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateGraphicsPipelineState(&psoDesc, __uuidof(), _pipelineState.GetVoidAddressOf()); } } catch (Exception ex) @@ -222,6 +290,9 @@ public unsafe class Shader : IDisposable ShaderDescription shaderDesc; reflection.Get()->GetDesc(&shaderDesc); + var cbufferRegistry = _constantBuffers.ToDictionary(cb => cb.Name); + var textureRegistry = _textures.ToDictionary(t => t.Name); + for (uint i = 0; i < shaderDesc.BoundResources; i++) { ShaderInputBindDescription bindDesc; @@ -230,7 +301,7 @@ public unsafe class Shader : IDisposable if (bindDesc.Type == ShaderInputType.ConstantBuffer) { var cbufferName = Marshal.PtrToStringAnsi((IntPtr)bindDesc.Name); - if (cbufferName == null || _constantBuffers.ContainsKey(cbufferName)) + if (cbufferName == null || cbufferRegistry.ContainsKey(cbufferName)) { continue; } @@ -239,12 +310,13 @@ public unsafe class Shader : IDisposable ShaderBufferDescription cbufferDesc; cbuffer->GetDesc(&cbufferDesc); - _constantBuffers.Add(cbufferName, new CBufferInfo + var cbufferInfo = new CBufferInfo { Name = cbufferName, Size = cbufferDesc.Size, RegisterSlot = bindDesc.BindPoint - }); + }; + cbufferRegistry.Add(cbufferName, cbufferInfo); for (uint j = 0; j < cbufferDesc.Variables; j++) { @@ -253,24 +325,80 @@ public unsafe class Shader : IDisposable variable->GetDesc(&varDesc); var variableName = Marshal.PtrToStringAnsi((IntPtr)varDesc.Name); - if (variableName == null || _properties.ContainsKey(variableName)) + if (variableName == null || _propertyNameToIdMap.ContainsKey(variableName)) { continue; } - _properties.Add(variableName, new PropertyInfo + var propInfo = new PropertyInfo { Name = variableName, - CBufferName = cbufferName, + CBufferIndex = cbufferInfo.RegisterSlot, ByteOffset = varDesc.StartOffset, Size = varDesc.Size - }); + }; + + // Add to the list and create the name-to-ID mapping + var newId = _properties.Count; + _properties.Add(propInfo); + _propertyNameToIdMap.Add(variableName, newId); } } + else if (bindDesc.Type == ShaderInputType.Texture) + { + var textureName = Marshal.PtrToStringAnsi((IntPtr)bindDesc.Name); + if (textureName == null || textureRegistry.ContainsKey(textureName)) + { + continue; + } + + // The root parameter index for textures is after all CBVs + var textureInfo = new TextureInfo + { + Name = textureName, + RegisterSlot = bindDesc.BindPoint, + RootParameterIndex = (uint)_constantBuffers.Count // Descriptor table comes after CBVs + }; + + textureRegistry.Add(textureName, textureInfo); + + // Add to the texture name-to-ID mapping + var newId = _textures.Count; + _textureNameToIdMap.Add(textureName, newId); + } } + + _constantBuffers.Clear(); + _constantBuffers.AddRange(cbufferRegistry.Values); + + _textures.Clear(); + _textures.AddRange(textureRegistry.Values); + reflection.Dispose(); } + /// + /// Gets a unique, stable ID for a shader property. + /// This should be called once and the ID cached for repeated use. + /// + /// The name of the property (e.g., "_Color"). + /// The integer ID of the property, or -1 if not found. + public int GetPropertyId(string propertyName) + { + return _propertyNameToIdMap.TryGetValue(propertyName, out var id) ? id : -1; + } + + /// + /// Gets a unique, stable ID for a texture property. + /// This should be called once and the ID cached for repeated use. + /// + /// The name of the texture (e.g., "_MainTex"). + /// The integer ID of the texture, or -1 if not found. + public int GetTextureId(string textureName) + { + return _textureNameToIdMap.TryGetValue(textureName, out var id) ? id : -1; + } + public void Dispose() { if (_disposed) @@ -283,6 +411,8 @@ public unsafe class Shader : IDisposable _constantBuffers.Clear(); _properties.Clear(); + _textures.Clear(); + _textureNameToIdMap.Clear(); GC.SuppressFinalize(this); diff --git a/Ghost.Graphics/Utilities/MeshBuilder.cs b/Ghost.Graphics/Utilities/MeshBuilder.cs index a3f3214..aeb0d62 100644 --- a/Ghost.Graphics/Utilities/MeshBuilder.cs +++ b/Ghost.Graphics/Utilities/MeshBuilder.cs @@ -8,17 +8,17 @@ public static class MeshBuilder /// /// Creates a unit cube centered at the origin with size 1. /// - public static Mesh CreateCube(float size = 1.0f, Color128 color = default, Vector3 offset = default) + public static Mesh CreateCube(float size = 1.0f, Color128 color = default) { var half = size * 0.5f; var mesh = new Mesh(24, 36); - var corners = new Vector3[] + var corners = new Vector4[] { - new Vector3(-half, -half, -half) + offset, new Vector3( half, -half, -half) + offset, - new Vector3( half, half, -half) + offset, new Vector3(-half, half, -half) + offset, - new Vector3(-half, -half, half) + offset, new Vector3( half, -half, half) + offset, - new Vector3( half, half, half) + offset, new Vector3(-half, half, half) + offset + new (-half, -half, -half, 1.0f), new (half, -half, -half, 1.0f), + new (half, half, -half, 1.0f), new (-half, half, -half, 1.0f), + new (-half, -half, half, 1.0f), new (half, -half, half, 1.0f), + new (half, half, half, 1.0f), new (-half, half, half, 1.0f) }; int[][] faces = @@ -40,13 +40,13 @@ public static class MeshBuilder { var vertex = new Vertex { - Position = corners[face[i]].AsVector4(), + Position = corners[face[i]], Normal = Vector4.Zero, Tangent = Vector4.Zero, Color = color, UV = uvs[i].AsVector4() }; - mesh.AddVertex(new(corners[face[i]].AsVector4(), Vector4.Zero, Vector4.Zero, color, uvs[i].AsVector4())); + mesh.AddVertex(vertex); } mesh.AddTriangle((int)baseIndex + 0, (int)baseIndex + 1, (int)baseIndex + 2); diff --git a/Ghost.UnitTest/UnitTestAppWindow.xaml.cs b/Ghost.UnitTest/UnitTestAppWindow.xaml.cs index cf3aa1f..8a3ff5a 100644 --- a/Ghost.UnitTest/UnitTestAppWindow.xaml.cs +++ b/Ghost.UnitTest/UnitTestAppWindow.xaml.cs @@ -1,14 +1,16 @@ using Ghost.Graphics; using Ghost.Graphics.Contracts; +using Ghost.Graphics.D3D12; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; +using Misaki.HighPerformance.LowLevel.Buffer; using WinRT; namespace Ghost.UnitTest; public sealed partial class UnitTestAppWindow : Window { - private IRenderer? _renderView; + private Renderer? _renderer; private ISwapChainPanelNative _swapChainPanelNative; public UnitTestAppWindow() @@ -25,31 +27,36 @@ public sealed partial class UnitTestAppWindow : Window { if (e.NewSize.Width > 8.0 && e.NewSize.Height > 8.0) { - _renderView?.RequestResize((uint)e.NewSize.Width, (uint)e.NewSize.Height); + _renderer?.RequestResize((uint)e.NewSize.Width, (uint)e.NewSize.Height); } } private void UnitTestAppWindow_Activated(object sender, WindowActivatedEventArgs args) { - GraphicsPipeline.Initialize(Graphics.Data.GraphicsAPI.D3D12); +#if DEBUG + AllocationManager.EnableDebugLayer(); +#endif + GraphicsPipeline.Initialize(); GraphicsPipeline.Start(); var guid = typeof(ISwapChainPanelNative.Interface).GUID; ((IWinRTObject)Panel).NativeObject.TryAs(guid, out var swapChainPanelNativeHandle); _swapChainPanelNative = new ISwapChainPanelNative(swapChainPanelNativeHandle); - _renderView = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)AppWindow.Size.Width, (uint)AppWindow.Size.Height)); + _renderer = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)AppWindow.Size.Width, (uint)AppWindow.Size.Height)); CompositionTarget.Rendering += OnRendering; } private void UnitTestAppWindow_Closed(object sender, WindowEventArgs args) { + CompositionTarget.Rendering -= OnRendering; + GraphicsPipeline.SignalCPUReady(); GraphicsPipeline.Shutdown(); - CompositionTarget.Rendering -= OnRendering; + _swapChainPanelNative.Dispose(); - _renderView?.Dispose(); + _renderer?.Dispose(); } private void OnRendering(object? sender, object e)