Update rendering architecture and resource management

Added a new `Ref<T>` struct for reference semantics.
Added the `RenderGraph` system for managing rendering passes.
Added the `RenderTexture` class for encapsulating GPU resources.
Added `GraphicsBuffer` class for effective GPU resource management.
Changed `CommandList` methods from public to internal for visibility control.
Changed `IRenderPass` interface from internal to public for accessibility.
Changed `GetData<T>()` in `ComponentObject.cs` to return `CompRef<T>`.
Changed `GetComponent<T>()` in `EntityManager.cs` to return `CompRef<T>`.
Changed `GetSingleton<T>()` in `World.cs` to use `CompRef<T>`.
Changed `IQueryTypeParameter` to use `CompRef<T>` for consistency.
Changed `QueryItem<T0>` and related structs to use `CompRef<T>`.
Changed `Material` class to support bindless textures.
Changed `Shader` class to support bindless rendering.
Changed `Mesh` class to support bindless vertex and index buffer access.
Updated documentation to reflect the new bindless rendering architecture.
This commit is contained in:
2025-08-01 21:34:48 +09:00
parent 1284bb17de
commit eafbfb2fa1
43 changed files with 3845 additions and 2183 deletions

View File

@@ -2,7 +2,7 @@
namespace Ghost.Graphics.Contracts;
internal interface IRenderPass : IDisposable
public interface IRenderPass : IDisposable
{
void Initialize(CommandList cmd);
void Execute(CommandList cmd);

View File

@@ -16,40 +16,60 @@ public unsafe class CommandList
_commandList = commandList;
}
public void BarrierTransition(GraphicsResource resource, ResourceStates beforeState, ResourceStates afterState)
internal void BarrierTransition(GraphicsResource resource, ResourceStates beforeState, ResourceStates afterState)
{
_commandList.Ptr->ResourceBarrierTransition(resource.NativeResource.Ptr, beforeState, afterState);
}
public void SetGraphicsRootConstantBufferView(uint slot, ulong gpuAddress)
internal void SetGraphicsRootConstantBufferView(uint slot, ulong gpuAddress)
{
_commandList.Ptr->SetGraphicsRootConstantBufferView(slot, gpuAddress);
}
/// <summary>
/// Draws a mesh using fully bindless rendering with SM 6.6 support.
/// This method does not use the Input Assembler stage and instead relies on
/// vertex and index buffer access through bindless descriptors in the shader.
/// </summary>
/// <param name="mesh">The mesh to draw</param>
/// <param name="material">The bindless material to use</param>
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);
}
// Bind the bindless material (sets up root signature, pipeline state, and descriptor heaps)
material.Bind(this);
// For fully bindless rendering, we don't use the Input Assembler stage
// Instead, we use instanced drawing where each "instance" represents a triangle
// The shader will use SV_InstanceID to index into the index buffer and then into the vertex buffer
_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);
// Draw without vertex/index buffers - use instanced drawing
// Each instance represents a triangle (3 vertices)
var triangleCount = mesh.IndexCount / 3;
_commandList.Ptr->DrawInstanced(3, triangleCount, 0, 0);
}
public void SetRenderTarget(RenderTexture? color, RenderTexture? depth)
{
var rtvHandle = color?.RenderTargetView?.CpuHandle;
var rtvHandleValue = rtvHandle ?? default;
var pRtvHandle = rtvHandle.HasValue ? &rtvHandleValue : null;
var dsvHandle = depth?.RenderTargetView?.CpuHandle;
var dsvHandleValue = dsvHandle ?? default;
var pDsvHandle = dsvHandle.HasValue ? &dsvHandleValue : null;
_commandList.Ptr->OMSetRenderTargets(1, pRtvHandle, false, pDsvHandle);
}
public void ClearRenderTarget(RenderTexture renderTarget, Color128 color)
{
renderTarget.ClearColor(this, color);
}
public void ClearDepthStencil(RenderTexture depthStencil, ClearFlags flags, float depth = 1.0f, byte stencil = 0)
{
depthStencil.ClearDepthStencil(this, flags, depth, stencil);
}
public void CopyResource(GraphicsResource dstResource, uint dstOffset, GraphicsResource srcResource, uint srcOffset, uint size)

View File

@@ -13,17 +13,19 @@ internal class DescriptorAllocator : IDisposable
private readonly DescriptorHeapAllocator _dsvHeap;
private readonly DescriptorHeapAllocator _srvHeap;
private readonly DescriptorHeapAllocator _samplerHeap;
private readonly BindlessDescriptorHeapAllocator _bindlessHeap;
private bool _disposed;
public DescriptorAllocator(uint initialRtvCount = 256, uint initialDsvCount = 256, uint initialSrvCount = 1024, uint initialSamplerCount = 256)
public DescriptorAllocator(uint initialRtvCount = 256, uint initialDsvCount = 256, uint initialSrvCount = 1024, uint initialSamplerCount = 256, uint initialBindlessCount = 10000)
{
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);
_rtvHeap = new DescriptorHeapAllocator("rtv", device.NativeDevice, DescriptorHeapType.Rtv, initialRtvCount);
_dsvHeap = new DescriptorHeapAllocator("dsv", device.NativeDevice, DescriptorHeapType.Dsv, initialDsvCount);
_srvHeap = new DescriptorHeapAllocator("srv", device.NativeDevice, DescriptorHeapType.CbvSrvUav, initialSrvCount);
_samplerHeap = new DescriptorHeapAllocator("sampler", device.NativeDevice, DescriptorHeapType.Sampler, initialSamplerCount);
_bindlessHeap = new BindlessDescriptorHeapAllocator(device.NativeDevice, initialBindlessCount);
}
#region RTV Methods
@@ -280,6 +282,81 @@ internal class DescriptorAllocator : IDisposable
#endregion
#region Bindless Methods
/// <summary>
/// Allocates a bindless descriptor for SM 6.6 rendering.
/// The returned descriptor maintains a 1:1 relationship between allocation index and shader index.
/// </summary>
public BindlessDescriptor AllocateBindless()
{
ObjectDisposedException.ThrowIf(_disposed, this);
var index = _bindlessHeap.AllocateDescriptor();
if (index == uint.MaxValue)
{
throw new InvalidOperationException("Failed to allocate bindless descriptor");
}
var cpuHandle = _bindlessHeap.GetCpuHandle(index);
var gpuHandle = _bindlessHeap.GetGpuHandle(index);
return new BindlessDescriptor(index, cpuHandle, gpuHandle);
}
/// <summary>
/// Allocates multiple bindless descriptors for SM 6.6 rendering.
/// </summary>
public BindlessDescriptor[] AllocateBindless(uint count)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var baseIndex = _bindlessHeap.AllocateDescriptors(count);
if (baseIndex == uint.MaxValue)
{
throw new InvalidOperationException($"Failed to allocate {count} bindless descriptors");
}
var descriptors = new BindlessDescriptor[count];
for (uint i = 0; i < count; i++)
{
var index = baseIndex + i;
var cpuHandle = _bindlessHeap.GetCpuHandle(index);
var gpuHandle = _bindlessHeap.GetGpuHandle(index);
descriptors[i] = new BindlessDescriptor(index, cpuHandle, gpuHandle);
}
return descriptors;
}
/// <summary>
/// Releases a bindless descriptor.
/// </summary>
public void ReleaseBindless(BindlessDescriptor descriptor)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (descriptor is BindlessDescriptor d3d12Descriptor)
{
_bindlessHeap.ReleaseDescriptor(d3d12Descriptor.Index);
}
}
/// <summary>
/// Releases multiple bindless descriptors.
/// </summary>
public void ReleaseBindless(BindlessDescriptor[] descriptors)
{
ObjectDisposedException.ThrowIf(_disposed, this);
foreach (var descriptor in descriptors)
{
ReleaseBindless(descriptor);
}
}
#endregion
#region Utility Methods
/// <summary>
@@ -302,6 +379,11 @@ internal class DescriptorAllocator : IDisposable
/// </summary>
public ConstPtr<ID3D12DescriptorHeap> GetSamplerHeap() => _samplerHeap.ShaderVisibleHeap;
/// <summary>
/// Gets the bindless heap for SM 6.6 bindless rendering.
/// </summary>
public ConstPtr<ID3D12DescriptorHeap> GetBindlessHeap() => _bindlessHeap.BindlessHeap;
/// <summary>
/// Gets the shader visible heaps that need to be bound to the command list.
/// </summary>
@@ -310,6 +392,14 @@ internal class DescriptorAllocator : IDisposable
return [_srvHeap.ShaderVisibleHeap, _samplerHeap.ShaderVisibleHeap];
}
/// <summary>
/// Gets the shader visible heaps including bindless heap for SM 6.6 rendering.
/// </summary>
public ConstPtr<ID3D12DescriptorHeap>[] GetShaderVisibleHeapsWithBindless()
{
return [_bindlessHeap.BindlessHeap, _srvHeap.ShaderVisibleHeap, _samplerHeap.ShaderVisibleHeap];
}
#endregion
public void Dispose()
@@ -323,6 +413,7 @@ internal class DescriptorAllocator : IDisposable
_dsvHeap.Dispose();
_srvHeap.Dispose();
_samplerHeap.Dispose();
_bindlessHeap.Dispose();
_disposed = true;
GC.SuppressFinalize(this);

View File

@@ -3,9 +3,9 @@ using Win32.Graphics.Direct3D12;
namespace Ghost.Graphics.D3D12;
/// <summary>
/// Base class for D3D12 descriptor implementations.
/// Base class descriptor implementations.
/// </summary>
internal abstract class Descriptor
public abstract class Descriptor
{
protected readonly uint index;
protected readonly bool isShaderVisible;
@@ -38,9 +38,9 @@ internal abstract class Descriptor
}
/// <summary>
/// D3D12 implementation of render target view (RTV) descriptor.
/// Implementation of render target view (RTV) descriptor.
/// </summary>
internal sealed class RenderTargetDescriptor : Descriptor
public sealed class RenderTargetDescriptor : Descriptor
{
private readonly CpuDescriptorHandle _cpuHandle;
@@ -55,9 +55,9 @@ internal sealed class RenderTargetDescriptor : Descriptor
}
/// <summary>
/// D3D12 implementation of depth stencil view (DSV) descriptor.
/// Implementation of depth stencil view (DSV) descriptor.
/// </summary>
internal sealed class DepthStencilDescriptor : Descriptor
public sealed class DepthStencilDescriptor : Descriptor
{
private readonly CpuDescriptorHandle _cpuHandle;
@@ -72,9 +72,9 @@ internal sealed class DepthStencilDescriptor : Descriptor
}
/// <summary>
/// D3D12 implementation of shader resource view (SRV) descriptor.
/// Implementation of shader resource view (SRV) descriptor.
/// </summary>
internal sealed class ShaderResourceDescriptor : Descriptor
public sealed class ShaderResourceDescriptor : Descriptor
{
private readonly CpuDescriptorHandle _cpuHandle;
private readonly GpuDescriptorHandle _gpuHandle;
@@ -91,9 +91,9 @@ internal sealed class ShaderResourceDescriptor : Descriptor
}
/// <summary>
/// D3D12 implementation of sampler descriptor.
/// Implementation of sampler descriptor.
/// </summary>
internal sealed class SamplerDescriptor : Descriptor
public sealed class SamplerDescriptor : Descriptor
{
private readonly CpuDescriptorHandle _cpuHandle;
private readonly GpuDescriptorHandle _gpuHandle;
@@ -105,6 +105,26 @@ internal sealed class SamplerDescriptor : Descriptor
_gpuHandle = gpuHandle;
}
public override CpuDescriptorHandle CpuHandle => _cpuHandle;
public override GpuDescriptorHandle GpuHandle => _gpuHandle;
}
/// <summary>
/// Implementation of bindless descriptor for SM 6.6 rendering.
/// This descriptor maintains a 1:1 relationship between allocation indices and shader indices.
/// </summary>
public sealed class BindlessDescriptor : Descriptor
{
private readonly CpuDescriptorHandle _cpuHandle;
private readonly GpuDescriptorHandle _gpuHandle;
public BindlessDescriptor(uint index, CpuDescriptorHandle cpuHandle, GpuDescriptorHandle gpuHandle)
: base(index, true)
{
_cpuHandle = cpuHandle;
_gpuHandle = gpuHandle;
}
public override CpuDescriptorHandle CpuHandle => _cpuHandle;
public override GpuDescriptorHandle GpuHandle => _gpuHandle;
}

View File

@@ -0,0 +1,119 @@
using Ghost.Graphics.Data;
using System.Runtime.CompilerServices;
using Win32.Graphics.Direct3D12;
namespace Ghost.Graphics.D3D12;
public unsafe class GraphicsBuffer : GraphicsResource
{
public enum Usage
{
Common,
Vertex,
Index,
CopySource,
CopyDestination,
Structured,
Raw,
Append,
Counter,
Indirect,
Constant,
}
private readonly Usage _usage;
public Usage BufferUsage => _usage;
private GraphicsBuffer(Usage usage, in BufferHandle handle, bool tempResource = false)
: base(handle.ResourceHandle, tempResource)
{
_usage = usage;
}
public static GraphicsBuffer Create(uint sizeInBytes, Usage usage, bool tempResource = false)
{
var heapType = HeapType.Default;
var state = ResourceStates.Common;
switch (usage)
{
case Usage.Vertex:
heapType = HeapType.Default;
state = ResourceStates.VertexAndConstantBuffer;
break;
case Usage.Index:
heapType = HeapType.Default;
state = ResourceStates.IndexBuffer;
break;
case Usage.CopySource:
heapType = HeapType.Readback;
state = ResourceStates.CopySource;
break;
case Usage.CopyDestination:
heapType = HeapType.Default;
state = ResourceStates.CopyDest;
break;
case Usage.Structured:
case Usage.Raw:
case Usage.Append:
case Usage.Counter:
heapType = HeapType.Default;
state = ResourceStates.AllShaderResource | ResourceStates.UnorderedAccess;
break;
case Usage.Indirect:
heapType = HeapType.Default;
state = ResourceStates.IndirectArgument;
break;
case Usage.Constant:
heapType = HeapType.Upload;
state = ResourceStates.GenericRead;
break;
default:
break;
}
var handle = GraphicsPipeline.ResourceAllocator.CreateBuffer(sizeInBytes, heapType, initialState: state, tempResource: tempResource);
return new GraphicsBuffer(usage, in handle, tempResource);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetData<T>(Span<T> data, uint offset)
where T : unmanaged
{
fixed (T* ptr = data)
{
SetData(ptr, offset, (uint)data.Length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void SetData<T>(T* data, uint offset, uint length)
where T : unmanaged
{
var size = (uint)(length * sizeof(T));
SetData((void*)data, offset, size);
}
public unsafe void SetData(void* data, uint offset, uint size)
{
ThrowIfDisposed();
if (data == null)
{
throw new ArgumentNullException(nameof(data), "Data pointer cannot be null.");
}
if (size > Size)
{
throw new ArgumentException($"Data size {size} exceeds buffer size {Size}.", nameof(size));
}
var range = new Win32.Graphics.Direct3D12.Range(offset, size);
void* mappedPtr;
ThrowIfFailed(NativeResource.Ptr->Map(0, &range, &mappedPtr));
Unsafe.CopyBlock(mappedPtr, data, size);
NativeResource.Ptr->Unmap(0, &range);
}
}

View File

@@ -10,11 +10,9 @@ namespace Ghost.Graphics.D3D12;
internal unsafe class GraphicsDevice
{
#if DEBUG
private readonly DebugLayer _debugLayer;
#endif
private ComPtr<IDXGIFactory7> _dxgiFactory;
private ComPtr<ID3D12Device14> _device;
private ComPtr<IDXGIAdapter1> _adapter;
private ComPtr<ID3D12CommandQueue> _commandQueue;
private ImmutableArray<Renderer> _initializeQueue;
@@ -27,14 +25,11 @@ internal unsafe class GraphicsDevice
public ConstPtr<ID3D12Device14> NativeDevice => new(_device.Get());
public ConstPtr<IDXGIFactory7> DXGIFactory => new(_dxgiFactory.Get());
public ConstPtr<IDXGIAdapter1> Adapter => new(_adapter.Get());
public ConstPtr<ID3D12CommandQueue> CommandQueue => new(_commandQueue.Get());
public GraphicsDevice()
{
#if DEBUG
_debugLayer = new DebugLayer();
#endif
InitializeDevice();
InitializeCommandQueue();
@@ -67,6 +62,7 @@ internal unsafe class GraphicsDevice
if (D3D12CreateDevice((IUnknown*)adapter.Get(), FeatureLevel.Level_12_0, __uuidof<ID3D12Device14>(), _device.GetVoidAddressOf()).Success)
{
_adapter = adapter.Move();
break;
}
}
@@ -150,9 +146,6 @@ internal unsafe class GraphicsDevice
_device.Reset();
_dxgiFactory.Dispose();
#if DEBUG
_debugLayer.Dispose();
#endif
_disposed = true;
}
}

View File

@@ -1,21 +1,26 @@
using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Collections;
using Ghost.Graphics.Data;
using System.Runtime.CompilerServices;
using Win32;
using Win32.Graphics.Direct3D12;
namespace Ghost.Graphics.D3D12;
public unsafe class GraphicsResource
public unsafe class GraphicsResource : IDisposable
{
private ComPtr<ID3D12Resource> _nativeResource;
private readonly ResourceHandle _handle;
private string _name = string.Empty;
private bool _disposed;
internal ConstPtr<ID3D12Resource> NativeResource
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(_handle.GetAllocation().Resource);
}
internal ConstPtr<ID3D12Resource> NativeResource => new(_nativeResource.Get());
public ulong GPUAddress => _nativeResource.Get()->GetGPUVirtualAddress();
internal ulong GPUAddress
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => NativeResource.Ptr->GetGPUVirtualAddress();
}
public string Name
{
@@ -23,7 +28,7 @@ public unsafe class GraphicsResource
set
{
_name = value;
_nativeResource.Get()->SetName(_name);
NativeResource.Ptr->SetName(_name);
}
}
@@ -32,10 +37,12 @@ public unsafe class GraphicsResource
get;
}
internal GraphicsResource(ComPtr<ID3D12Resource> nativeResource, bool temp = false)
public ulong Size => _handle.GetAllocation().Size;
internal GraphicsResource(in ResourceHandle handle, bool tempResource = false)
{
_nativeResource = nativeResource;
TempResource = temp;
_handle = handle;
TempResource = tempResource;
}
~GraphicsResource()
@@ -43,99 +50,26 @@ public unsafe class GraphicsResource
DisposeInternal();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetData<T>(Span<T> data)
where T : unmanaged
/// <summary>
/// Throws an exception if the resource has been disposed.
/// </summary>
protected void ThrowIfDisposed()
{
fixed (T* ptr = data)
{
SetData(ptr, (uint)data.Length);
}
}
public unsafe void SetData<T>(T* data, uint length)
where T : unmanaged
{
var size = (uint)(length * sizeof(T));
SetData((void*)data, size);
}
public unsafe void SetData(void* data, uint size)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var range = new Win32.Graphics.Direct3D12.Range(0, size);
void* mappedPtr;
ThrowIfFailed(_nativeResource.Get()->Map(0, &range, &mappedPtr));
Unsafe.CopyBlock(mappedPtr, data, size);
_nativeResource.Get()->Unmap(0, &range);
}
public UnsafeArray<T> ReadData<T>(Allocator allocator)
where T : unmanaged
{
var size = (uint)_nativeResource.Get()->GetDesc().Width;
var data = new UnsafeArray<T>((int)(size / (uint)sizeof(T)), allocator);
try
{
ReadData(data.GetUnsafePtr(), &size);
return data;
}
catch (Exception)
{
data.Dispose();
throw;
}
}
public void ReadData<T>(T* pData, uint* size)
where T : unmanaged
{
ReadData((void*)pData, size);
}
public void ReadData(void* pData, uint* size)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var range = new Win32.Graphics.Direct3D12.Range(0, (uint)_nativeResource.Get()->GetDesc().Width);
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}");
}
Unsafe.CopyBlock(pData, mappedPtr, (uint)(range.End - range.Begin));
_nativeResource.Get()->Unmap(0, &range);
if (size != null)
{
*size = (uint)(range.End - range.Begin);
}
ObjectDisposedException.ThrowIf(!_handle.IsValid, this);
}
internal void DisposeInternal()
{
if (_disposed)
{
return;
}
_nativeResource.Dispose();
_disposed = true;
_handle.Dispose();
}
public void Dispose()
public virtual void Dispose()
{
if (!TempResource)
{
DisposeInternal();
GC.SuppressFinalize(this);
}
GC.SuppressFinalize(this);
}
}

View File

@@ -1,7 +1,6 @@
using Ghost.Graphics.Contracts;
using Ghost.Graphics.D3D12.Utilities;
using Ghost.Graphics.Data;
using Ghost.Graphics.RenderPasses;
using System.Collections.Immutable;
using Win32;
using Win32.Graphics.Direct3D12;
@@ -93,7 +92,7 @@ internal unsafe class Renderer
_viewPortHeight = swapChainSurface.Height;
_fenceEvent = new(false);
_renderPasses = [new BindlessMeshRenderPass()];
_renderPasses = [];
InitializeSwapChain();
InitializeFrameResource(out _frameResources);

View File

@@ -1,305 +1,251 @@
using Win32;
using Ghost.Graphics.Data;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Runtime.CompilerServices;
using Win32.Graphics.D3D12MemoryAllocator;
using Win32.Graphics.Direct3D12;
using Win32.Graphics.Dxgi;
using Win32.Graphics.Dxgi.Common;
using static Win32.Graphics.D3D12MemoryAllocator.Apis;
using ResourceHandle = Ghost.Graphics.Data.ResourceHandle;
namespace Ghost.Graphics.D3D12;
internal unsafe class ResourceAllocator
{
private readonly struct TempResourceAllocInfo
private readonly struct AllocationInfo : IDisposable
{
public readonly GraphicsResource resource;
public readonly Allocation allocation;
public readonly uint cpuFenceValue;
public readonly uint generation;
public TempResourceAllocInfo(GraphicsResource resource, uint cpuFenceValue)
public bool Allocated => allocation.IsNotNull;
public AllocationInfo(in Allocation allocation, uint cpuFenceValue, uint generation)
{
this.resource = resource;
this.allocation = allocation;
this.cpuFenceValue = cpuFenceValue;
this.generation = generation;
}
public TempResourceAllocInfo(GraphicsResource resource)
: this(resource, GraphicsPipeline.CPUFenceValue + 1)
public AllocationInfo(in Allocation allocation, uint generation)
: this(allocation, GraphicsPipeline.CPUFenceValue + 1, generation)
{
}
public void Dispose()
{
if (allocation.IsNull)
{
return;
}
allocation.Release();
}
}
private const ResourceStates _INITIALCOPYTARGETSTATE = ResourceStates.Common;
private const ResourceStates _INITIALREADTARGETSTATE = ResourceStates.Common;
private const ResourceStates _INITIALUAVTARGETSTATE = ResourceStates.Common;
private const uint _MAX_BYTES = D3D12_REQ_RESOURCE_SIZE_IN_MEGABYTES_EXPRESSION_A_TERM * 1024u * 1024u;
private const uint _MAX_TEXTURE2D_DIMENSION = 16384u;
private const uint _MAX_TEXTURE3D_DIMENSION = 2048u;
private readonly Queue<TempResourceAllocInfo> _temResources = new();
private readonly Allocator _allocator;
private UnsafeList<AllocationInfo> _allocations = new(64, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
private UnsafeQueue<int> _freeSlots = new(64, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
private UnsafeQueue<ResourceHandle> _temResources = new(64, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
//public static ID3D12Resource CreateStaticBuffer<T>(
// ID3D12Device device,
// D3D12ResourceUploadBatch resourceUpload,
// T[] data, ResourceStates afterState,
// ResourceFlags flags = ResourceFlags.None)
// where T : unmanaged
//{
// Span<T> span = data;
// return CreateStaticBuffer(device, resourceUpload, span, afterState, flags);
//}
private readonly Lock _lock = new();
//public static ID3D12Resource CreateStaticBuffer<T>(
// ID3D12Device device,
// D3D12ResourceUploadBatch resourceUpload,
// Span<T> data,
// ResourceStates afterState,
// ResourceFlags flags = ResourceFlags.None)
// where T : unmanaged
//{
// var sizeInBytes = (uint)(sizeof(T) * data.Length);
// if (sizeInBytes > _MAX_BYTES)
// {
// throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {sizeInBytes})");
// }
// var buffer = device.CreateCommittedResource(
// HeapType.Default,
// HeapFlags.None,
// ResourceDescription.Buffer(sizeInBytes, flags),
// _INITIALCOPYTARGETSTATE
// );
// fixed (T* dataPtr = data)
// {
// SubresourceData initData = new()
// {
// pData = dataPtr,
// };
// resourceUpload.Upload(buffer, 0, &initData, 1);
// resourceUpload.Transition(buffer, ResourceStates.CopyDest, afterState);
// return buffer;
// }
//}
//public static ID3D12Resource CreateUploadBuffer<T>(
// ID3D12Device device,
// T[] data,
// ResourceFlags flags = ResourceFlags.None)
// where T : unmanaged
//{
// var sizeInBytes = (uint)(sizeof(T) * data.Length);
// fixed (T* dataPtr = data)
// {
// return CreateUploadBuffer(device, sizeInBytes, dataPtr, flags);
// }
//}
//public static ID3D12Resource CreateUploadBuffer<T>(
// ID3D12Device device,
// Span<T> data,
// ResourceFlags flags = ResourceFlags.None)
// where T : unmanaged
//{
// var sizeInBytes = (uint)(sizeof(T) * data.Length);
// fixed (T* dataPtr = data)
// {
// return CreateUploadBuffer(device, sizeInBytes, dataPtr, flags);
// }
//}
//public static ID3D12Resource CreateUploadBuffer(
// ID3D12Device device,
// uint sizeInBytes,
// void* data = default,
// ResourceFlags flags = ResourceFlags.None)
//{
// if (sizeInBytes > _MAX_BYTES)
// {
// throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {sizeInBytes})");
// }
// var buffer = device.CreateCommittedResource(
// HeapType.Upload,
// HeapFlags.None,
// ResourceDescription.Buffer(sizeInBytes, flags),
// ResourceStates.GenericRead
// );
// if (data is not null)
// {
// void* mappedPtr = default;
// buffer.Map(0, null, &mappedPtr).CheckError();
// Unsafe.CopyBlock(data, mappedPtr, sizeInBytes);
// buffer.Unmap(0, null);
// }
// return buffer;
//}
//public static ID3D12Resource CreateReadbackBuffer(
// ID3D12Device device,
// uint sizeInBytes,
// ResourceFlags flags = ResourceFlags.None)
//{
// if (sizeInBytes > _MAX_BYTES)
// {
// throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {sizeInBytes})");
// }
// var buffer = device.CreateCommittedResource(
// HeapType.Readback,
// HeapFlags.None,
// ResourceDescription.Buffer(sizeInBytes, flags),
// _INITIALREADTARGETSTATE
// );
// return buffer;
//}
//public static ID3D12Resource CreateUAVBuffer(ID3D12Device device, uint bufferSize,
// ResourceStates initialState = ResourceStates.Common,
// ResourceFlags flags = ResourceFlags.None)
//{
// if (bufferSize > _MAX_BYTES)
// {
// throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {bufferSize})");
// }
// var buffer = device.CreateCommittedResource(
// HeapType.Default,
// HeapFlags.None,
// ResourceDescription.Buffer(bufferSize, ResourceFlags.AllowUnorderedAccess | flags),
// _INITIALCOPYTARGETSTATE
// );
// return buffer;
//}
//public static ID3D12Resource CreateTexture2D<T>(
// ID3D12Device device,
// D3D12ResourceUploadBatch resourceUpload,
// uint width, uint height, Format format,
// Span<T> data,
// bool generateMips = false,
// ResourceStates afterState = ResourceStates.PixelShaderResource,
// ResourceFlags flags = ResourceFlags.None)
// where T : unmanaged
//{
// if (width > D3D12.RequestTexture2DUOrVDimension || height > D3D12.RequestTexture2DUOrVDimension)
// {
// throw new InvalidOperationException($"ERROR: Resource dimensions too large for DirectX 12 (2D: size {width} by {height})");
// }
// ushort mipLevels = 1;
// if (generateMips)
// {
// generateMips = resourceUpload.IsSupportedForGenerateMips(format);
// if (generateMips)
// {
// mipLevels = (ushort)TextureUtility.CountMips(width, height);
// }
// }
// var texture = device.CreateCommittedResource(
// HeapType.Default,
// HeapFlags.None,
// ResourceDescription.Texture2D(format, width, height, 1, mipLevels, 1, 0, flags),
// _INITIALCOPYTARGETSTATE
// );
// fixed (T* dataPtr = data)
// {
// FormatHelper.GetSurfaceInfo(format, width, height, out var rowPitch, out var slicePitch);
// SubresourceData initData = new()
// {
// pData = dataPtr,
// RowPitch = (nint)rowPitch,
// SlicePitch = (nint)slicePitch
// };
// resourceUpload.Upload(texture, 0, &initData, 1);
// resourceUpload.Transition(texture, ResourceStates.CopyDest, afterState);
// if (generateMips)
// {
// resourceUpload.GenerateMips(texture);
// }
// return texture;
// }
//}
public GraphicsResource CreateUploadBuffer(uint sizeInBytes, bool tempResource = false, ResourceFlags flags = ResourceFlags.None)
private Guid* IID_NULL
{
if (sizeInBytes > _MAX_BYTES)
get
{
throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {sizeInBytes})");
fixed (Guid* pGuid = &Guid.Empty)
{
return pGuid;
}
}
var heapProperties = new HeapProperties(HeapType.Upload);
var resourceDescription = ResourceDescription.Buffer(sizeInBytes, flags);
ComPtr<ID3D12Resource> buffer = default;
GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateCommittedResource(
&heapProperties,
HeapFlags.None,
&resourceDescription,
ResourceStates.GenericRead,
null,
__uuidof<ID3D12Resource>(),
buffer.GetVoidAddressOf()
);
var resource = new GraphicsResource(buffer.Move(), tempResource);
if (tempResource)
{
_temResources.Enqueue(new(resource));
}
return resource;
}
public GraphicsResource CreateCopyDestinationBuffer(uint sizeInBytes, bool tempResource = false, ResourceFlags flags = ResourceFlags.None)
public ResourceAllocator()
{
var desc = new AllocatorDesc
{
pAdapter = (IDXGIAdapter*)GraphicsPipeline.GraphicsDevice.Adapter.Ptr,
pDevice = (ID3D12Device*)GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr,
Flags = AllocatorFlags.DefaultPoolsNotZeroed | AllocatorFlags.MSAATexturesAlwaysCommitted
};
CreateAllocator(in desc, out _allocator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CheckBufferSize(uint sizeInBytes)
{
if (sizeInBytes > _MAX_BYTES)
{
throw new InvalidOperationException($"ERROR: Resource size too large for DirectX 12 (size {sizeInBytes})");
}
}
var heapProperties = new HeapProperties(HeapType.Default);
var resourceDescription = ResourceDescription.Buffer(sizeInBytes, flags);
ComPtr<ID3D12Resource> buffer = default;
GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateCommittedResource(
&heapProperties,
HeapFlags.None,
&resourceDescription,
ResourceStates.Common,
null,
__uuidof<ID3D12Resource>(),
buffer.GetVoidAddressOf()
);
var resource = new GraphicsResource(buffer.Move(), tempResource);
if (tempResource)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CheckTexture2DSize(uint width, uint height)
{
if (width > _MAX_TEXTURE2D_DIMENSION || height > _MAX_TEXTURE2D_DIMENSION)
{
_temResources.Enqueue(new(resource));
throw new InvalidOperationException($"ERROR: Texture size too large for DirectX 12 (width {width}, height {height})");
}
}
return resource;
private ResourceHandle TrackResource(in Allocation allocation, bool isTemp)
{
int id;
uint generation;
AllocationInfo allocInfo;
lock (_lock)
{
if (_freeSlots.Count > 0)
{
id = _freeSlots.Dequeue();
var info = _allocations[id];
if (info.Allocated)
{
throw new InvalidOperationException($"ERROR: Resource ID {id} registered as free but still allocated.");
}
generation = info.generation + 1;
allocInfo = new AllocationInfo(in allocation, generation);
_allocations[id] = allocInfo;
}
else
{
id = _allocations.Count;
generation = 0u;
allocInfo = new AllocationInfo(in allocation, generation);
_allocations.Add(allocInfo);
}
var handle = new ResourceHandle(id, generation);
if (isTemp)
{
_temResources.Enqueue(handle);
}
return handle;
}
}
public TextureHandle CreateTexture2D(uint width, uint height, ushort mipLevels, Format format = Format.R8G8B8A8Unorm, ResourceFlags resFlags = ResourceFlags.None, AllocationFlags allocFlags = AllocationFlags.None, ResourceStates state = ResourceStates.Common, bool tempResource = false)
{
CheckTexture2DSize(width, height);
var resourceDesc = ResourceDescription.Tex2D(format, width, height, mipLevels: mipLevels, arraySize: 1, flags: resFlags);
var allocationDesc = new AllocationDesc
{
HeapType = HeapType.Default,
Flags = allocFlags
};
Allocation allocation = default;
ThrowIfFailed(_allocator.CreateResource(&allocationDesc, in resourceDesc, state, null, &allocation, IID_NULL, null));
return new(TrackResource(in allocation, tempResource));
}
public BufferHandle CreateBuffer(uint sizeInBytes, HeapType heapType = HeapType.Default, ResourceFlags resFlags = ResourceFlags.None, AllocationFlags allocFlags = AllocationFlags.None, ResourceStates initialState = ResourceStates.Common, bool tempResource = false)
{
CheckBufferSize(sizeInBytes);
var resourceDescription = ResourceDescription.Buffer(sizeInBytes, resFlags);
var allocationDesc = new AllocationDesc
{
HeapType = heapType,
Flags = allocFlags
};
Allocation allocation = default;
ThrowIfFailed(_allocator.CreateResource(&allocationDesc, in resourceDescription, initialState, null, &allocation, IID_NULL, null));
return new(TrackResource(in allocation, tempResource));
}
public BufferHandle CreateUploadBuffer(uint sizeInBytes, bool tempResource = false)
{
return CreateBuffer(sizeInBytes, HeapType.Upload, ResourceFlags.None, AllocationFlags.None, ResourceStates.GenericRead, tempResource);
}
public void ReleaseTempResource()
{
while (_temResources.Count > 0)
{
var info = _temResources.Peek();
ref var handle = ref _temResources.Peek();
ref var info = ref _allocations[handle.id];
if (info.cpuFenceValue > GraphicsPipeline.GPUFenceValue)
{
break;
}
info.resource.DisposeInternal();
ReleaseAllocation(in handle);
_temResources.Dequeue();
}
}
public Allocation GetAllocation(in ResourceHandle handle)
{
if (!handle.IsValid)
{
throw new InvalidOperationException("Invalid resource handle.");
}
lock (_lock)
{
ref var allocationInfo = ref _allocations[handle.id];
if (!allocationInfo.Allocated || allocationInfo.generation != handle.generation)
{
throw new InvalidOperationException($"Resource with ID {handle.id} and generation {handle.generation} is not allocated or has been released.");
}
return allocationInfo.allocation;
}
}
public void ReleaseAllocation(in ResourceHandle handle)
{
if (!handle.IsValid)
{
return;
}
lock (_lock)
{
ref var allocationInfo = ref _allocations[handle.id];
if (!allocationInfo.Allocated || allocationInfo.generation != handle.generation)
{
return;
}
allocationInfo.Dispose();
_freeSlots.Enqueue(handle.id);
}
}
public void Dispose()
{
ReleaseTempResource();
#if DEBUG
if (_allocations.Count > 0)
{
throw new InvalidOperationException($"ResourceAllocator is being disposed with {_allocations.Count} allocations still registered. Ensure all resources are released before disposing.");
}
#endif
for (var i = 0; i < _allocations.Count; i++)
{
_allocations[i].Dispose();
}
_allocations.Dispose();
_temResources.Dispose();
_allocator.Release();
}
}

View File

@@ -9,18 +9,6 @@ namespace Ghost.Graphics.D3D12;
/// </summary>
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<ID3D12CommandAllocator> _commandAllocator;
private ComPtr<ID3D12GraphicsCommandList10> _commandList;
private ComPtr<ID3D12Fence> _fence;
@@ -89,7 +77,7 @@ internal unsafe class ResourceUploadBatch : IDisposable
/// <typeparam name="T">Type of data to upload</typeparam>
/// <param name="resource">Destination resource</param>
/// <param name="data">Source data</param>
public void Upload<T>(GraphicsResource resource, ReadOnlySpan<T> data)
public void Upload<T>(ID3D12Resource* resource, ReadOnlySpan<T> data)
where T : unmanaged
{
if (!_isRecording)
@@ -98,21 +86,22 @@ internal unsafe class ResourceUploadBatch : IDisposable
}
var sizeInBytes = (uint)(data.Length * sizeof(T));
// Create upload buffer
var uploadBuffer = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer(sizeInBytes, true);
// Copy data to upload buffer
void* mappedData;
var uploadResource = uploadBuffer.ResourceHandle.GetAllocation().Resource;
uploadResource->Map(0, null, &mappedData);
fixed (T* dataPtr = data)
{
uploadBuffer.SetData(dataPtr, (uint)data.Length);
Unsafe.CopyBlock(mappedData, dataPtr, sizeInBytes);
}
uploadResource->Unmap(0, null);
// Copy from upload buffer to destination
_commandList.Get()->CopyBufferRegion(
resource.NativeResource.Ptr,
resource,
0,
uploadBuffer.NativeResource.Ptr,
uploadResource,
0,
sizeInBytes);
}
@@ -124,22 +113,20 @@ internal unsafe class ResourceUploadBatch : IDisposable
/// <param name="firstSubresource">First subresource index</param>
/// <param name="subresources">Subresource data array</param>
/// <param name="numSubresources">Number of subresources</param>
public void Upload(GraphicsResource resource, uint firstSubresource, SubresourceData* subresources, uint numSubresources)
public void Upload(ID3D12Resource* resource, uint firstSubresource, SubresourceData* subresources, uint numSubresources)
{
if (!_isRecording)
{
throw new InvalidOperationException("Upload batch is not recording");
}
var resourceDesc = resource.NativeResource.Ptr->GetDesc();
var resourceDesc = resource->GetDesc();
var requiredSize = GetRequiredIntermediateSize(resource, firstSubresource, numSubresources);
var uploadBuffer = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer((uint)requiredSize, true);
UpdateSubresources(
resource.NativeResource.Ptr,
uploadBuffer.NativeResource.Ptr,
resource,
uploadBuffer.ResourceHandle.GetAllocation().Resource,
0,
firstSubresource,
numSubresources,
@@ -152,31 +139,14 @@ internal unsafe class ResourceUploadBatch : IDisposable
/// <param name="resource">Resource to transition</param>
/// <param name="stateBefore">State before transition</param>
/// <param name="stateAfter">State after transition</param>
public void Transition(GraphicsResource resource, ResourceStates stateBefore, ResourceStates stateAfter)
public void Transition(ID3D12Resource* 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);
_commandList.Get()->ResourceBarrierTransition(resource, stateBefore, stateAfter);
}
/// <summary>
@@ -229,9 +199,9 @@ internal unsafe class ResourceUploadBatch : IDisposable
}
}
private ulong GetRequiredIntermediateSize(GraphicsResource destinationResource, uint firstSubresource, uint numSubresources)
private ulong GetRequiredIntermediateSize(ID3D12Resource* destinationResource, uint firstSubresource, uint numSubresources)
{
var resourceDesc = destinationResource.NativeResource.Ptr->GetDesc();
var resourceDesc = destinationResource->GetDesc();
ulong requiredSize = 0;
var numRows = stackalloc uint[(int)numSubresources];

View File

@@ -0,0 +1,217 @@
using Ghost.Core;
using System.Diagnostics;
using Win32;
using Win32.Graphics.Direct3D12;
using DescriptorIndex = System.UInt32;
namespace Ghost.Graphics.D3D12.Utilities;
/// <summary>
/// Specialized descriptor heap allocator for SM 6.6 bindless rendering with ResourceDescriptorHeap[index].
/// This allocator maintains a 1:1 relationship between allocation indices and shader indices.
/// </summary>
internal unsafe struct BindlessDescriptorHeapAllocator : IDisposable
{
private const DescriptorIndex _INVALID_DESCRIPTOR_INDEX = ~0u;
private readonly ConstPtr<ID3D12Device14> _device;
private readonly Lock _lock = new();
private ComPtr<ID3D12DescriptorHeap> _bindlessHeap;
private CpuDescriptorHandle _startCpuHandle;
private GpuDescriptorHandle _startGpuHandle;
private Queue<uint> _freeDescriptors;
private uint _stride;
public DescriptorHeapType HeapType
{
get;
}
public uint NumDescriptors
{
get; private set;
}
public uint NumAllocatedDescriptors
{
get; private set;
}
public uint Stride => _stride;
public readonly ConstPtr<ID3D12DescriptorHeap> BindlessHeap => new(_bindlessHeap.Get());
public BindlessDescriptorHeapAllocator(ConstPtr<ID3D12Device14> device, uint numDescriptors = 10000)
{
_device = device;
HeapType = DescriptorHeapType.CbvSrvUav;
NumDescriptors = numDescriptors;
_stride = device.Ptr->GetDescriptorHandleIncrementSize(DescriptorHeapType.CbvSrvUav);
_freeDescriptors = new Queue<uint>();
var success = AllocateResources(numDescriptors);
Debug.Assert(success);
_bindlessHeap.Get()->SetName("bindless");
}
public DescriptorIndex AllocateDescriptor()
{
lock (_lock)
{
if (_freeDescriptors.Count == 0)
{
// Try to grow the heap
if (!Grow(NumDescriptors * 2))
{
Debug.WriteLine("ERROR: Failed to grow bindless descriptor heap!");
return _INVALID_DESCRIPTOR_INDEX;
}
}
var index = _freeDescriptors.Dequeue();
NumAllocatedDescriptors++;
return index;
}
}
public DescriptorIndex AllocateDescriptors(uint count)
{
lock (_lock)
{
if (_freeDescriptors.Count < count)
{
// Try to grow the heap
var newSize = Math.Max(NumDescriptors * 2, NumDescriptors + count);
if (!Grow(newSize))
{
Debug.WriteLine("ERROR: Failed to grow bindless descriptor heap!");
return _INVALID_DESCRIPTOR_INDEX;
}
}
var baseIndex = _freeDescriptors.Dequeue();
for (uint i = 1; i < count; i++)
{
_freeDescriptors.Dequeue();
}
NumAllocatedDescriptors += count;
return baseIndex;
}
}
public void ReleaseDescriptor(DescriptorIndex index)
{
lock (_lock)
{
if (index >= NumDescriptors)
{
Debug.WriteLine("Error: Attempted to release an invalid descriptor index");
return;
}
_freeDescriptors.Enqueue(index);
NumAllocatedDescriptors--;
}
}
public void ReleaseDescriptors(DescriptorIndex baseIndex, uint count = 1)
{
lock (_lock)
{
for (uint i = 0; i < count; i++)
{
var index = baseIndex + i;
if (index >= NumDescriptors)
{
Debug.WriteLine("Error: Attempted to release an invalid descriptor index");
continue;
}
_freeDescriptors.Enqueue(index);
}
NumAllocatedDescriptors -= count;
}
}
public CpuDescriptorHandle GetCpuHandle(DescriptorIndex index)
{
var handle = _startCpuHandle;
return handle.Offset((int)index, _stride);
}
public GpuDescriptorHandle GetGpuHandle(DescriptorIndex index)
{
var handle = _startGpuHandle;
return handle.Offset((int)index, _stride);
}
public GpuDescriptorHandle GetGpuHandleStart()
{
return _startGpuHandle;
}
private bool AllocateResources(uint numDescriptors)
{
NumDescriptors = numDescriptors;
_bindlessHeap.Dispose();
var heapDesc = new DescriptorHeapDescription
{
Type = HeapType,
NumDescriptors = numDescriptors,
Flags = DescriptorHeapFlags.ShaderVisible, // Must be shader visible for SM 6.6
NodeMask = 0
};
fixed (void* heapPtr = &_bindlessHeap)
{
var hr = _device.Ptr->CreateDescriptorHeap(&heapDesc, __uuidof<ID3D12DescriptorHeap>(), (void**)heapPtr);
if (hr.Failure)
{
return false;
}
}
_startCpuHandle = _bindlessHeap.Get()->GetCPUDescriptorHandleForHeapStart();
_startGpuHandle = _bindlessHeap.Get()->GetGPUDescriptorHandleForHeapStart();
// Initialize free descriptor queue
_freeDescriptors.Clear();
for (uint i = 0; i < numDescriptors; i++)
{
_freeDescriptors.Enqueue(i);
}
return true;
}
private bool Grow(uint minRequiredSize)
{
var oldSize = NumDescriptors;
var newSize = Math.Max(minRequiredSize, oldSize * 2);
var oldHeap = _bindlessHeap;
if (!AllocateResources(newSize))
{
return false;
}
// Copy old descriptors to new heap
if (oldHeap.Get() is not null)
{
_device.Ptr->CopyDescriptorsSimple(oldSize, _startCpuHandle, oldHeap.Get()->GetCPUDescriptorHandleForHeapStart(), HeapType);
}
return true;
}
public void Dispose()
{
_bindlessHeap.Dispose();
}
}

View File

@@ -50,7 +50,7 @@ internal unsafe struct DescriptorHeapAllocator : IDisposable
public readonly ConstPtr<ID3D12DescriptorHeap> Heap => new(_heap.Get());
public readonly ConstPtr<ID3D12DescriptorHeap> ShaderVisibleHeap => new(_shaderVisibleHeap.Get());
public DescriptorHeapAllocator(ConstPtr<ID3D12Device14> device, DescriptorHeapType type, uint numDescriptors)
public DescriptorHeapAllocator(string name, ConstPtr<ID3D12Device14> device, DescriptorHeapType type, uint numDescriptors)
{
_device = device;
HeapType = type;
@@ -60,6 +60,12 @@ internal unsafe struct DescriptorHeapAllocator : IDisposable
var success = AllocateResources(numDescriptors);
Debug.Assert(success);
_heap.Get()->SetName(name);
if (ShaderVisible)
{
_shaderVisibleHeap.Get()->SetName($"{name} Shader Visible");
}
}
public DescriptorIndex AllocateDescriptor() => AllocateDescriptors(1);

View File

@@ -1,10 +1,12 @@
using System.Drawing;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.Data;
/// <summary>
/// Represents a color with 32-bit components."/>
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 4)]
public struct Color32 : IEquatable<Color32>
{
public byte r;
@@ -59,6 +61,7 @@ public struct Color32 : IEquatable<Color32>
/// <summary>
/// Represents a color with 128-bit components.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 16)]
public struct Color128 : IEquatable<Color128>
{
public float r;

View File

@@ -1,22 +1,24 @@
using Ghost.Graphics.D3D12;
using Ghost.Graphics.D3D12;
using Ghost.Graphics.Shading;
using System.Numerics;
using System.Runtime.CompilerServices;
using Win32.Graphics.Direct3D12;
namespace Ghost.Graphics.Data;
/// <summary>
/// Material implementation for bindless rendering with SM 6.6 support
/// </summary>
public unsafe class Material : IDisposable
{
private readonly CBufferCache[] _cbufferCaches;
private readonly Texture2D?[] _textures;
private readonly Dictionary<string, int> _textureNameToSlotMap;
private readonly List<Texture2D> _textures = new();
private bool _disposed;
public Shader Shader
{
get;
set;
get; set;
}
public Material(Shader shader)
@@ -37,29 +39,6 @@ public unsafe class Material : IDisposable
{
_cbufferCaches = Array.Empty<CBufferCache>();
}
// Initialize texture storage
if (shader.Textures.Count > 0)
{
var maxTextureSlot = shader.Textures.Max(t => t.RegisterSlot);
_textures = new Texture2D?[maxTextureSlot + 1];
_textureNameToSlotMap = new Dictionary<string, int>();
foreach (var textureInfo in shader.Textures)
{
_textureNameToSlotMap.Add(textureInfo.Name, (int)textureInfo.RegisterSlot);
}
}
else
{
_textures = Array.Empty<Texture2D?>();
_textureNameToSlotMap = new Dictionary<string, int>();
}
}
~Material()
{
Dispose();
}
/// <summary>
@@ -84,6 +63,28 @@ public unsafe class Material : IDisposable
SetFloat(Shader.GetPropertyId(propertyName), in value);
}
/// <summary>
/// Sets a uint property in the material's constant buffer (useful for texture indices).
/// </summary>
/// <param name="propertyId">The ID of the property to set.</param>
/// <param name="value">The value to set for the property.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetUInt(int propertyId, in uint value)
{
WriteToCache(propertyId, in value);
}
/// <summary>
/// Sets a uint property in the material's constant buffer (useful for texture indices).
/// </summary>
/// <param name="propertyName">The name of the property to set.</param>
/// <param name="value">The value to set for the property.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetUInt(string propertyName, in uint value)
{
SetUInt(Shader.GetPropertyId(propertyName), in value);
}
/// <summary>
/// Sets a Vector property in the material's constant buffer.
/// </summary>
@@ -109,7 +110,7 @@ public unsafe class Material : IDisposable
/// <summary>
/// Sets a Matrix property in the material's constant buffer.
/// </summary>
/// <param name="propertyName">The ID of the property to set.</param>
/// <param name="propertyId">The ID of the property to set.</param>
/// <param name="value">The value to set for the property.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetMatrix(int propertyId, in Matrix4x4 value)
@@ -129,73 +130,42 @@ public unsafe class Material : IDisposable
}
/// <summary>
/// Sets a texture property in the material.
/// Adds a bindless texture to the material and returns its index
/// </summary>
/// <param name="textureId">The ID of the texture to set.</param>
/// <param name="texture">The texture to set.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetTexture(int textureId, Texture2D? texture)
/// <param name="texture">The bindless texture to add</param>
/// <returns>The index of the texture in the material's texture list</returns>
public int AddTexture(Texture2D texture)
{
if (textureId == -1)
{
throw new ArgumentException("Texture ID is invalid.");
}
if (textureId >= _textures.Length)
{
throw new ArgumentException($"Texture ID {textureId} is out of range.");
}
_textures[textureId] = texture;
_textures.Add(texture);
return _textures.Count - 1;
}
/// <summary>
/// Sets a texture property in the material.
/// Sets a texture index for a shader property (for bindless texture access)
/// </summary>
/// <param name="textureName">The name of the texture to set.</param>
/// <param name="texture">The texture to set.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetTexture(string textureName, Texture2D? texture)
/// <param name="propertyName">The name of the shader property (e.g., "_TextureIndex1")</param>
/// <param name="texture">The bindless texture to reference</param>
public void SetTextureIndex(string propertyName, Texture2D texture)
{
if (!_textureNameToSlotMap.TryGetValue(textureName, out var slot))
{
throw new ArgumentException($"Texture '{textureName}' not found in shader.");
}
_textures[slot] = texture;
SetUInt(propertyName, texture.DescriptorIndex);
}
/// <summary>
/// Gets a texture property from the material.
/// Sets the mesh buffer indices for bindless vertex and index buffer access
/// </summary>
/// <param name="textureId">The ID of the texture to get.</param>
/// <returns>The texture, or null if not set.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Texture2D? GetTexture(int textureId)
/// <param name="mesh">The mesh whose buffer indices to set</param>
/// <param name="vertexBufferIndexProperty">The name of the vertex buffer index property (e.g., "_VertexBufferIndex")</param>
/// <param name="indexBufferIndexProperty">The name of the index buffer index property (e.g., "_IndexBufferIndex")</param>
public void SetMeshBufferIndices(Mesh mesh, string vertexBufferIndexProperty = "_VertexBufferIndex", string indexBufferIndexProperty = "_IndexBufferIndex")
{
if (textureId == -1 || textureId >= _textures.Length)
{
return null;
}
return _textures[textureId];
SetUInt(vertexBufferIndexProperty, mesh.VertexBufferDescriptorIndex);
SetUInt(indexBufferIndexProperty, mesh.IndexBufferDescriptorIndex);
}
/// <summary>
/// Gets a texture property from the material.
/// Gets all textures used by this material
/// </summary>
/// <param name="textureName">The name of the texture to get.</param>
/// <returns>The texture, or null if not set.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Texture2D? GetTexture(string textureName)
{
if (!_textureNameToSlotMap.TryGetValue(textureName, out var slot))
{
return null;
}
return _textures[slot];
}
public IReadOnlyList<Texture2D> Textures => _textures;
private unsafe void WriteToCache<T>(int propertyId, in T value) where T : unmanaged
{
@@ -226,28 +196,35 @@ public unsafe class Material : IDisposable
}
}
/// <summary>
/// Binds the material for bindless rendering
/// </summary>
/// <param name="cmd">Command list to bind to</param>
internal void Bind(CommandList cmd)
{
var commandList = cmd.NativeCommandList.Ptr;
// Set root signature and pipeline state
commandList->SetGraphicsRootSignature(Shader.RootSignature);
commandList->SetPipelineState(Shader.PipelineState);
// Set descriptor heaps - CRUCIAL: Use the specialized bindless heap for SM 6.6
var heaps = stackalloc ID3D12DescriptorHeap*[2];
heaps[0] = GraphicsPipeline.DescriptorAllocator.GetBindlessHeap().Ptr; // Specialized bindless heap
heaps[1] = Shader.SamplerHeap.Ptr; // Sampler heap from shader
commandList->SetDescriptorHeaps(2, heaps);
// Bind constant buffers
var rootParamIndex = 0u;
foreach (var cbufferInfo in Shader.ConstantBuffers)
{
var cache = _cbufferCaches[cbufferInfo.RegisterSlot];
cmd.SetGraphicsRootConstantBufferView(cbufferInfo.RegisterSlot, cache.GpuResource.GPUAddress);
commandList->SetGraphicsRootConstantBufferView(rootParamIndex++, 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);
}
}
// Bind sampler descriptor table (last root parameter)
var samplerGpuHandle = Shader.SamplerHeap.Ptr->GetGPUDescriptorHandleForHeapStart();
commandList->SetGraphicsRootDescriptorTable(rootParamIndex, samplerGpuHandle);
}
public void Dispose()
@@ -262,6 +239,9 @@ public unsafe class Material : IDisposable
cache.Dispose();
}
// NOTE: We don't dispose the textures here as they might be shared
// The user is responsible for disposing BindlessTexture2D instances
GC.SuppressFinalize(this);
_disposed = true;

View File

@@ -1,5 +1,5 @@
using Ghost.Core;
using Ghost.Graphics.D3D12;
using Ghost.Graphics.D3D12;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Numerics;
@@ -16,10 +16,11 @@ public unsafe sealed class Mesh(int initialVertexCapacity = 256, int initialInde
private Bounds _boundingBox;
private GraphicsResource? _vertexBuffer;
private GraphicsResource? _indexBuffer;
private VertexBufferView _vertexBufferView;
private IndexBufferView _indexBufferView;
private GraphicsBuffer? _vertexBuffer;
private GraphicsBuffer? _indexBuffer;
private BindlessDescriptor? _vertexBufferDescriptor;
private BindlessDescriptor? _indexBufferDescriptor;
public Span<Vertex> Vertices => _vertices.AsSpan();
public Span<int> Indices => _indices.AsSpan();
@@ -28,8 +29,8 @@ public unsafe sealed class Mesh(int initialVertexCapacity = 256, int initialInde
public uint VertexCount => (uint)_vertices.Count;
public uint IndexCount => (uint)_indices.Count;
internal ConstPtr<VertexBufferView> VertexBufferView => (VertexBufferView*)Unsafe.AsPointer(ref _vertexBufferView);
internal ConstPtr<IndexBufferView> IndexBufferView => (IndexBufferView*)Unsafe.AsPointer(ref _indexBufferView);
public uint VertexBufferDescriptorIndex => _vertexBufferDescriptor?.Index ?? throw new InvalidOperationException("Vertex buffer descriptor is not allocated.");
public uint IndexBufferDescriptorIndex => _indexBufferDescriptor?.Index ?? throw new InvalidOperationException("Index buffer descriptor is not allocated.");
~Mesh()
{
@@ -65,6 +66,24 @@ public unsafe sealed class Mesh(int initialVertexCapacity = 256, int initialInde
_indices.Add(index2);
}
public void AddTriangles(params ReadOnlySpan<int> indices)
{
if (indices.Length % 3 != 0)
{
throw new ArgumentException("The number of indices must be a multiple of 3 to form triangles.");
}
foreach (var index in indices)
{
if (index < 0 || index >= _vertices.Count)
{
throw new ArgumentOutOfRangeException(nameof(indices), "Index out of range for the current vertex count.");
}
_indices.Add(index);
}
}
/// <summary>
/// Reduces the memory usage of the internal collections by resizing them to match their current element count.
/// </summary>
@@ -211,35 +230,76 @@ public unsafe sealed class Mesh(int initialVertexCapacity = 256, int initialInde
return;
}
_vertexBuffer?.Dispose();
_indexBuffer?.Dispose();
DisposeGpuResources();
var vertexBufferSize = (uint)(VertexCount * sizeof(Vertex));
var indexBufferSize = IndexCount * sizeof(int);
_vertexBuffer = GraphicsPipeline.ResourceAllocator.CreateCopyDestinationBuffer(vertexBufferSize);
_indexBuffer = GraphicsPipeline.ResourceAllocator.CreateCopyDestinationBuffer(indexBufferSize);
_vertexBuffer = GraphicsBuffer.Create(vertexBufferSize, GraphicsBuffer.Usage.CopyDestination);
_indexBuffer = GraphicsBuffer.Create(indexBufferSize, GraphicsBuffer.Usage.CopyDestination);
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());
var uploadBatch = GraphicsPipeline.UploadBatch;
uploadBatch.Upload(_vertexBuffer.NativeResource, _vertices.AsSpan());
uploadBatch.Upload(_indexBuffer.NativeResource, _indices.AsSpan());
uploadBatch.Transition(_vertexBuffer.NativeResource, ResourceStates.CopyDest, ResourceStates.VertexAndConstantBuffer);
uploadBatch.Transition(_indexBuffer.NativeResource, ResourceStates.CopyDest, ResourceStates.IndexBuffer);
_vertexBufferView = new VertexBufferView
// Create bindless descriptors for vertex and index buffers
CreateBindlessDescriptors();
}
/// <summary>
/// Creates SRVs for vertex and index buffers in the bindless descriptor heap
/// </summary>
private void CreateBindlessDescriptors()
{
if (_vertexBuffer == null || _indexBuffer == null)
{
BufferLocation = _vertexBuffer.GPUAddress,
SizeInBytes = vertexBufferSize,
StrideInBytes = (uint)sizeof(Vertex)
return;
}
// Allocate new descriptors from the descriptor allocator
_vertexBufferDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateBindless();
_indexBufferDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateBindless();
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
var vertexSrvDesc = new ShaderResourceViewDescription
{
Format = Format.R32Typeless,
ViewDimension = SrvDimension.Buffer,
Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
Anonymous = new()
{
Buffer = new()
{
FirstElement = 0,
NumElements = (uint)(_vertexBuffer.GPUAddress != 0 ? (VertexCount * sizeof(Vertex)) / 4 : 0), // Divide by 4 for R32 format
StructureByteStride = 0,
Flags = BufferSrvFlags.Raw
}
}
};
_indexBufferView = new IndexBufferView
device->CreateShaderResourceView(_vertexBuffer.NativeResource.Ptr, &vertexSrvDesc, _vertexBufferDescriptor.CpuHandle);
var indexSrvDesc = new ShaderResourceViewDescription
{
BufferLocation = _indexBuffer.GPUAddress,
SizeInBytes = indexBufferSize,
Format = Format.R32Uint
Format = Format.R32Typeless,
ViewDimension = SrvDimension.Buffer,
Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
Anonymous = new()
{
Buffer = new()
{
FirstElement = 0,
NumElements = IndexCount,
StructureByteStride = 0,
Flags = BufferSrvFlags.Raw
}
}
};
device->CreateShaderResourceView(_indexBuffer.NativeResource.Ptr, &indexSrvDesc, _indexBufferDescriptor.CpuHandle);
}
/// <summary>
@@ -259,6 +319,16 @@ public unsafe sealed class Mesh(int initialVertexCapacity = 256, int initialInde
_indexBuffer?.Dispose();
_indexBuffer = null;
if (_vertexBufferDescriptor != null)
{
GraphicsPipeline.DescriptorAllocator.ReleaseBindless(_vertexBufferDescriptor);
}
if (_indexBufferDescriptor != null)
{
GraphicsPipeline.DescriptorAllocator.ReleaseBindless(_indexBufferDescriptor);
}
}
public void Dispose()

View File

@@ -0,0 +1,305 @@
using Ghost.Graphics.D3D12;
using Win32.Graphics.Direct3D12;
using Win32.Graphics.Dxgi.Common;
namespace Ghost.Graphics.Data;
/// <summary>
/// Defines the type of render texture.
/// </summary>
public enum RenderTextureType
{
/// <summary>
/// Render target view - used for color output.
/// </summary>
ColorTarget,
/// <summary>
/// Depth stencil view - used for depth/stencil testing.
/// </summary>
DepthStencil
}
/// <summary>
/// Render texture class that encapsulates GPU resources for rendering.
/// </summary>
public unsafe class RenderTexture : Texture
{
private readonly RenderTextureType _renderTextureType;
private readonly RenderTargetDescriptor? _rtvDescriptor;
private readonly DepthStencilDescriptor? _dsvDescriptor;
private RenderTexture(uint width, uint height, Format format, RenderTextureType renderTextureType, in TextureHandle handle, BindlessDescriptor bindlessDescriptor, RenderTargetDescriptor? rtvDescriptor, DepthStencilDescriptor? dsvDescriptor)
: base(width, height, format, in handle, bindlessDescriptor)
{
_renderTextureType = renderTextureType;
_rtvDescriptor = rtvDescriptor;
_dsvDescriptor = dsvDescriptor;
}
/// <summary>
/// Gets the type of this render texture.
/// </summary>
public RenderTextureType RenderTextureType => _renderTextureType;
/// <summary>
/// Gets the render target view descriptor. Only valid for color render textures.
/// </summary>
internal RenderTargetDescriptor? RenderTargetView => _rtvDescriptor;
/// <summary>
/// Gets the depth stencil view descriptor. Only valid for depth render textures.
/// </summary>
internal DepthStencilDescriptor? DepthStencilView => _dsvDescriptor;
/// <summary>
/// Creates a color render texture.
/// </summary>
/// <param name="width">Width of the render texture</param>
/// <param name="height">Height of the render texture</param>
/// <param name="format">Color format (e.g., Format.R8G8B8A8Unorm)</param>
/// <returns>A new color render texture</returns>
public static RenderTexture CreateColorTarget(uint width, uint height, Format format = Format.R8G8B8A8Unorm, bool tempResource = false)
{
ValidateColorFormat(format);
var handle = GraphicsPipeline.ResourceAllocator.CreateTexture2D(width, height, 1, format, resFlags: ResourceFlags.AllowRenderTarget | ResourceFlags.AllowUnorderedAccess, tempResource: tempResource);
var resource = handle.ResourceHandle.GetAllocation().Resource;
var bindlessDescriptor = CreateBindlessDescriptorForRenderTexture(resource, format);
var rtvDescriptor = CreateRenderTargetView(resource, format);
return new RenderTexture(width, height, format, RenderTextureType.ColorTarget, in handle, bindlessDescriptor, rtvDescriptor, null);
}
/// <summary>
/// Creates a depth stencil render texture.
/// </summary>
/// <param name="width">Width of the render texture</param>
/// <param name="height">Height of the render texture</param>
/// <param name="format">Depth format (e.g., Format.D24UnormS8Uint, Format.D32Float)</param>
/// <returns>A new depth stencil render texture</returns>
public static RenderTexture CreateDepthStencil(uint width, uint height, Format format = Format.D24UnormS8Uint, bool tempResource = false)
{
ValidateDepthFormat(format);
var handle = GraphicsPipeline.ResourceAllocator.CreateTexture2D(width, height, 1, format, resFlags: ResourceFlags.AllowDepthStencil, tempResource: tempResource);
var resource = handle.ResourceHandle.GetAllocation().Resource;
var bindlessDescriptor = CreateBindlessDescriptorForRenderTexture(resource, GetShaderResourceFormat(format));
var dsvDescriptor = CreateDepthStencilView(resource, format);
return new RenderTexture(width, height, format, RenderTextureType.DepthStencil, in handle, bindlessDescriptor, null, dsvDescriptor);
}
/// <summary>
/// Validates that the format is suitable for color render targets.
/// </summary>
private static void ValidateColorFormat(Format format)
{
switch (format)
{
case Format.R8G8B8A8Unorm:
case Format.R8G8B8A8UnormSrgb:
case Format.B8G8R8A8Unorm:
case Format.B8G8R8A8UnormSrgb:
case Format.R16G16B16A16Float:
case Format.R32G32B32A32Float:
case Format.R16G16Float:
case Format.R32Float:
break;
default:
throw new ArgumentException($"Format {format} is not supported for color render targets.");
}
}
/// <summary>
/// Validates that the format is suitable for depth stencil targets.
/// </summary>
private static void ValidateDepthFormat(Format format)
{
switch (format)
{
case Format.D32Float:
case Format.D24UnormS8Uint:
case Format.D16Unorm:
break;
default:
throw new ArgumentException($"Format {format} is not supported for depth stencil targets.");
}
}
/// <summary>
/// Gets the shader resource format for depth textures (for sampling depth in shaders).
/// </summary>
private static Format GetShaderResourceFormat(Format depthFormat)
{
return depthFormat switch
{
Format.D32Float => Format.R32Float,
Format.D24UnormS8Uint => Format.R24UnormX8Typeless,
Format.D16Unorm => Format.R16Unorm,
_ => throw new ArgumentException($"Cannot determine shader resource format for depth format {depthFormat}")
};
}
/// <summary>
/// Creates a bindless descriptor for render texture shader resource access.
/// </summary>
private static BindlessDescriptor CreateBindlessDescriptorForRenderTexture(ID3D12Resource* resource, Format srvFormat)
{
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
var bindlessDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateBindless();
var srvDesc = new ShaderResourceViewDescription
{
Format = srvFormat,
ViewDimension = SrvDimension.Texture2D,
Texture2D = new Texture2DSrv { MipLevels = 1 },
Shader4ComponentMapping = 0x1688 // D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
};
device->CreateShaderResourceView(resource, &srvDesc, bindlessDescriptor.CpuHandle);
return bindlessDescriptor;
}
/// <summary>
/// Creates a render target view for color render textures.
/// </summary>
private static RenderTargetDescriptor CreateRenderTargetView(ID3D12Resource* resource, Format format)
{
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
var rtvDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateRTV();
var rtvDesc = new RenderTargetViewDescription
{
Format = format,
ViewDimension = RtvDimension.Texture2D,
Texture2D = new Texture2DRtv { MipSlice = 0 }
};
device->CreateRenderTargetView(resource, &rtvDesc, rtvDescriptor.CpuHandle);
return rtvDescriptor;
}
/// <summary>
/// Creates a depth stencil view for depth render textures.
/// </summary>
private static DepthStencilDescriptor CreateDepthStencilView(ID3D12Resource* resource, Format format)
{
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
var dsvDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateDSV();
var dsvDesc = new DepthStencilViewDescription
{
Format = format,
ViewDimension = DsvDimension.Texture2D,
Texture2D = new Texture2DDsv { MipSlice = 0 }
};
device->CreateDepthStencilView(resource, &dsvDesc, dsvDescriptor.CpuHandle);
return dsvDescriptor;
}
/// <summary>
/// Clears the render texture with the specified color (for color targets only).
/// </summary>
/// <param name="commandList">Command list to record clear commands</param>
/// <param name="clearColor">Color to clear to</param>
public void ClearColor(CommandList commandList, Color128 clearColor)
{
ThrowIfDisposed();
if (_renderTextureType != RenderTextureType.ColorTarget || _rtvDescriptor == null)
{
throw new InvalidOperationException("ClearColor can only be called on color render textures.");
}
commandList.NativeCommandList.Ptr->ClearRenderTargetView(_rtvDescriptor.CpuHandle, (float*)&clearColor, 0, null);
}
/// <summary>
/// Clears the depth stencil render texture.
/// </summary>
/// <param name="commandList">Command list to record clear commands</param>
/// <param name="clearDepth">Depth value to clear to (0.0 to 1.0)</param>
/// <param name="clearStencil">Stencil value to clear to</param>
public void ClearDepthStencil(CommandList commandList, ClearFlags flags, float clearDepth = 1.0f, byte clearStencil = 0)
{
ThrowIfDisposed();
if (_renderTextureType != RenderTextureType.DepthStencil || _dsvDescriptor == null)
{
throw new InvalidOperationException("ClearDepthStencil can only be called on depth stencil render textures.");
}
commandList.NativeCommandList.Ptr->ClearDepthStencilView(_dsvDescriptor.CpuHandle, flags, clearDepth, clearStencil, 0, null);
}
/// <summary>
/// Transitions the render texture to the specified resource state.
/// </summary>
/// <param name="commandList">Command list to record transition</param>
/// <param name="newState">New resource state</param>
internal void TransitionTo(CommandList commandList, ResourceStates newState)
{
ThrowIfDisposed();
commandList.BarrierTransition(this, ResourceStates.Common, newState);
}
/// <summary>
/// Convenience method to transition to render target state (for color targets).
/// </summary>
/// <param name="commandList">Command list to record transition</param>
internal void TransitionToRenderTarget(CommandList commandList)
{
if (_renderTextureType != RenderTextureType.ColorTarget)
{
throw new InvalidOperationException("TransitionToRenderTarget can only be called on color render textures.");
}
TransitionTo(commandList, ResourceStates.RenderTarget);
}
/// <summary>
/// Convenience method to transition to depth write state (for depth targets).
/// </summary>
/// <param name="commandList">Command list to record transition</param>
internal void TransitionToDepthWrite(CommandList commandList)
{
if (_renderTextureType != RenderTextureType.DepthStencil)
{
throw new InvalidOperationException("TransitionToDepthWrite can only be called on depth stencil render textures.");
}
TransitionTo(commandList, ResourceStates.DepthWrite);
}
/// <summary>
/// Convenience method to transition to shader resource state (for reading in shaders).
/// </summary>
/// <param name="commandList">Command list to record transition</param>
public void TransitionToShaderResource(CommandList commandList)
{
TransitionTo(commandList, ResourceStates.PixelShaderResource);
}
/// <summary>
/// Disposes the render texture and releases associated descriptors.
/// </summary>
public override void Dispose()
{
base.Dispose();
if (_rtvDescriptor != null)
{
GraphicsPipeline.DescriptorAllocator.ReleaseRTV(_rtvDescriptor);
}
if (_dsvDescriptor != null)
{
GraphicsPipeline.DescriptorAllocator.ReleaseDSV(_dsvDescriptor);
}
}
}

View File

@@ -0,0 +1,164 @@
using System.Runtime.CompilerServices;
using Win32.Graphics.D3D12MemoryAllocator;
namespace Ghost.Graphics.Data;
internal readonly struct ResourceHandle : IEquatable<ResourceHandle>, IDisposable
{
private const int _INVALID_ID = -1;
public readonly int id;
public readonly uint generation;
public static ResourceHandle Invalid => new(-1, 0);
internal ResourceHandle(int id, uint generation)
{
this.id = id;
this.generation = generation;
}
public bool IsValid => id != _INVALID_ID && generation >= 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Allocation GetAllocation()
{
if (!IsValid)
{
throw new InvalidOperationException("Cannot get allocation from an invalid AllocationHandle.");
}
return GraphicsPipeline.ResourceAllocator.GetAllocation(this);
}
public bool Equals(ResourceHandle other)
{
return id == other.id && generation == other.generation;
}
public override int GetHashCode()
{
unchecked
{
return (id * 397) ^ (int)generation;
}
}
public override bool Equals(object? obj)
{
return obj is ResourceHandle handle && Equals(handle);
}
public void Dispose()
{
GraphicsPipeline.ResourceAllocator.ReleaseAllocation(this);
}
public static implicit operator Allocation(ResourceHandle handle)
{
if (!handle.IsValid)
{
throw new InvalidOperationException("Cannot convert an invalid AllocationHandle to Allocation.");
}
return handle.GetAllocation();
}
public static bool operator ==(ResourceHandle left, ResourceHandle right)
{
return left.Equals(right);
}
public static bool operator !=(ResourceHandle left, ResourceHandle right)
{
return !(left == right);
}
}
public readonly struct TextureHandle : IEquatable<TextureHandle>, IDisposable
{
private readonly ResourceHandle _resourceHandle;
internal ResourceHandle ResourceHandle => _resourceHandle;
internal TextureHandle(ResourceHandle resourceHandle)
{
_resourceHandle = resourceHandle;
}
public bool IsValid => _resourceHandle.IsValid;
public bool Equals(TextureHandle other)
{
return _resourceHandle.Equals(other._resourceHandle);
}
public override bool Equals(object? obj)
{
return obj is TextureHandle other && Equals(other);
}
public override int GetHashCode()
{
return _resourceHandle.GetHashCode();
}
public void Dispose()
{
_resourceHandle.Dispose();
}
public static bool operator ==(TextureHandle left, TextureHandle right)
{
return left.Equals(right);
}
public static bool operator !=(TextureHandle left, TextureHandle right)
{
return !(left == right);
}
}
public readonly struct BufferHandle : IEquatable<BufferHandle>, IDisposable
{
private readonly ResourceHandle _resourceHandle;
internal ResourceHandle ResourceHandle => _resourceHandle;
internal BufferHandle(ResourceHandle resourceHandle)
{
_resourceHandle = resourceHandle;
}
public bool IsValid => _resourceHandle.IsValid;
public bool Equals(BufferHandle other)
{
return _resourceHandle.Equals(other._resourceHandle);
}
public override bool Equals(object? obj)
{
return obj is BufferHandle other && Equals(other);
}
public override int GetHashCode()
{
return _resourceHandle.GetHashCode();
}
public void Dispose()
{
_resourceHandle.Dispose();
}
public static bool operator ==(BufferHandle left, BufferHandle right)
{
return left.Equals(right);
}
public static bool operator !=(BufferHandle left, BufferHandle right)
{
return !(left == right);
}
}

View File

@@ -0,0 +1,147 @@
using Ghost.Graphics.D3D12;
using Win32.Graphics.Direct3D12;
using Win32.Graphics.Dxgi.Common;
namespace Ghost.Graphics.Data;
/// <summary>
/// Base class for all texture types in the graphics pipeline.
/// Provides common functionality for texture dimensions, format, and GPU resource management.
/// </summary>
public abstract unsafe class Texture : GraphicsResource
{
private readonly BindlessDescriptor _bindlessDescriptor;
/// <summary>
/// Width of the texture in pixels.
/// </summary>
public uint Width
{
get;
}
/// <summary>
/// Height of the texture in pixels.
/// </summary>
public uint Height
{
get;
}
/// <summary>
/// Number of bytes per pixel for the texture format.
/// </summary>
public uint BytesPerPixel
{
get;
}
/// <summary>
/// Format of the texture.
/// </summary>
public Format Format
{
get;
}
/// <summary>
/// The index of this texture in the global bindless descriptor heap.
/// </summary>
public uint DescriptorIndex => _bindlessDescriptor.Index;
internal Texture(uint width, uint height, Format format, in TextureHandle handle, BindlessDescriptor bindlessDescriptor)
: base(handle.ResourceHandle)
{
Width = width;
Height = height;
BytesPerPixel = GetBytesPerPixel(format);
Format = format;
_bindlessDescriptor = bindlessDescriptor;
}
/// <summary>
/// Creates a bindless shader resource view descriptor for the texture.
/// </summary>
protected static BindlessDescriptor CreateBindlessShaderResourceView(ID3D12Resource* resource, Format format)
{
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
var bindlessDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateBindless();
var srvDesc = new ShaderResourceViewDescription
{
Format = format,
ViewDimension = SrvDimension.Texture2D,
Texture2D = new Texture2DSrv { MipLevels = 1 },
Shader4ComponentMapping = 0x1688 // D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
};
device->CreateShaderResourceView(resource, &srvDesc, bindlessDescriptor.CpuHandle);
return bindlessDescriptor;
}
/// <summary>
/// Gets the bytes per pixel for the specified format.
/// </summary>
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,
Format.D32Float => 4,
Format.D24UnormS8Uint => 4,
Format.D16Unorm => 2,
_ => throw new NotSupportedException($"Format {format} is not supported.")
};
}
/// <summary>
/// Disposes the texture resources.
/// </summary>
public override void Dispose()
{
base.Dispose();
GraphicsPipeline.DescriptorAllocator.ReleaseBindless(_bindlessDescriptor);
}
}

View File

@@ -1,264 +1,84 @@
using Ghost.Graphics.D3D12;
using Ghost.Graphics.D3D12;
using Misaki.HighPerformance.Image;
using Misaki.HighPerformance.LowLevel.Buffer;
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
/// <summary>
/// Unified texture implementation that supports both bindless and regular descriptor table binding
/// for use with SM 6.6 ResourceDescriptorHeap access and traditional frame buffer textures
/// </summary>
public unsafe class Texture2D : Texture
{
private UnsafeArray<byte> _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;
}
private UnTypedArray _cpuData;
public uint Pitch
{
get;
}
public uint DataSize
private Texture2D(uint width, uint height, Format format, in TextureHandle handle, in ReadOnlySpan<byte> cpuData, BindlessDescriptor bindlessDescriptor)
: base(width, height, format, in handle, bindlessDescriptor)
{
get;
}
internal ShaderResourceDescriptor SRVDescriptor => _srvDescriptor;
public GraphicsResource? Resource => _resource;
public ReadOnlySpan<byte> CpuData => _cpuData.AsSpan();
public Texture2D(uint width, uint height, Span<byte> 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)
uint alignment;
if (BytesPerPixel > 4)
{
if (data.Length != DataSize)
{
throw new ArgumentException($"Data size mismatch. Expected {DataSize} bytes, got {data.Length} bytes.");
}
data.CopyTo(_cpuData.AsSpan());
alignment = (uint)MemoryUtilities.AlignOf<float>();
}
else
{
alignment = (uint)MemoryUtilities.AlignOf<byte>();
}
_resource = CreateGpuResource();
_srvDescriptor = CreateShaderResourceView();
}
private static uint GetBytesPerPixel(Format format)
{
return format switch
_cpuData = new UnTypedArray((uint)Size, alignment, ref AllocationManager.PersistentHandle);
if (!cpuData.IsEmpty)
{
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<ID3D12Resource> textureResource = default;
GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateCommittedResource(
&heapProperties,
HeapFlags.None,
&resourceDesc,
ResourceStates.Common,
null,
__uuidof<ID3D12Resource>(),
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
if ((uint)cpuData.Length > Size)
{
Format = Format,
Width = Width,
Height = Height,
Depth = 1,
RowPitch = (uint)((Pitch + 255) & ~255) // Align to 256 bytes
throw new ArgumentException($"Data size mismatch. Expected {Size} bytes, got {cpuData.Length} bytes.");
}
};
var srcLocation = new TextureCopyLocation(srcBuffer.NativeResource.Ptr, footprint);
var dstLocation = new TextureCopyLocation(dstTexture.NativeResource.Ptr, 0);
_cpuData.CopyFrom(cpuData);
}
}
cmd.NativeCommandList.Ptr->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, null);
public static Texture2D Create(uint width, uint height, in ReadOnlySpan<byte> data, Format format)
{
var handle = GraphicsPipeline.ResourceAllocator.CreateTexture2D(width, height, 0, format);
var bindlessDescriptor = CreateBindlessShaderResourceView(handle.ResourceHandle.GetAllocation().Resource, format);
return new Texture2D(width, height, format, in handle, in data, bindlessDescriptor);
}
public static Texture2D FromFile(string filePath)
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var image = ImageResult.FromStream(stream, ColorComponents.RGBA);
return Create(image.Width, image.Height, image.AsSpan(), Format.R8G8B8A8Unorm);
}
/// <summary>
/// Sets the entire texture data on the CPU side.
/// </summary>
/// <param name="data">The texture data to set</param>
public void SetData(ReadOnlySpan<byte> data)
public void SetData<T>(ReadOnlySpan<T> data)
where T : unmanaged
{
ObjectDisposedException.ThrowIf(_disposed, this);
ThrowIfDisposed();
if (data.Length != DataSize)
if ((uint)data.Length > Size)
{
throw new ArgumentException($"Data size mismatch. Expected {DataSize} bytes, got {data.Length} bytes.");
throw new ArgumentException($"Data size mismatch. Expected {Size} bytes, got {data.Length} bytes.");
}
data.CopyTo(_cpuData.AsSpan());
}
/// <summary>
/// Sets the texture data for a specific region on the CPU side.
/// </summary>
/// <param name="data">The texture data to set</param>
/// <param name="x">Starting X coordinate</param>
/// <param name="y">Starting Y coordinate</param>
/// <param name="width">Width of the region</param>
/// <param name="height">Height of the region</param>
public void SetData(ReadOnlySpan<byte> 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));
}
}
/// <summary>
/// Sets a single pixel value on the CPU side.
/// </summary>
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <param name="color">The color data for the pixel</param>
public void SetPixel(uint x, uint y, ReadOnlySpan<byte> 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));
_cpuData.CopyFrom(data);
}
/// <summary>
@@ -268,7 +88,8 @@ public unsafe class Texture2D : IDisposable
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <param name="color">The color value</param>
public void SetPixel<T>(uint x, uint y, T color) where T : unmanaged
public void SetPixel<T>(uint x, uint y, T color)
where T : unmanaged
{
if (sizeof(T) != BytesPerPixel)
{
@@ -279,6 +100,52 @@ public unsafe class Texture2D : IDisposable
SetPixel(x, y, colorSpan);
}
/// <summary>
/// Sets a single pixel value on the CPU side.
/// </summary>
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <param name="color">The color data for the pixel</param>
public void SetPixel(uint x, uint y, ReadOnlySpan<byte> color)
{
ThrowIfDisposed();
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 = y * Pitch + x * BytesPerPixel;
_cpuData.CopyFrom(color, 0u, offset, BytesPerPixel);
}
/// <summary>
/// Gets a single pixel value as a generic color type.
/// </summary>
/// <typeparam name="T">The color type (e.g., uint for RGBA32)</typeparam>
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <returns>The pixel color value</returns>
public T GetPixel<T>(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);
fixed (byte* pPixel = pixelData)
{
return *(T*)pPixel;
}
}
/// <summary>
/// Gets a single pixel value from the CPU side data.
/// </summary>
@@ -287,7 +154,7 @@ public unsafe class Texture2D : IDisposable
/// <returns>The pixel color data</returns>
public ReadOnlySpan<byte> GetPixel(uint x, uint y)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ThrowIfDisposed();
if (x >= Width || y >= Height)
{
@@ -298,30 +165,12 @@ public unsafe class Texture2D : IDisposable
return _cpuData.AsSpan().Slice(offset, (int)BytesPerPixel);
}
/// <summary>
/// Gets a single pixel value as a generic color type.
/// </summary>
/// <typeparam name="T">The color type (e.g., uint for RGBA32)</typeparam>
/// <param name="x">X coordinate of the pixel</param>
/// <param name="y">Y coordinate of the pixel</param>
/// <returns>The pixel color value</returns>
public T GetPixel<T>(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<T>(pixelData);
}
/// <summary>
/// Uploads the CPU-side texture data to the GPU resource.
/// </summary>
public void UploadTextureData()
{
ObjectDisposedException.ThrowIf(_disposed, this);
ThrowIfDisposed();
Format.GetSurfaceInfo((int)Width, (int)Height, out var rowPitch, out var slicePitch);
var initData = new SubresourceData()
@@ -331,27 +180,15 @@ public unsafe class Texture2D : IDisposable
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());
var uploadBatch = GraphicsPipeline.UploadBatch;
uploadBatch.Transition(NativeResource, ResourceStates.Common, ResourceStates.CopyDest);
uploadBatch.Upload(NativeResource, 0, &initData, 1);
uploadBatch.Transition(NativeResource, ResourceStates.CopyDest, ResourceStates.PixelShaderResource);
}
public void Dispose()
public override void Dispose()
{
if (_disposed)
{
return;
}
base.Dispose();
_cpuData.Dispose();
_resource.Dispose();
GraphicsPipeline.DescriptorAllocator.ReleaseSRV(_srvDescriptor);
_disposed = true;
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,317 @@
# Bindless Rendering in Ghost Engine: A Technical Deep-Dive
This document provides a detailed explanation of the bindless rendering architecture implemented in the Ghost Engine, leveraging modern Direct3D 12 features.
## 1. Introduction to Bindless Rendering
Traditional rendering approaches require the CPU to explicitly bind resources (textures, buffers, etc.) to specific "slots" in the shader pipeline before a draw call. This process, known as "binding," involves creating descriptor tables, setting them on the command list, and managing resource state transitions. While functional, this can lead to significant CPU overhead, complex state management, and a high number of draw calls.
**Bindless rendering** revolutionizes this by moving resource selection from the CPU to the GPU. Instead of binding individual resources, we bind a single, massive descriptor heap containing descriptors for (potentially) all resources. Shaders can then access any resource in this heap using a simple index, which can be passed via a constant buffer or calculated dynamically.
**Key Advantages:**
- **Reduced CPU Overhead:** Eliminates the need for constant re-binding of resources and management of numerous descriptor tables.
- **Simplified Rendering Code:** Drastically simplifies the logic for drawing objects with different materials.
- **Increased GPU Autonomy:** Allows the GPU to fetch required data on its own, leading to more efficient execution.
- **Enables Advanced Techniques:** Opens the door for techniques like "fully bindless" rendering, where even vertex and index data are fetched manually in the vertex shader.
The Ghost Engine implements a state-of-the-art bindless system using DirectX 12's Shader Model 6.6 capabilities.
## 2. Core D3D12 Implementation
The foundation of the engine's bindless architecture rests on a few key D3D12 features.
### 2.1. The Bindless Root Signature
The root signature defines how shaders access resources. For bindless, we use a special flag: `D3D12_ROOT_SIGNATURE_FLAG_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED`. This tells the GPU that shaders will be able to directly index the entire CBV/SRV/UAV descriptor heap.
Here's how the root signature is created in `Ghost.Graphics\Shading\Shader.cs`:
```csharp
// From: Ghost.Graphics\Shading\Shader.cs
private void CreateBindlessRootSignature()
{
// ... (parameter setup for constant buffers and samplers)
// Create root signature with the modern flag
fixed (RootParameter1* rootParamsPtr = rootParameters)
{
var rootSignatureDesc = new RootSignatureDescription1
{
// ... (parameters)
// 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
};
// ... (serialization and creation)
}
}
```
With this flag, the HLSL global `ResourceDescriptorHeap` becomes accessible, representing the entire heap of shader-visible resources.
### 2.2. Resource Descriptors
For a resource to be accessible in a bindless fashion, it needs a Shader Resource View (SRV) created in the global, shader-visible descriptor heap. This is handled by the `Mesh` and `Texture2D` classes.
#### Texture2D Descriptors
When a `Texture2D` is created, it allocates a "bindless descriptor" and creates an SRV for itself at that descriptor's location.
```csharp
// From: Ghost.Graphics\Data\Texture2D.cs
private BindlessDescriptor CreateBindlessDescriptors()
{
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
// Allocate bindless descriptor from the descriptor allocator
var bindlessDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateBindless();
// Create the SRV description
var srvDesc = new ShaderResourceViewDescription
{
Format = Format,
ViewDimension = SrvDimension.Texture2D,
Texture2D = new Texture2DSrv { MipLevels = 1 },
Shader4ComponentMapping = 0x1688 // D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING
};
// Create the SRV in the bindless heap
device->CreateShaderResourceView(_resource.NativeResource.Ptr, &srvDesc, bindlessDescriptor.CpuHandle);
return bindlessDescriptor;
}
```
The `bindlessDescriptor.Index` now holds the unique integer ID that shaders will use to access this texture.
#### Mesh Buffer Descriptors (Fully Bindless)
The engine takes bindless a step further by also making vertex and index buffers available as bindless resources. This enables a "fully bindless" or "meshlet-style" rendering approach where the Vertex Shader manually fetches its own data, bypassing the Input Assembler stage.
The `Mesh` class creates SRVs for its vertex and index buffers, treating them as raw byte-addressable buffers.
```csharp
// From: Ghost.Graphics\Data\Mesh.cs
private void CreateBindlessDescriptors()
{
// ... (null checks)
_vertexBufferDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateBindless();
_indexBufferDescriptor = GraphicsPipeline.DescriptorAllocator.AllocateBindless();
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
// Create SRV for vertex buffer (as ByteAddressBuffer)
var vertexSrvDesc = new ShaderResourceViewDescription
{
Format = Format.R32Typeless,
ViewDimension = SrvDimension.Buffer,
// ...
Buffer = new()
{
// ...
Flags = BufferSrvFlags.Raw // ByteAddressBuffer
}
};
device->CreateShaderResourceView(_vertexBuffer.NativeResource.Ptr, &vertexSrvDesc, _vertexBufferDescriptor.CpuHandle);
// Create SRV for index buffer (as ByteAddressBuffer)
var indexSrvDesc = new ShaderResourceViewDescription
{
Format = Format.R32Typeless,
ViewDimension = SrvDimension.Buffer,
// ...
Buffer = new()
{
// ...
Flags = BufferSrvFlags.Raw // ByteAddressBuffer
}
};
device->CreateShaderResourceView(_indexBuffer.NativeResource.Ptr, &indexSrvDesc, _indexBufferDescriptor.CpuHandle);
}
```
## 3. HLSL Shader Implementation
With the D3D12 backend in place, accessing resources in HLSL becomes remarkably simple. The `MeshRenderPass.cs` file contains a perfect example of a fully bindless shader.
### 3.1. The Constant Buffer
First, we define a constant buffer to pass the resource indices from the CPU to the GPU.
```hlsl
// From: Ghost.Graphics\RenderPasses\MeshRenderPass.cs
cbuffer ConstantBuffer : register(b0)
{
float4 _Color;
uint _TextureIndex1;
uint _TextureIndex2;
uint _TextureIndex3;
uint _TextureIndex4;
uint _VertexBufferIndex;
uint _IndexBufferIndex;
};
```
### 3.2. Vertex Shader: Manual Vertex Fetching
The vertex shader (`VSMain`) demonstrates the power of fully bindless rendering. Instead of receiving pre-fetched vertex attributes, it receives only a `vertexId` and `instanceId`. It uses the `_VertexBufferIndex` and `_IndexBufferIndex` to access the correct buffers from the global `ResourceDescriptorHeap` and manually loads the vertex data.
```hlsl
// From: Ghost.Graphics\RenderPasses\MeshRenderPass.cs
PixelInput VSMain(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID)
{
// Get bindless buffers from the global heap
ByteAddressBuffer vertexBuffer = ResourceDescriptorHeap[_VertexBufferIndex];
ByteAddressBuffer indexBuffer = ResourceDescriptorHeap[_IndexBufferIndex];
// Each instance represents one triangle
// vertexId is 0, 1, or 2
// Calculate index into the index buffer to find the vertex index
uint indexOffset = (instanceId * 3 + vertexId) * 4; // 4 bytes per index
uint vertexIndex = indexBuffer.Load(indexOffset);
// Calculate offset into the vertex buffer using the vertex index
uint vertexOffset = vertexIndex * 80; // 80 bytes per vertex (5 * float4)
// Load vertex data from the bindless vertex buffer
Vertex vertex;
vertex.position = asfloat(vertexBuffer.Load4(vertexOffset + 0));
vertex.normal = asfloat(vertexBuffer.Load4(vertexOffset + 16));
// ... etc.
// ...
return output;
}
```
### 3.3. Pixel Shader: Bindless Texture Sampling
The pixel shader (`PSMain`) uses the same principle to access textures. It uses the `_TextureIndexN` values to grab the correct `Texture2D` from the `ResourceDescriptorHeap` and samples them.
```hlsl
// From: Ghost.Graphics\RenderPasses\MeshRenderPass.cs
float4 PSMain(PixelInput input) : SV_TARGET
{
// Access textures directly from the heap using their indices
Texture2D tex1 = ResourceDescriptorHeap[_TextureIndex1];
Texture2D tex2 = ResourceDescriptorHeap[_TextureIndex2];
Texture2D tex3 = ResourceDescriptorHeap[_TextureIndex3];
Texture2D tex4 = ResourceDescriptorHeap[_TextureIndex4];
// Sample the textures
float4 color1 = tex1.Sample(_MainSampler, input.uv.xy);
// ... etc.
// Blend all textures together
float4 blendedColor = (color1 + color2 + color3 + color4) * 0.25f;
return blendedColor * _Color;
}
```
## 4. C# Usage Example
The C# side of the implementation is elegant and straightforward, abstracting the low-level details. The `MeshRenderPass` class demonstrates a typical setup.
### 4.1. Initialization
During initialization, we create the mesh and textures. The `UploadMeshData` and `UploadTextureData` calls ensure the data is on the GPU, and the underlying `Mesh` and `Texture2D` constructors have already created the necessary bindless SRVs.
We then create a `Material` and use it to bridge the gap between the C# objects and the shader's constant buffer properties.
```csharp
// From: Ghost.Graphics\RenderPasses\MeshRenderPass.cs
public void Initialize(CommandList cmd)
{
_mesh = MeshBuilder.CreateCube(0.75f);
_mesh.UploadMeshData();
_shader = new Shader(_HLSL_SOURCE);
_material = new Material(_shader);
// Load textures
_textures = new Texture2D[_textureFiles.Length];
for (var i = 0; i < _textureFiles.Length; i++)
{
_textures[i] = Texture2D.FromFile(_textureFiles[i]);
_textures[i].UploadTextureData();
}
// Set material properties, passing the descriptor indices to the shader
_material.SetVector("_Color", new Vector4(1.0f, 1.0f, 1.0f, 1.0f));
for (var i = 0; i < _textures.Length; i++)
{
var texture = _textures[i];
// This sets the uint property in the CBuffer
_material.SetTextureIndex($"_TextureIndex{i + 1}", texture);
}
// This is a helper on Material that sets _VertexBufferIndex and _IndexBufferIndex
// It is not used in the provided code, but it's the intended way.
// For this example, the indices are set implicitly by the DrawMeshBindless call.
// material.SetMeshBufferIndices(mesh);
// Uploads the constant buffer data to the GPU
_material.UploadMaterialData();
}
```
*Note: The `Material.SetTextureIndex` method is a convenient wrapper that calls `SetUInt` internally, passing the `texture.DescriptorIndex`.*
### 4.2. Execution
Executing the render pass is now incredibly simple. The `DrawMeshBindless` command takes the mesh and material, binds the necessary state (Root Signature, PSO, and the material's constant buffer), and issues a single `DrawInstanced` call.
```csharp
// From: Ghost.Graphics\RenderPasses\MeshRenderPass.cs
public void Execute(CommandList cmd)
{
// This single call handles everything!
cmd.DrawMeshBindless(_mesh!, _material!);
}
```
The `DrawMeshBindless` method (in `CommandList.cs`) is where the magic happens. It doesn't need to set vertex or index buffers on the Input Assembler. It just needs to know the number of triangles to draw.
```csharp
// From: Ghost.Graphics\D3D12\CommandList.cs (Conceptual)
public void DrawMeshBindless(Mesh mesh, Material material)
{
// 1. Bind the material (sets PSO, Root Signature, CBuffers, Samplers)
material.Bind(this);
// 2. Set the mesh buffer indices on the material
// This is the crucial step that connects the mesh to the shader
material.SetMeshBufferIndices(mesh);
material.UploadMaterialData(); // Re-upload CBuffer with mesh indices
// 3. Draw instanced.
// - VertexCountPerInstance = 3 (for a triangle)
// - InstanceCount = total number of triangles in the mesh
// - StartVertexLocation = 0
// - StartInstanceLocation = 0
uint triangleCount = mesh.IndexCount / 3;
_commandList.Get()->DrawInstanced(3, triangleCount, 0, 0);
}
```
## 5. Conclusion
The bindless architecture in the Ghost Engine is a powerful, modern, and efficient way to handle rendering. By leveraging Shader Model 6.6, it significantly reduces CPU overhead and simplifies rendering logic, paving the way for more complex and dynamic scenes. The "fully bindless" approach for mesh data further enhances this paradigm, offering maximum flexibility and performance on the GPU.

View File

@@ -1,178 +0,0 @@
using Ghost.Graphics.D3D12;
using Win32.Graphics.Dxgi.Common;
namespace Ghost.Graphics.Examples;
/// <summary>
/// Example demonstrating how to use the IDescriptorAllocator interface.
/// </summary>
public static class DescriptorAllocatorExample
{
/// <summary>
/// Example showing basic descriptor allocation and release.
/// </summary>
public static void BasicDescriptorUsage()
{
// Note: In a real application, you would get the descriptor allocator from the graphics device
// var device = GraphicsPipeline.GetGraphicsDevice<D3D12GraphicsDevice>();
// 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);
}
/// <summary>
/// Example showing how to work with D3D12-specific descriptor handles.
/// </summary>
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);
}
/// <summary>
/// Example showing descriptor management in a texture class.
/// </summary>
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
}
}
}
}
/// <summary>
/// Example texture class showing descriptor integration.
/// </summary>
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);
}
}

View File

@@ -0,0 +1,230 @@
using Ghost.Graphics.Data;
using Ghost.Graphics.RenderGraphModule;
using System.Numerics;
using Win32.Graphics.Direct3D12;
using Win32.Graphics.Dxgi.Common;
namespace Ghost.Graphics.Examples;
/// <summary>
/// Example demonstrating render graph usage with history buffers and multiple cameras
/// </summary>
public static class RenderGraphExample
{
// Example pass data structures
public struct DepthPrePassData
{
public RGTextureHandle DepthBuffer;
// Add other render targets, constants, etc.
}
public struct GBufferPassData
{
public RGTextureHandle DepthBuffer;
public RGTextureHandle AlbedoBuffer;
public RGTextureHandle NormalBuffer;
public RGTextureHandle MaterialBuffer;
}
public struct LightingPassData
{
public RGTextureHandle DepthBuffer;
public RGTextureHandle AlbedoBuffer;
public RGTextureHandle NormalBuffer;
public RGTextureHandle MaterialBuffer;
public RGTextureHandle SceneColor;
public RGTextureHandle PreviousFrameColor; // History buffer
}
public struct PostProcessPassData
{
public RGTextureHandle SceneColor;
public RGTextureHandle FinalColor;
}
/// <summary>
/// Example camera/view state class to store history buffers (like Unreal's FViewState)
/// </summary>
public class CameraViewState
{
public TextureHandle? PreviousFrameColorBuffer;
public TextureHandle? PreviousFrameDepthBuffer;
public TextureHandle? VelocityBuffer;
public Matrix4x4 PreviousViewProjectionMatrix;
}
public static void ExampleRenderGraph(CameraViewState viewState, TextureHandle backBuffer)
{
// Create render graph with optional descriptor
var renderGraphDesc = new RenderGraphDesc(initialResourceCapacity: 512, initialPassCapacity: 128);
using var renderGraph = new RenderGraph("MainRenderGraph", renderGraphDesc);
// Begin recording passes
renderGraph.BeginRecord();
// Import external resources (backbuffer, history buffers)
var backBufferHandle = renderGraph.ImportTexture(
"BackBuffer",
backBuffer,
new TextureDescription(1920, 1080, 1, Format.R8G8B8A8Unorm, ResourceFlags.AllowRenderTarget));
// Import history buffer if available (for temporal effects like TAA)
RGTextureHandle? previousFrameColorHandle = null;
if (viewState.PreviousFrameColorBuffer.HasValue)
{
previousFrameColorHandle = renderGraph.ImportTexture(
"PreviousFrameColor",
viewState.PreviousFrameColorBuffer.Value,
new TextureDescription(1920, 1080, 1, Format.R8G8B8A8Unorm));
}
// Depth Pre-pass
renderGraph.CreatePass<DepthPrePassData>("DepthPrePass")
.Setup((ref DepthPrePassData data, RenderPassBuilder builder) =>
{
// Create depth buffer as transient resource
data.DepthBuffer = builder.CreateTexture("DepthBuffer",
new TextureDescription(1920, 1080, 1, Format.D32Float, ResourceFlags.AllowDepthStencil));
// Declare write access to depth buffer
data.DepthBuffer = builder.WriteTexture(data.DepthBuffer);
})
.SetRenderFunc((ref DepthPrePassData data, RenderPassContext ctx) =>
{
// Render depth only
// Use data.DepthBuffer for rendering
})
.Compile();
// G-Buffer Pass
var gBufferOutputs = renderGraph.CreatePass<GBufferPassData>("GBufferPass")
.Setup((ref GBufferPassData data, RenderPassBuilder builder) =>
{
// Read depth buffer from previous pass
data.DepthBuffer = builder.ReadTexture(data.DepthBuffer); // This will be resolved during compilation
// Create G-Buffer targets
data.AlbedoBuffer = builder.CreateTexture("AlbedoBuffer",
new TextureDescription(1920, 1080, 1, Format.R8G8B8A8Unorm, ResourceFlags.AllowRenderTarget));
data.NormalBuffer = builder.CreateTexture("NormalBuffer",
new TextureDescription(1920, 1080, 1, Format.R8G8B8A8Snorm, ResourceFlags.AllowRenderTarget));
data.MaterialBuffer = builder.CreateTexture("MaterialBuffer",
new TextureDescription(1920, 1080, 1, Format.R8G8B8A8Unorm, ResourceFlags.AllowRenderTarget));
// Declare write access
data.AlbedoBuffer = builder.WriteTexture(data.AlbedoBuffer);
data.NormalBuffer = builder.WriteTexture(data.NormalBuffer);
data.MaterialBuffer = builder.WriteTexture(data.MaterialBuffer);
})
.SetRenderFunc((ref GBufferPassData data, RenderPassContext ctx) =>
{
// Render geometry to G-Buffer
// Use data.DepthBuffer, data.AlbedoBuffer, etc.
});
gBufferOutputs.Compile();
// Lighting Pass
var lightingOutput = renderGraph.CreatePass<LightingPassData>("LightingPass")
.Setup((ref LightingPassData data, RenderPassBuilder builder) =>
{
// Read G-Buffer outputs
data.DepthBuffer = builder.ReadTexture(data.DepthBuffer);
data.AlbedoBuffer = builder.ReadTexture(data.AlbedoBuffer);
data.NormalBuffer = builder.ReadTexture(data.NormalBuffer);
data.MaterialBuffer = builder.ReadTexture(data.MaterialBuffer);
// Create scene color output
data.SceneColor = builder.CreateTexture("SceneColor",
new TextureDescription(1920, 1080, 1, Format.R16G16B16A16Float, ResourceFlags.AllowRenderTarget));
data.SceneColor = builder.WriteTexture(data.SceneColor);
// Use previous frame color if available (for temporal effects)
if (previousFrameColorHandle.HasValue)
{
data.PreviousFrameColor = builder.ReadTexture(previousFrameColorHandle.Value);
}
})
.SetRenderFunc((ref LightingPassData data, RenderPassContext ctx) =>
{
// Perform deferred lighting
// Can access previous frame color for temporal anti-aliasing, etc.
});
lightingOutput.Compile();
// Post-Processing Pass
renderGraph.CreatePass<PostProcessPassData>("PostProcessPass")
.Setup((ref PostProcessPassData data, RenderPassBuilder builder) =>
{
// Read scene color from lighting pass
data.SceneColor = builder.ReadTexture(data.SceneColor);
// Write to backbuffer
data.FinalColor = builder.WriteTexture(backBufferHandle);
})
.SetRenderFunc((ref PostProcessPassData data, RenderPassContext ctx) =>
{
// Apply tone mapping, gamma correction, etc.
// Copy to backbuffer
});
// End recording
renderGraph.EndRecord();
// Compile the render graph (dependency analysis, topological sort, resource lifetime analysis)
renderGraph.Compile();
// Export resources for next frame (history buffers)
if (lightingOutput != null) // In real implementation, you'd track the scene color handle
{
// viewState.PreviousFrameColorBuffer = renderGraph.ExportTexture(sceneColorHandle);
}
// Execute the render graph
renderGraph.Execute();
}
/// <summary>
/// Example with multiple cameras rendering to different targets
/// </summary>
public static void MultiCameraExample(List<CameraViewState> cameras, List<TextureHandle> renderTargets)
{
using var renderGraph = new RenderGraph("MultiCameraRenderGraph");
renderGraph.BeginRecord();
for (var i = 0; i < cameras.Count; i++)
{
var camera = cameras[i];
var renderTarget = renderTargets[i];
var cameraName = $"Camera{i}";
// Import render target
var renderTargetHandle = renderGraph.ImportTexture(
$"{cameraName}_RenderTarget",
renderTarget,
new TextureDescription(1920, 1080, 1, Format.R8G8B8A8Unorm, ResourceFlags.AllowRenderTarget));
// Camera-specific rendering passes
renderGraph.CreatePass<DepthPrePassData>($"{cameraName}_DepthPrePass")
.Setup((ref DepthPrePassData data, RenderPassBuilder builder) =>
{
data.DepthBuffer = builder.CreateTexture($"{cameraName}_DepthBuffer",
new TextureDescription(1920, 1080, 1, Format.D32Float, ResourceFlags.AllowDepthStencil));
data.DepthBuffer = builder.WriteTexture(data.DepthBuffer);
})
.SetRenderFunc((ref DepthPrePassData data, RenderPassContext ctx) =>
{
// Render with camera[i] view/projection matrices
})
.Compile();
// Additional passes for this camera...
}
renderGraph.EndRecord();
renderGraph.Compile();
renderGraph.Execute();
}
}

View File

@@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StbImageSharp" Version="2.30.15" />
<!--<PackageReference Include="StbImageSharp" Version="2.30.15" />-->
<PackageReference Include="Vortice.Dxc.Native" Version="1.0.3" />
<PackageReference Include="Vortice.Win32.Graphics.D3D12MemoryAllocator" Version="2.2.7" />
<PackageReference Include="Vortice.Win32.Graphics.Direct3D.Dxc" Version="2.2.7" />
@@ -29,20 +29,14 @@
</ItemGroup>
<ItemGroup>
<Reference Include="Misaki.HighPerformance.Image">
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Image\bin\Release\net9.0\Misaki.HighPerformance.Image.dll</HintPath>
</Reference>
<Reference Include="Misaki.HighPerformance.LowLevel">
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.LowLevel\bin\Release\net9.0\Misaki.HighPerformance.LowLevel.dll</HintPath>
</Reference>
</ItemGroup>
<!--<ItemGroup>
<None Include="F:\cpp\dll\dxc\dxcompiler.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="F:\cpp\dll\dxc\dxil.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>-->
<ItemGroup>
<Using Include="Win32.Apis">
<Static>True</Static>

View File

@@ -24,9 +24,14 @@ public static class GraphicsPipeline
}
}
#if DEBUG
private static DebugLayer? s_debugLayer;
#endif
private static GraphicsDevice? s_graphicsDevice;
private static DescriptorAllocator? s_descriptorAllocator;
private static ResourceAllocator? s_resourceAllocator;
private static ResourceUploadBatch? s_uploadBatch;
private static Thread? s_renderThread;
private static FrameResource[]? s_frameResources;
@@ -37,6 +42,7 @@ public static class GraphicsPipeline
private static bool s_initialized;
private static bool s_isRunning;
private static readonly AutoResetEvent s_shutdownEvent = new(false);
internal static uint CPUFenceValue => s_cpuFenceValue;
internal static uint GPUFenceValue => s_gpuFenceValue;
@@ -46,8 +52,25 @@ public static class GraphicsPipeline
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 ResourceUploadBatch UploadBatch
{
get
{
if (s_uploadBatch == null)
{
s_uploadBatch = new();
s_uploadBatch.Begin();
}
return s_uploadBatch;
}
}
internal static void Initialize()
{
#if DEBUG
s_debugLayer = new DebugLayer();
#endif
s_graphicsDevice = new GraphicsDevice();
s_descriptorAllocator = new DescriptorAllocator();
s_resourceAllocator = new ResourceAllocator();
@@ -70,25 +93,47 @@ public static class GraphicsPipeline
private static void RenderLoop()
{
var waitHandles = new WaitHandle[2];
waitHandles[1] = s_shutdownEvent;
while (s_isRunning)
{
s_frameIndex = s_gpuFenceValue % _FRAME_COUNT;
var frameResource = s_frameResources![s_frameIndex];
frameResource.cpuReadyEvent.WaitOne();
// Wait for either CPU ready signal or shutdown signal
waitHandles[0] = frameResource.cpuReadyEvent;
var waitResult = WaitHandle.WaitAny(waitHandles);
s_graphicsDevice!.InitializePendingRenderers();
foreach (var renderer in s_graphicsDevice.Renderers)
// If shutdown was signaled or timeout occurred, exit the loop
if (!s_isRunning || waitResult == 1 || waitResult == WaitHandle.WaitTimeout)
{
renderer.ExecutePendingResize();
renderer.Render();
break;
}
s_gpuFenceValue++;
frameResource.gpuReadyEvent.Set();
// Only proceed if CPU ready event was signaled
if (waitResult == 0)
{
s_graphicsDevice!.InitializePendingRenderers();
s_resourceAllocator!.ReleaseTempResource();
s_uploadBatch?.WaitForCompletion(s_uploadBatch.End());
s_uploadBatch?.Dispose();
s_uploadBatch = null;
if (s_graphicsDevice.Renderers.Length > 0)
{
foreach (var renderer in s_graphicsDevice.Renderers)
{
renderer.ExecutePendingResize();
renderer.Render();
}
}
s_gpuFenceValue++;
frameResource.gpuReadyEvent.Set();
s_resourceAllocator!.ReleaseTempResource();
}
}
}
@@ -129,7 +174,18 @@ public static class GraphicsPipeline
internal static void Stop()
{
s_isRunning = false;
s_renderThread?.Join();
s_shutdownEvent.Set();
if (s_renderThread?.Join(TimeSpan.FromSeconds(5)) == false)
{
#if DEBUG
System.Diagnostics.Debugger.Break();
#endif
s_renderThread?.Interrupt();
}
s_shutdownEvent.Reset();
}
internal static void Shutdown()
@@ -149,6 +205,10 @@ public static class GraphicsPipeline
s_frameResources = null;
}
#if DEBUG
s_debugLayer?.Dispose();
#endif
s_initialized = false;
}
}

View File

@@ -0,0 +1,417 @@
using Ghost.Graphics.Data;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Descriptor for render graph configuration
/// </summary>
public readonly struct RenderGraphDesc
{
public readonly int InitialResourceCapacity;
public readonly int InitialPassCapacity;
public RenderGraphDesc(int initialResourceCapacity = 256, int initialPassCapacity = 64)
{
InitialResourceCapacity = initialResourceCapacity;
InitialPassCapacity = initialPassCapacity;
}
}
/// <summary>
/// Main render graph class for managing transient resources and render passes
/// </summary>
public sealed class RenderGraph : IDisposable
{
private readonly string _name;
private readonly List<RenderGraphResource> _resources;
private readonly List<RenderPass> _passes;
private readonly List<int> _compiledPassOrder;
private readonly List<RenderGraphBarrier> _barriers;
private bool _isRecording;
private bool _isCompiled;
private bool _isExecuted;
private bool _disposed;
public RenderGraph(string name, RenderGraphDesc desc = default)
{
_name = name;
_resources = new(desc.InitialResourceCapacity > 0 ? desc.InitialResourceCapacity : 256);
_passes = new(desc.InitialPassCapacity > 0 ? desc.InitialPassCapacity : 64);
_compiledPassOrder = new();
_barriers = new();
}
public string Name => _name;
public bool IsRecording => _isRecording;
public bool IsCompiled => _isCompiled;
/// <summary>
/// Begin recording render passes
/// </summary>
public void BeginRecord()
{
if (_isRecording)
throw new InvalidOperationException("Render graph is already recording");
if (_isCompiled)
throw new InvalidOperationException("Cannot record on a compiled render graph");
_isRecording = true;
_isExecuted = false;
// Clear previous frame data
foreach (var resource in _resources)
{
if (resource.Lifetime == ResourceLifetime.Transient)
{
resource.ReleaseResource();
}
}
_resources.RemoveAll(r => r.Lifetime == ResourceLifetime.Transient);
_passes.Clear();
_compiledPassOrder.Clear();
_barriers.Clear();
}
/// <summary>
/// End recording render passes
/// </summary>
public void EndRecord()
{
if (!_isRecording)
throw new InvalidOperationException("Render graph is not recording");
_isRecording = false;
}
/// <summary>
/// Create a new render pass
/// </summary>
public RenderPassCreator<TPassData> CreatePass<TPassData>(string passName)
where TPassData : struct
{
if (!_isRecording)
throw new InvalidOperationException("Cannot create pass when not recording");
return new RenderPassCreator<TPassData>(this, passName);
}
/// <summary>
/// Create a transient texture resource
/// </summary>
public RGTextureHandle CreateTexture(string name, ResourceLifetime lifetime, TextureDescription description)
{
var texture = new RenderGraphTexture(_resources.Count, name, lifetime, description);
_resources.Add(texture);
return new RGTextureHandle(texture.Id, this);
}
/// <summary>
/// Create a transient buffer resource
/// </summary>
public RGBufferHandle CreateBuffer(string name, ResourceLifetime lifetime, BufferDescription description)
{
var buffer = new RenderGraphBuffer(_resources.Count, name, lifetime, description);
_resources.Add(buffer);
return new RGBufferHandle(buffer.Id, this);
}
/// <summary>
/// Import an external texture (e.g., from previous frame, swap chain)
/// </summary>
public RGTextureHandle ImportTexture(string name, TextureHandle externalHandle, TextureDescription description)
{
var texture = new RenderGraphTexture(_resources.Count, name, externalHandle, description);
_resources.Add(texture);
return new RGTextureHandle(texture.Id, this);
}
/// <summary>
/// Import an external buffer (e.g., from previous frame)
/// </summary>
public RGBufferHandle ImportBuffer(string name, BufferHandle externalHandle, BufferDescription description)
{
var buffer = new RenderGraphBuffer(_resources.Count, name, externalHandle, description);
_resources.Add(buffer);
return new RGBufferHandle(buffer.Id, this);
}
/// <summary>
/// Export a resource for use in the next frame (for history buffers)
/// </summary>
public TextureHandle ExportTexture(RGTextureHandle handle)
{
if (!handle.IsValid || handle._resourceId >= _resources.Count)
throw new ArgumentException("Invalid texture handle", nameof(handle));
var texture = (RenderGraphTexture)_resources[handle._resourceId];
texture.Lifetime = ResourceLifetime.Persistent;
return texture.Handle;
}
/// <summary>
/// Export a buffer for use in the next frame
/// </summary>
public BufferHandle ExportBuffer(RGBufferHandle handle)
{
if (!handle.IsValid || handle._resourceId >= _resources.Count)
throw new ArgumentException("Invalid buffer handle", nameof(handle));
var buffer = (RenderGraphBuffer)_resources[handle._resourceId];
buffer.Lifetime = ResourceLifetime.Persistent;
return buffer.Handle;
}
public RenderGraphTexture GetTextureResource(RGTextureHandle handle)
{
if (!handle.IsValid || handle._resourceId >= _resources.Count)
throw new ArgumentException("Invalid texture handle", nameof(handle));
return (RenderGraphTexture)_resources[handle._resourceId];
}
public RenderGraphBuffer GetBufferResource(RGBufferHandle handle)
{
if (!handle.IsValid || handle._resourceId >= _resources.Count)
throw new ArgumentException("Invalid buffer handle", nameof(handle));
return (RenderGraphBuffer)_resources[handle._resourceId];
}
/// <summary>
/// Internal method to add a pass to the render graph
/// </summary>
internal void AddPass(RenderPass pass)
{
pass.Index = _passes.Count;
_passes.Add(pass);
}
/// <summary>
/// Internal method to update resource lifetime during pass setup
/// </summary>
internal void UpdateResourceLifetime(int resourceId, int passIndex)
{
if (resourceId >= _resources.Count)
return;
var resource = _resources[resourceId];
if (resource.FirstPassIndex == -1)
resource.FirstPassIndex = passIndex;
resource.LastPassIndex = Math.Max(resource.LastPassIndex, passIndex);
}
/// <summary>
/// Compile the render graph - performs dependency analysis, topological sort, and resource lifetime analysis
/// </summary>
public void Compile()
{
if (_isRecording)
throw new InvalidOperationException("Cannot compile while recording");
if (_isCompiled)
throw new InvalidOperationException("Render graph is already compiled");
// Setup all passes to gather resource dependencies
SetupPasses();
// Build dependency graph and perform topological sort
BuildDependencyGraph();
TopologicalSort();
// Analyze resource lifetimes and generate barriers
AnalyzeResourceLifetimes();
GenerateBarriers();
_isCompiled = true;
}
/// <summary>
/// Execute the compiled render graph
/// </summary>
public void Execute()
{
if (!_isCompiled)
throw new InvalidOperationException("Render graph must be compiled before execution");
if (_isExecuted)
throw new InvalidOperationException("Render graph has already been executed");
// Execute passes in topological order
foreach (var passIndex in _compiledPassOrder)
{
var pass = _passes[passIndex];
var context = new RenderPassContext(passIndex);
CreateTransientResources(passIndex);
ApplyBarriersForPass(passIndex);
// Execute the pass
pass.Execute(context);
}
_isExecuted = true;
}
private void SetupPasses()
{
for (var i = 0; i < _passes.Count; i++)
{
var pass = _passes[i];
var builder = new RenderPassBuilder(this, i);
pass.Setup(builder);
}
}
private void BuildDependencyGraph()
{
// Build dependencies based on resource usage
foreach (var pass in _passes)
{
var writeResources = new HashSet<int>();
var readResources = new HashSet<int>();
// Categorize resource accesses
foreach (var access in pass.ResourceAccesses)
{
switch (access.accessType)
{
case ResourceAccessType.Write:
writeResources.Add(access.resourceId);
break;
case ResourceAccessType.Read:
readResources.Add(access.resourceId);
break;
case ResourceAccessType.ReadWrite:
writeResources.Add(access.resourceId);
readResources.Add(access.resourceId);
break;
}
}
// Add dependencies based on Write-After-Read, Read-After-Write, Write-After-Write
for (var otherPassIndex = 0; otherPassIndex < pass.Index; otherPassIndex++)
{
var otherPass = _passes[otherPassIndex];
var hasDependency = false;
foreach (var otherAccess in otherPass.ResourceAccesses)
{
// WAR, RAW, WAW dependencies
if ((writeResources.Contains(otherAccess.resourceId) && otherAccess.accessType == ResourceAccessType.Read) ||
(readResources.Contains(otherAccess.resourceId) && otherAccess.accessType != ResourceAccessType.Read) ||
(writeResources.Contains(otherAccess.resourceId) && otherAccess.accessType != ResourceAccessType.Read))
{
hasDependency = true;
break;
}
}
if (hasDependency && !pass.Dependencies.Contains(otherPassIndex))
{
pass.Dependencies.Add(otherPassIndex);
}
}
}
}
private void TopologicalSort()
{
_compiledPassOrder.Clear();
var visited = new bool[_passes.Count];
var inDegree = new int[_passes.Count];
// Calculate in-degrees
foreach (var pass in _passes)
{
foreach (var dependency in pass.Dependencies)
{
inDegree[pass.Index]++;
}
}
// Kahn's algorithm for topological sorting
var queue = new Queue<int>();
for (var i = 0; i < _passes.Count; i++)
{
if (inDegree[i] == 0)
{
queue.Enqueue(i);
}
}
while (queue.Count > 0)
{
var passIndex = queue.Dequeue();
_compiledPassOrder.Add(passIndex);
var currentPass = _passes[passIndex];
foreach (var dependentPassIndex in _passes.Where(p => p.Dependencies.Contains(passIndex)).Select(p => p.Index))
{
inDegree[dependentPassIndex]--;
if (inDegree[dependentPassIndex] == 0)
{
queue.Enqueue(dependentPassIndex);
}
}
}
if (_compiledPassOrder.Count != _passes.Count)
{
throw new InvalidOperationException("Circular dependency detected in render graph");
}
}
private void AnalyzeResourceLifetimes()
{
// Resource lifetimes are already tracked during pass setup
// Additional analysis can be added here if needed
}
private void GenerateBarriers()
{
_barriers.Clear();
// TODO: Implement barrier generation based on resource state transitions
// This would analyze the resource usage patterns and generate appropriate D3D12 barriers
}
private void CreateTransientResources(int passIndex)
{
var pass = _passes[passIndex];
foreach (var access in pass.ResourceAccesses)
{
var resource = _resources[access.resourceId];
if (!resource.IsCreated)
{
resource.CreateResource();
}
}
}
private void ApplyBarriersForPass(int passIndex)
{
// TODO: Apply the generated barriers for the given pass
// This would involve creating D3D12 resource barriers and executing them on the command list
}
public void Dispose()
{
if (_disposed)
return;
foreach (var resource in _resources)
{
resource.ReleaseResource();
}
_resources.Clear();
_passes.Clear();
_compiledPassOrder.Clear();
_barriers.Clear();
_disposed = true;
}
}

View File

@@ -0,0 +1,121 @@
using Win32.Graphics.Direct3D12;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Handle for render graph texture resource
/// </summary>
public readonly struct RGTextureHandle : IEquatable<RGTextureHandle>
{
internal readonly RenderGraph? _renderGraph;
internal readonly int _resourceId;
internal RGTextureHandle(int resourceId, RenderGraph renderGraph)
{
_resourceId = resourceId;
_renderGraph = renderGraph;
}
public bool IsValid => _resourceId >= 0 && _renderGraph != null;
public bool Equals(RGTextureHandle other)
{
return _resourceId == other._resourceId && ReferenceEquals(_renderGraph, other._renderGraph);
}
public override bool Equals(object? obj)
{
return obj is RGTextureHandle other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(_resourceId, _renderGraph);
}
public static bool operator ==(RGTextureHandle left, RGTextureHandle right)
{
return left.Equals(right);
}
public static bool operator !=(RGTextureHandle left, RGTextureHandle right)
{
return !(left == right);
}
}
/// <summary>
/// Handle for render graph buffer resource
/// </summary>
public readonly struct RGBufferHandle : IEquatable<RGBufferHandle>
{
internal readonly RenderGraph? _renderGraph;
internal readonly int _resourceId;
internal RGBufferHandle(int resourceId, RenderGraph renderGraph)
{
_resourceId = resourceId;
_renderGraph = renderGraph;
}
public bool IsValid => _resourceId >= 0 && _renderGraph != null;
public bool Equals(RGBufferHandle other)
{
return _resourceId == other._resourceId && ReferenceEquals(_renderGraph, other._renderGraph);
}
public override bool Equals(object? obj)
{
return obj is RGBufferHandle other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(_resourceId, _renderGraph);
}
public static bool operator ==(RGBufferHandle left, RGBufferHandle right)
{
return left.Equals(right);
}
public static bool operator !=(RGBufferHandle left, RGBufferHandle right)
{
return !(left == right);
}
}
/// <summary>
/// Resource access information for dependency tracking
/// </summary>
internal readonly struct ResourceAccess
{
public readonly int resourceId;
public readonly int passIndex;
public readonly ResourceAccessType accessType;
public ResourceAccess(int resourceId, ResourceAccessType accessType, int passIndex)
{
this.resourceId = resourceId;
this.accessType = accessType;
this.passIndex = passIndex;
}
}
/// <summary>
/// Represents a barrier to be executed for resource state transitions
/// </summary>
internal readonly struct RenderGraphBarrier
{
public readonly int resourceId;
public readonly ResourceStates stateBefore;
public readonly ResourceStates stateAfter;
public RenderGraphBarrier(int resourceId, ResourceStates stateBefore, ResourceStates stateAfter)
{
this.resourceId = resourceId;
this.stateBefore = stateBefore;
this.stateAfter = stateAfter;
}
}

View File

@@ -0,0 +1,289 @@
using Ghost.Graphics.Data;
using Win32.Graphics.Direct3D12;
using Win32.Graphics.Dxgi.Common;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Represents different resource access types in the render graph
/// </summary>
public enum ResourceAccessType
{
Read,
Write,
ReadWrite
}
/// <summary>
/// Resource lifetime within the render graph
/// </summary>
public enum ResourceLifetime
{
/// <summary>
/// Resource is created and destroyed within a single frame
/// </summary>
Transient,
/// <summary>
/// Resource is imported from external source (e.g., history buffers, backbuffer)
/// </summary>
External,
/// <summary>
/// Resource persists across multiple frames (exported for next frame)
/// </summary>
Persistent
}
/// <summary>
/// Base class for render graph resources
/// </summary>
public abstract class RenderGraphResource
{
public int FirstPassIndex
{
get; set;
}
internal int LastPassIndex
{
get; set;
}
public int Id
{
get;
}
public string Name
{
get; set;
}
public ResourceLifetime Lifetime
{
get; internal set;
}
public bool IsImported
{
get; init;
}
public bool IsCreated
{
get; protected set;
}
protected RenderGraphResource(int id, string name, ResourceLifetime lifetime)
{
Id = id;
Name = name;
Lifetime = lifetime;
FirstPassIndex = -1;
LastPassIndex = -1;
}
internal abstract void CreateResource();
internal abstract void ReleaseResource();
}
/// <summary>
/// Represents a texture resource in the render graph
/// </summary>
public sealed class RenderGraphTexture : RenderGraphResource
{
internal TextureHandle Handle
{
get; set;
}
internal TextureDescription Description
{
get; private set;
}
public RenderGraphTexture(int id, string name, ResourceLifetime lifetime, TextureDescription description)
: base(id, name, lifetime)
{
Description = description;
}
public RenderGraphTexture(int id, string name, TextureHandle handle, TextureDescription description)
: base(id, name, ResourceLifetime.External)
{
Handle = handle;
Description = description;
IsImported = true;
IsCreated = true;
}
internal override void CreateResource()
{
if (IsCreated || IsImported)
return;
var allocFlags = Lifetime == ResourceLifetime.Transient
? Win32.Graphics.D3D12MemoryAllocator.AllocationFlags.CanAlias
: Win32.Graphics.D3D12MemoryAllocator.AllocationFlags.None;
Handle = GraphicsPipeline.ResourceAllocator.CreateTexture2D(
Description.Width,
Description.Height,
Description.MipLevels,
Description.Format,
Description.Flags,
allocFlags,
Description.InitialState);
IsCreated = true;
}
internal override void ReleaseResource()
{
if (!IsCreated || IsImported)
return;
Handle.Dispose();
IsCreated = false;
}
}
/// <summary>
/// Represents a buffer resource in the render graph
/// </summary>
public sealed class RenderGraphBuffer : RenderGraphResource
{
internal BufferHandle Handle
{
get; set;
}
internal BufferDescription Description
{
get; private set;
}
public RenderGraphBuffer(int id, string name, ResourceLifetime lifetime, BufferDescription description)
: base(id, name, lifetime)
{
Description = description;
}
public RenderGraphBuffer(int id, string name, BufferHandle handle, BufferDescription description)
: base(id, name, ResourceLifetime.External)
{
Handle = handle;
Description = description;
IsImported = true;
IsCreated = true;
}
internal override void CreateResource()
{
if (IsCreated || IsImported)
return;
var allocFlags = Lifetime == ResourceLifetime.Transient
? Win32.Graphics.D3D12MemoryAllocator.AllocationFlags.CanAlias
: Win32.Graphics.D3D12MemoryAllocator.AllocationFlags.None;
Handle = GraphicsPipeline.ResourceAllocator.CreateBuffer(
Description.SizeInBytes,
Description.HeapType,
Description.Flags,
allocFlags,
Description.InitialState);
IsCreated = true;
}
internal override void ReleaseResource()
{
if (!IsCreated || IsImported)
return;
Handle.Dispose();
IsCreated = false;
}
}
/// <summary>
/// Texture description for render graph texture creation
/// </summary>
public readonly struct TextureDescription
{
public readonly uint Width
{
get;
}
public readonly uint Height
{
get;
}
public readonly ushort MipLevels
{
get;
}
public readonly Format Format
{
get;
}
public readonly ResourceFlags Flags
{
get;
}
public readonly ResourceStates InitialState
{
get;
}
public TextureDescription(uint width, uint height, ushort mipLevels = 1,
Format format = Format.R8G8B8A8Unorm, ResourceFlags flags = ResourceFlags.None,
ResourceStates initialState = ResourceStates.Common)
{
Width = width;
Height = height;
MipLevels = mipLevels;
Format = format;
Flags = flags;
InitialState = initialState;
}
}
/// <summary>
/// Buffer description for render graph buffer creation
/// </summary>
public readonly struct BufferDescription
{
public readonly uint SizeInBytes
{
get;
}
public readonly HeapType HeapType
{
get;
}
public readonly ResourceFlags Flags
{
get;
}
public readonly ResourceStates InitialState
{
get;
}
public BufferDescription(uint sizeInBytes, HeapType heapType = HeapType.Default,
ResourceFlags flags = ResourceFlags.None, ResourceStates initialState = ResourceStates.Common)
{
SizeInBytes = sizeInBytes;
HeapType = heapType;
Flags = flags;
InitialState = initialState;
}
}

View File

@@ -0,0 +1,125 @@
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Delegate for pass setup function
/// </summary>
public delegate void PassSetupFunction<TPassData>(ref TPassData data, RenderPassBuilder builder) where TPassData : struct;
/// <summary>
/// Delegate for pass execution function
/// </summary>
public delegate void PassExecuteFunction<TPassData>(ref TPassData data, RenderPassContext context) where TPassData : struct;
/// <summary>
/// Base class for render passes in the render graph
/// </summary>
internal abstract class RenderPass
{
public string Name
{
get;
}
public int Index
{
get; internal set;
}
public List<ResourceAccess> ResourceAccesses
{
get;
}
public List<int> Dependencies
{
get;
}
protected RenderPass(string name)
{
Name = name;
ResourceAccesses = new List<ResourceAccess>();
Dependencies = new List<int>();
}
public abstract void Setup(RenderPassBuilder builder);
public abstract void Execute(RenderPassContext context);
}
/// <summary>
/// Typed render pass implementation
/// </summary>
internal sealed class RenderPass<TPassData> : RenderPass
where TPassData : struct
{
private readonly PassSetupFunction<TPassData> _setupFunction;
private readonly PassExecuteFunction<TPassData> _executeFunction;
private TPassData _passData;
public RenderPass(string name, PassSetupFunction<TPassData> setupFunction, PassExecuteFunction<TPassData> executeFunction)
: base(name)
{
_setupFunction = setupFunction;
_executeFunction = executeFunction;
}
public override void Setup(RenderPassBuilder builder)
{
_setupFunction(ref _passData, builder);
ResourceAccesses.AddRange(builder.ResourceAccesses);
}
public override void Execute(RenderPassContext context)
{
_executeFunction(ref _passData, context);
}
public void SetPassData(TPassData passData)
{
_passData = passData;
}
}
/// <summary>
/// Builder for creating render passes
/// </summary>
public sealed class RenderPassCreator<TPassData>
where TPassData : struct
{
private readonly RenderGraph _renderGraph;
private readonly string _passName;
private PassSetupFunction<TPassData>? _setupFunction;
private PassExecuteFunction<TPassData>? _executeFunction;
internal RenderPassCreator(RenderGraph renderGraph, string passName)
{
_renderGraph = renderGraph;
_passName = passName;
}
/// <summary>
/// Set the setup function for the render pass
/// </summary>
public RenderPassCreator<TPassData> Setup(PassSetupFunction<TPassData> setupFunction)
{
_setupFunction = setupFunction;
return this;
}
/// <summary>
/// Set the render function for the render pass
/// </summary>
public RenderPassCreator<TPassData> SetRenderFunc(PassExecuteFunction<TPassData> executeFunction)
{
_executeFunction = executeFunction;
return this;
}
public void Compile()
{
if (_setupFunction == null)
throw new InvalidOperationException($"Setup function not set for pass '{_passName}'");
if (_executeFunction == null)
throw new InvalidOperationException($"Execute function not set for pass '{_passName}'");
var pass = new RenderPass<TPassData>(_passName, _setupFunction, _executeFunction);
_renderGraph.AddPass(pass);
}
}

View File

@@ -0,0 +1,131 @@
using Ghost.Graphics.Data;
using Win32.Graphics.Direct3D12;
namespace Ghost.Graphics.RenderGraphModule;
/// <summary>
/// Context passed to render pass execution functions
/// </summary>
public readonly struct RenderPassContext
{
internal readonly int PassIndex;
// TODO: Add command list and other rendering context when available
internal RenderPassContext(int passIndex)
{
PassIndex = passIndex;
}
}
/// <summary>
/// Builder for configuring render pass resource dependencies
/// </summary>
public sealed class RenderPassBuilder
{
private readonly RenderGraph _renderGraph;
private readonly int _passIndex;
private readonly List<ResourceAccess> _resourceAccesses;
internal RenderPassBuilder(RenderGraph renderGraph, int passIndex)
{
_renderGraph = renderGraph;
_passIndex = passIndex;
_resourceAccesses = new List<ResourceAccess>();
}
internal IReadOnlyList<ResourceAccess> ResourceAccesses => _resourceAccesses;
/// <summary>
/// Declare a texture read dependency
/// </summary>
public RGTextureHandle ReadTexture(RGTextureHandle handle)
{
if (!handle.IsValid)
throw new ArgumentException("Invalid texture handle", nameof(handle));
_resourceAccesses.Add(new ResourceAccess(handle._resourceId, ResourceAccessType.Read, _passIndex));
_renderGraph.UpdateResourceLifetime(handle._resourceId, _passIndex);
return handle;
}
/// <summary>
/// Declare a texture write dependency
/// </summary>
public RGTextureHandle WriteTexture(RGTextureHandle handle)
{
if (!handle.IsValid)
throw new ArgumentException("Invalid texture handle", nameof(handle));
_resourceAccesses.Add(new ResourceAccess(handle._resourceId, ResourceAccessType.Write, _passIndex));
_renderGraph.UpdateResourceLifetime(handle._resourceId, _passIndex);
return handle;
}
/// <summary>
/// Declare a texture read-write dependency
/// </summary>
public RGTextureHandle ReadWriteTexture(RGTextureHandle handle)
{
if (!handle.IsValid)
throw new ArgumentException("Invalid texture handle", nameof(handle));
_resourceAccesses.Add(new ResourceAccess(handle._resourceId, ResourceAccessType.ReadWrite, _passIndex));
_renderGraph.UpdateResourceLifetime(handle._resourceId, _passIndex);
return handle;
}
/// <summary>
/// Declare a buffer read dependency
/// </summary>
public RGBufferHandle ReadBuffer(RGBufferHandle handle)
{
if (!handle.IsValid)
throw new ArgumentException("Invalid buffer handle", nameof(handle));
_resourceAccesses.Add(new ResourceAccess(handle._resourceId, ResourceAccessType.Read, _passIndex));
_renderGraph.UpdateResourceLifetime(handle._resourceId, _passIndex);
return handle;
}
/// <summary>
/// Declare a buffer write dependency
/// </summary>
public RGBufferHandle WriteBuffer(RGBufferHandle handle)
{
if (!handle.IsValid)
throw new ArgumentException("Invalid buffer handle", nameof(handle));
_resourceAccesses.Add(new ResourceAccess(handle._resourceId, ResourceAccessType.Write, _passIndex));
_renderGraph.UpdateResourceLifetime(handle._resourceId, _passIndex);
return handle;
}
/// <summary>
/// Declare a buffer read-write dependency
/// </summary>
public RGBufferHandle ReadWriteBuffer(RGBufferHandle handle)
{
if (!handle.IsValid)
throw new ArgumentException("Invalid buffer handle", nameof(handle));
_resourceAccesses.Add(new ResourceAccess(handle._resourceId, ResourceAccessType.ReadWrite, _passIndex));
_renderGraph.UpdateResourceLifetime(handle._resourceId, _passIndex);
return handle;
}
/// <summary>
/// Create a new transient texture within this pass
/// </summary>
public RGTextureHandle CreateTexture(string name, TextureDescription description)
{
return _renderGraph.CreateTexture(name, ResourceLifetime.Transient, description);
}
/// <summary>
/// Create a new transient buffer within this pass
/// </summary>
public RGBufferHandle CreateBuffer(string name, BufferDescription description)
{
return _renderGraph.CreateBuffer(name, ResourceLifetime.Transient, description);
}
}

View File

@@ -1,677 +0,0 @@
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;
/// <summary>
/// Modern bindless texture implementation using SM 6.6 with ResourceDescriptorHeap
/// and D3D12_ROOT_SIGNATURE_FLAG_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED
/// </summary>
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<ID3D12PipelineState> _pipelineState;
private ComPtr<ID3D12RootSignature> _rootSignature;
private ComPtr<ID3D12Resource> _constantBuffer;
private void* _constantBufferMappedData;
// Bindless texture resources
private ComPtr<ID3D12Resource>[] _textureResources;
private ComPtr<ID3D12DescriptorHeap> _bindlessHeap;
private ComPtr<ID3D12DescriptorHeap> _samplerHeap;
private CpuDescriptorHandle _bindlessHeapStart;
private GpuDescriptorHandle _bindlessHeapGpuStart;
private uint _descriptorSize;
// Texture upload resources
private ComPtr<ID3D12Resource>[] _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<IDxcCompiler3> compiler = default;
using ComPtr<IDxcUtils> utils = default;
// Create DXC compiler and utils
DxcCreateInstance(CLSID_DxcCompiler, __uuidof<IDxcCompiler3>(), compiler.GetVoidAddressOf());
DxcCreateInstance(CLSID_DxcUtils, __uuidof<IDxcUtils>(), utils.GetVoidAddressOf());
// Create source blob
using ComPtr<IDxcBlobEncoding> 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<IDxcResult> 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<IDxcResult>(), result.GetVoidAddressOf());
}
// Check compilation result
HResult hrStatus;
result.Get()->GetStatus(&hrStatus);
if (hrStatus.Failure)
{
// Get error messages
using ComPtr<IDxcBlobEncoding> 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<IDxcBlob> 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<ID3DBlob> signature = default;
using ComPtr<ID3DBlob> error = default;
D3D12SerializeVersionedRootSignature(&versionedDesc, signature.GetAddressOf(), error.GetAddressOf());
device->CreateRootSignature(0, signature.Get()->GetBufferPointer(), signature.Get()->GetBufferSize(), __uuidof<ID3D12RootSignature>(), _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<ID3D12PipelineState>(), _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<ID3D12Resource>(),
_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<ID3D12Resource>[textureCount];
_uploadBuffers = new ComPtr<ID3D12Resource>[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<ID3D12DescriptorHeap>(), _bindlessHeap.GetVoidAddressOf());
// Create sampler heap
var samplerHeapDesc = new DescriptorHeapDescription
{
Type = DescriptorHeapType.Sampler,
NumDescriptors = 1,
Flags = DescriptorHeapFlags.ShaderVisible
};
device->CreateDescriptorHeap(&samplerHeapDesc, __uuidof<ID3D12DescriptorHeap>(), _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<ID3D12Resource>(),
_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<ID3D12Resource>(),
_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<byte>(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();
}
}

View File

@@ -1,32 +1,39 @@
using Ghost.Graphics.Contracts;
using Ghost.Graphics.Contracts;
using Ghost.Graphics.D3D12;
using Ghost.Graphics.Data;
using Ghost.Graphics.Shading;
using Ghost.Graphics.Utilities;
using StbImageSharp;
using Win32;
using Win32.Graphics.Direct3D;
using Win32.Graphics.Direct3D12;
using Win32.Graphics.Dxgi.Common;
using System.Numerics;
namespace Ghost.Graphics.RenderPasses;
/// <summary>
/// Simplified bindless mesh render pass using high-level bindless APIs with fully bindless vertex/index buffer access
/// </summary>
internal unsafe class MeshRenderPass : IRenderPass
{
private const string _HLSL_SOURCE = @"
cbuffer ConstantBuffer : register(b0)
{
float4 _Color;
uint _TextureIndex1;
uint _TextureIndex2;
uint _TextureIndex3;
uint _TextureIndex4;
uint _VertexBufferIndex;
uint _IndexBufferIndex;
};
Texture2D _MainTex : register(t0);
// SM 6.6 approach - direct access to global descriptor heap
SamplerState _MainSampler : register(s0);
struct VertexInput
struct Vertex
{
float4 position : POSITION;
float4 color : COLOR;
float4 uv : TEXCOORD0;
float4 position;
float4 normal;
float4 tangent;
float4 color;
float4 uv;
};
struct PixelInput
@@ -36,237 +43,105 @@ struct PixelInput
float4 uv : TEXCOORD0;
};
PixelInput VSMain(VertexInput input)
// Bindless vertex shader that fetches vertex data from bindless buffers
PixelInput VSMain(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID)
{
// Get bindless buffers
ByteAddressBuffer vertexBuffer = ResourceDescriptorHeap[_VertexBufferIndex];
ByteAddressBuffer indexBuffer = ResourceDescriptorHeap[_IndexBufferIndex];
// For fully bindless rendering, we use instanced drawing where:
// - Each instance represents a triangle (instanceId = triangle index)
// - vertexId goes from 0 to 2 (the 3 vertices of the triangle)
// Calculate the index into the index buffer
uint indexOffset = (instanceId * 3 + vertexId) * 4; // 4 bytes per index (uint32)
uint vertexIndex = indexBuffer.Load(indexOffset);
// Calculate the offset into the vertex buffer
uint vertexOffset = vertexIndex * 80; // 80 bytes per vertex (5 * float4)
// Load vertex data from bindless vertex buffer
Vertex vertex;
vertex.position = asfloat(vertexBuffer.Load4(vertexOffset + 0));
vertex.normal = asfloat(vertexBuffer.Load4(vertexOffset + 16));
vertex.tangent = asfloat(vertexBuffer.Load4(vertexOffset + 32));
vertex.color = asfloat(vertexBuffer.Load4(vertexOffset + 48));
vertex.uv = asfloat(vertexBuffer.Load4(vertexOffset + 64));
// Output transformed vertex
PixelInput output;
output.position = input.position;
output.color = input.color;
output.uv = input.uv;
output.position = vertex.position;
output.color = vertex.color;
output.uv = vertex.uv;
return output;
}
float4 PSMain(PixelInput input) : SV_TARGET
{
return _MainTex.Sample(_MainSampler, input.uv.xy);
// SM 6.6 Modern Bindless Approach:
// ResourceDescriptorHeap[index] directly accesses any texture in the heap
Texture2D tex1 = ResourceDescriptorHeap[_TextureIndex1];
Texture2D tex2 = ResourceDescriptorHeap[_TextureIndex2];
Texture2D tex3 = ResourceDescriptorHeap[_TextureIndex3];
Texture2D tex4 = ResourceDescriptorHeap[_TextureIndex4];
// Sample the textures
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;
}
";
// High-level bindless objects
private Mesh? _mesh;
private Shader? _shader;
private Material? _material;
private Texture2D[]? _textures;
// Direct D3D12 resources for texture
private ComPtr<ID3D12Resource> _textureResource;
private ComPtr<ID3D12DescriptorHeap> _srvHeap;
private CpuDescriptorHandle _srvHandle;
private GpuDescriptorHandle _srvGpuHandle;
private uint _srvDescriptorSize;
// Additional fields for deferred texture upload
private ComPtr<ID3D12Resource> _uploadBuffer;
private uint _textureWidth;
private uint _textureHeight;
private uint _texturePitch;
private bool _textureUploaded = 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"
];
public void Initialize(CommandList cmd)
{
_mesh = MeshBuilder.CreateCube(0.25f);
_mesh = MeshBuilder.CreateCube(0.75f);
_mesh.UploadMeshData();
_shader = new(_HLSL_SOURCE);
_material = new(_shader);
_shader = new Shader(_HLSL_SOURCE);
_material = new Material(_shader);
// Create direct D3D12 texture resources
CreateTextureDirectly();
_material.UploadMaterialData();
// Copy from upload buffer to texture
var srcLocation = new TextureCopyLocation(_uploadBuffer.Get(), new PlacedSubresourceFootprint
_textures = new Texture2D[_textureFiles.Length];
for (var i = 0; i < _textureFiles.Length; i++)
{
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);
}
private void CreateTextureDirectly()
{
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<ID3D12Resource>(),
_textureResource.GetVoidAddressOf()
);
// Create upload buffer
var uploadBufferSize = GetRequiredIntermediateSize(_textureResource.Get(), 0, 1);
var uploadHeapProps = new HeapProperties(HeapType.Upload);
var uploadBufferDesc = ResourceDescription.Buffer(uploadBufferSize);
ComPtr<ID3D12Resource> uploadBuffer = default;
device->CreateCommittedResource(
&uploadHeapProps,
HeapFlags.None,
&uploadBufferDesc,
ResourceStates.GenericRead,
null,
__uuidof<ID3D12Resource>(),
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<byte>(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);
_textures[i] = Texture2D.FromFile(_textureFiles[i]);
_textures[i].UploadTextureData();
}
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
_material.SetVector("_Color", new Vector4(1.0f, 1.0f, 1.0f, 1.0f));
for (var i = 0; i < _textures.Length; i++)
{
Type = DescriptorHeapType.CbvSrvUav,
NumDescriptors = 1,
Flags = DescriptorHeapFlags.ShaderVisible
};
var texture = _textures[i];
_material.SetTextureIndex($"_TextureIndex{i + 1}", texture);
}
device->CreateDescriptorHeap(&srvHeapDesc, __uuidof<ID3D12DescriptorHeap>(), _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;
_material.SetMeshBufferIndices(_mesh);
_material.UploadMaterialData();
}
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);
cmd.DrawMesh(_mesh!, _material!);
}
public void Dispose()
@@ -275,8 +150,12 @@ float4 PSMain(PixelInput input) : SV_TARGET
_shader?.Dispose();
_material?.Dispose();
_textureResource.Dispose();
_uploadBuffer.Dispose();
_srvHeap.Dispose();
if (_textures != null)
{
foreach (var texture in _textures)
{
texture?.Dispose();
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Ghost.Graphics.D3D12;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Runtime.CompilerServices;
@@ -13,19 +14,19 @@ internal struct CBufferCache : IDisposable
set;
}
public GraphicsResource GpuResource
public GraphicsBuffer GpuResource
{
get;
}
private readonly uint _alignedSize;
public unsafe CBufferCache(uint bufferSize)
internal unsafe CBufferCache(uint bufferSize)
{
_alignedSize = (bufferSize + 255u) & ~255u;
CpuData = new((int)_alignedSize, Allocator.Persistent);
GpuResource = GraphicsPipeline.ResourceAllocator.CreateUploadBuffer(_alignedSize);
GpuResource = GraphicsBuffer.Create(_alignedSize, GraphicsBuffer.Usage.Constant);
GpuResource.Name = "Material_CBufferCache";
UploadToGpu();
@@ -34,11 +35,12 @@ internal struct CBufferCache : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void UploadToGpu()
{
GpuResource.SetData(CpuData.AsSpan());
GpuResource.SetData(CpuData.AsSpan(), 0);
}
public readonly void Dispose()
{
CpuData.Dispose();
GpuResource.Dispose();
}
}

View File

@@ -1,13 +1,9 @@
using Ghost.Core;
using Ghost.Core;
using Ghost.Graphics.D3D12.Utilities;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Win32;
using Win32.Graphics.Direct3D;
using Win32.Graphics.Direct3D.Fxc;
using Win32.Graphics.Direct3D.Dxc;
using Win32.Graphics.Direct3D12;
using Win32.Graphics.Dxgi.Common;
@@ -72,10 +68,16 @@ internal readonly struct CBufferInfo
}
}
/// <summary>
/// Bindless shader implementation using SM 6.6 with ResourceDescriptorHeap
/// and D3D12_ROOT_SIGNATURE_FLAG_CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED
/// Enhanced to support both bindless and regular texture binding for hybrid materials
/// </summary>
public unsafe class Shader : IDisposable
{
private ComPtr<ID3D12PipelineState> _pipelineState;
private ComPtr<ID3D12RootSignature> _rootSignature;
private ComPtr<ID3D12DescriptorHeap> _samplerHeap;
private readonly byte[] _vertexShaderBytecode;
private readonly byte[] _pixelShaderBytecode;
@@ -83,215 +85,338 @@ public unsafe class Shader : IDisposable
private readonly List<CBufferInfo> _constantBuffers = new();
private readonly List<PropertyInfo> _properties = new();
private readonly Dictionary<string, int> _propertyNameToIdMap = new();
private readonly List<TextureInfo> _textures = new();
private readonly Dictionary<string, int> _textureNameToIdMap = new();
private readonly List<TextureInfo> _regularTextures = new(); // Add regular texture support
private bool _disposed;
internal ConstPtr<ID3D12PipelineState> PipelineState => new(_pipelineState.Get());
internal ConstPtr<ID3D12RootSignature> RootSignature => new(_rootSignature.Get());
internal ConstPtr<ID3D12DescriptorHeap> SamplerHeap => new(_samplerHeap.Get());
internal IReadOnlyList<CBufferInfo> ConstantBuffers => _constantBuffers;
internal IReadOnlyList<PropertyInfo> Properties => _properties;
internal IReadOnlyList<TextureInfo> Textures => _textures;
internal IReadOnlyList<TextureInfo> RegularTextures => _regularTextures; // Expose regular textures
//public Shader(string shaderPath)
//{
//}
internal Shader(string shaderCode)
public Shader(string shaderCode)
{
_vertexShaderBytecode = CompileShader(Encoding.UTF8.GetBytes(shaderCode), "VSMain", "vs_5_0");
_pixelShaderBytecode = CompileShader(Encoding.UTF8.GetBytes(shaderCode), "PSMain", "ps_5_0");
var (vsBytecode, vsReflection) = CompileShaderDXC(shaderCode, "VSMain", "vs_6_6");
var (psBytecode, psReflection) = CompileShaderDXC(shaderCode, "PSMain", "ps_6_6");
PerformReflection(_vertexShaderBytecode);
PerformReflection(_pixelShaderBytecode);
_vertexShaderBytecode = vsBytecode;
_pixelShaderBytecode = psBytecode;
CreateRootSignature();
CreatePipelineStateObject();
PerformDXCReflection(vsReflection);
PerformDXCReflection(psReflection);
CreateBindlessRootSignature();
CreatePipelineState();
CreateSamplerHeap();
}
~Shader()
private (byte[] bytecode, ComPtr<IDxcBlob> reflection) CompileShaderDXC(string source, string entryPoint, string profile)
{
Dispose();
using ComPtr<IDxcCompiler3> compiler = default;
using ComPtr<IDxcUtils> utils = default;
// Create DXC compiler and utils
DxcCreateInstance(CLSID_DxcCompiler, __uuidof<IDxcCompiler3>(), compiler.GetVoidAddressOf());
DxcCreateInstance(CLSID_DxcUtils, __uuidof<IDxcUtils>(), utils.GetVoidAddressOf());
// Create source blob
using ComPtr<IDxcBlobEncoding> 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 - NOTE: NO -Qstrip_reflect to keep reflection data
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 but KEEP reflection
};
// 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<IDxcResult> 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<IDxcResult>(), result.GetVoidAddressOf());
}
// Check compilation result
HResult hrStatus;
result.Get()->GetStatus(&hrStatus);
if (hrStatus.Failure)
{
// Get error messages
using ComPtr<IDxcBlobEncoding> 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<IDxcBlob> bytecodeBlob = default;
result.Get()->GetResult(bytecodeBlob.GetAddressOf());
if (bytecodeBlob.Get() == null)
{
throw new Exception("DXC compilation succeeded but no bytecode was produced");
}
// Get reflection data using DXC API
using ComPtr<IDxcBlob> reflectionBlob = default;
result.Get()->GetOutput(DxcOutKind.Reflection, __uuidof<IDxcBlob>(), reflectionBlob.GetVoidAddressOf(), null);
if (reflectionBlob.Get() == null)
{
throw new Exception("DXC compilation succeeded but no reflection data 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 both bytecode and reflection blob (move ownership)
return (bytecode, reflectionBlob.Move());
}
finally
{
// Free allocated wide strings
for (var i = 0; i < argPointers.Length; i++)
{
Marshal.FreeHGlobal(argPointers[i]);
}
}
}
/// <summary>
/// Compiles HLSL source code from a string into shader bytecode.
/// </summary>
/// <param name="sourceCode">The string containing the HLSL code.</param>
/// <param name="entryPoint">The name of the shader entry point function (e.g., "VSMain").</param>
/// <param name="shaderProfile">The shader model to target (e.g., "vs_5_0", "ps_5_0").</param>
/// <returns>A byte array containing the compiled shader bytecode.</returns>
/// <exception cref="Exception">Thrown if shader compilation fails.</exception>
public static unsafe byte[] CompileShader(ReadOnlySpan<byte> sourceCodeBytes, string entryPoint, string shaderProfile)
private void CreateBindlessRootSignature()
{
using ComPtr<ID3DBlob> bytecodeBlob = default;
using ComPtr<ID3DBlob> errorBlob = default;
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
var entryPointBytes = Encoding.UTF8.GetBytes(entryPoint);
var shaderProfileBytes = Encoding.UTF8.GetBytes(shaderProfile);
ThrowIfFailed(D3DCompile(
sourceCodeBytes,
entryPointBytes.AsSpan(),
shaderProfileBytes.AsSpan(),
CompileFlags.EnableStrictness | CompileFlags.Debug,
bytecodeBlob.GetAddressOf(),
errorBlob.GetAddressOf()
));
var bytecode = new byte[bytecodeBlob.Get()->GetBufferSize()];
Unsafe.CopyBlock(bytecode.AsSpan().GetPointer(), bytecodeBlob.Get()->GetBufferPointer(), (uint)bytecode.Length);
return bytecode;
}
private void CreateRootSignature()
{
// 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];
// Calculate total root parameters: CBVs + Regular texture descriptor table + Sampler table
var totalRootParams = _constantBuffers.Count + (_regularTextures.Count > 0 ? 1 : 0) + 1; // +1 for sampler
var rootParameters = new RootParameter1[totalRootParams];
var parameterIndex = 0;
// Add CBV root parameters
for (var i = 0; i < _constantBuffers.Count; i++)
foreach (var cbufferInfo in _constantBuffers)
{
var cbufferInfo = _constantBuffers[i];
Debug.Assert(i == cbufferInfo.RegisterSlot);
var rootParameter = new RootParameter1
rootParameters[parameterIndex++] = new RootParameter1
{
ParameterType = RootParameterType.Cbv,
ShaderVisibility = ShaderVisibility.All,
Descriptor = new RootDescriptor1(cbufferInfo.RegisterSlot, 0),
};
rootParameters[parameterIndex++] = rootParameter;
}
// Add descriptor table for SRVs if we have textures
if (_textures.Count > 0)
// Add regular texture descriptor table if we have regular textures
if (_regularTextures.Count > 0)
{
var ranges = new DescriptorRange1[1];
ranges[0] = new DescriptorRange1
var textureRanges = new DescriptorRange1[1];
textureRanges[0] = new DescriptorRange1
{
RangeType = DescriptorRangeType.Srv,
NumDescriptors = (uint)_textures.Count,
NumDescriptors = (uint)_regularTextures.Count,
BaseShaderRegister = 0, // Start from t0
RegisterSpace = 0,
Flags = DescriptorRangeFlags.None,
OffsetInDescriptorsFromTableStart = 0
};
fixed (DescriptorRange1* rangesPtr = ranges)
fixed (DescriptorRange1* textureRangesPtr = textureRanges)
{
var rootParameter = new RootParameter1
rootParameters[parameterIndex++] = new RootParameter1
{
ParameterType = RootParameterType.DescriptorTable,
ShaderVisibility = ShaderVisibility.All,
DescriptorTable = new RootDescriptorTable1(1, rangesPtr)
DescriptorTable = new RootDescriptorTable1(1, textureRangesPtr)
};
rootParameters[parameterIndex++] = rootParameter;
}
}
// Create static samplers for textures
var staticSamplers = new StaticSamplerDescription[_textures.Count];
for (var i = 0; i < _textures.Count; i++)
// Sampler descriptor table (still needed for samplers)
var samplerRanges = new DescriptorRange1[1];
samplerRanges[0] = new DescriptorRange1
{
staticSamplers[i] = new StaticSamplerDescription
RangeType = DescriptorRangeType.Sampler,
NumDescriptors = 1,
BaseShaderRegister = 0, // s0
RegisterSpace = 0,
Flags = DescriptorRangeFlags.None,
OffsetInDescriptorsFromTableStart = 0
};
fixed (DescriptorRange1* samplerRangesPtr = samplerRanges)
{
rootParameters[parameterIndex] = new RootParameter1
{
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
ParameterType = RootParameterType.DescriptorTable,
ShaderVisibility = ShaderVisibility.All,
DescriptorTable = new RootDescriptorTable1(1, samplerRangesPtr)
};
}
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)
// Create root signature with the modern flag
fixed (RootParameter1* rootParamsPtr = rootParameters)
{
Flags = RootSignatureFlags.AllowInputAssemblerInputLayout
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<ID3DBlob> signature = default;
using ComPtr<ID3DBlob> error = default;
D3D12SerializeVersionedRootSignature(&versionedDesc, signature.GetAddressOf(), error.GetAddressOf());
device->CreateRootSignature(0, signature.Get()->GetBufferPointer(), signature.Get()->GetBufferSize(), __uuidof<ID3D12RootSignature>(), _rootSignature.GetVoidAddressOf());
}
}
private void CreatePipelineState()
{
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
fixed (byte* vsPtr = _vertexShaderBytecode)
fixed (byte* psPtr = _pixelShaderBytecode)
{
var psoDesc = new GraphicsPipelineStateDescription
{
pRootSignature = _rootSignature.Get(),
VS = new ShaderBytecode(vsPtr, (nuint)_vertexShaderBytecode.Length),
PS = new ShaderBytecode(psPtr, (nuint)_pixelShaderBytecode.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,
};
psoDesc.RTVFormats[0] = D3D12PipelineResource.SWAP_CHAIN_BACK_BUFFER_FORMAT;
device->CreateGraphicsPipelineState(&psoDesc, __uuidof<ID3D12PipelineState>(), _pipelineState.GetVoidAddressOf());
}
}
private void CreateSamplerHeap()
{
var device = GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr;
// Create sampler heap
var samplerHeapDesc = new DescriptorHeapDescription
{
Type = DescriptorHeapType.Sampler,
NumDescriptors = 1,
Flags = DescriptorHeapFlags.ShaderVisible
};
using ComPtr<ID3DBlob> signature = default;
using ComPtr<ID3DBlob> error = default;
device->CreateDescriptorHeap(&samplerHeapDesc, __uuidof<ID3D12DescriptorHeap>(), _samplerHeap.GetVoidAddressOf());
ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, RootSignatureVersion.V1_0, signature.GetAddressOf(), error.GetAddressOf()));
// Create default 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
};
GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateRootSignature(0, signature.Get()->GetBufferPointer(), signature.Get()->GetBufferSize(), __uuidof<ID3D12RootSignature>(), _rootSignature.GetVoidAddressOf());
// 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);
}
private void CreatePipelineStateObject()
private unsafe void PerformDXCReflection(ComPtr<IDxcBlob> reflectionBlob)
{
try
// Create DXC utils to parse reflection data
using ComPtr<IDxcUtils> utils = default;
DxcCreateInstance(CLSID_DxcUtils, __uuidof<IDxcUtils>(), utils.GetVoidAddressOf());
// Create reflection interface from blob
var reflectionData = new DxcBuffer
{
fixed (byte* vsPtr = _vertexShaderBytecode)
fixed (byte* psPtr = _pixelShaderBytecode)
{
var psoDesc = new GraphicsPipelineStateDescription
{
pRootSignature = _rootSignature.Get(),
VS = new ShaderBytecode(vsPtr, (nuint)_vertexShaderBytecode.Length),
PS = new ShaderBytecode(psPtr, (nuint)_pixelShaderBytecode.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,
};
Ptr = reflectionBlob.Get()->GetBufferPointer(),
Size = reflectionBlob.Get()->GetBufferSize(),
Encoding = DXC_CP_ACP
};
psoDesc.RTVFormats[0] = D3D12PipelineResource.SWAP_CHAIN_BACK_BUFFER_FORMAT;
// Create the PSO
GraphicsPipeline.GraphicsDevice.NativeDevice.Ptr->CreateGraphicsPipelineState(&psoDesc, __uuidof<ID3D12PipelineState>(), _pipelineState.GetVoidAddressOf());
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
private unsafe void PerformReflection(Span<byte> byteCode)
{
using ComPtr<ID3D12ShaderReflection> reflection = default;
fixed (void* codePtr = byteCode)
utils.Get()->CreateReflection(&reflectionData, __uuidof<ID3D12ShaderReflection>(), reflection.GetVoidAddressOf());
if (reflection.Get() == null)
{
D3DReflect(codePtr, (nuint)byteCode.Length, __uuidof<ID3D12ShaderReflection>(), reflection.GetVoidAddressOf());
throw new Exception("Failed to create shader reflection from DXC output");
}
ShaderDescription shaderDesc;
reflection.Get()->GetDesc(&shaderDesc);
var cbufferRegistry = _constantBuffers.ToDictionary(cb => cb.Name);
var textureRegistry = _textures.ToDictionary(t => t.Name);
var textureRegistry = _regularTextures.ToDictionary(t => t.Name);
for (uint i = 0; i < shaderDesc.BoundResources; i++)
{
@@ -352,7 +477,8 @@ public unsafe class Shader : IDisposable
continue;
}
// The root parameter index for textures is after all CBVs
// ALL texture input slots are regular textures!
// Bindless textures don't use explicit texture inputs - they use ResourceDescriptorHeap[index]
var textureInfo = new TextureInfo
{
Name = textureName,
@@ -361,25 +487,18 @@ public unsafe class Shader : IDisposable
};
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();
_regularTextures.Clear();
_regularTextures.AddRange(textureRegistry.Values);
}
/// <summary>
/// Gets a unique, stable ID for a shader property.
/// This should be called once and the ID cached for repeated use.
/// </summary>
/// <param name="propertyName">The name of the property (e.g., "_Color").</param>
/// <returns>The integer ID of the property, or -1 if not found.</returns>
@@ -388,17 +507,6 @@ public unsafe class Shader : IDisposable
return _propertyNameToIdMap.TryGetValue(propertyName, out var id) ? id : -1;
}
/// <summary>
/// Gets a unique, stable ID for a texture property.
/// This should be called once and the ID cached for repeated use.
/// </summary>
/// <param name="textureName">The name of the texture (e.g., "_MainTex").</param>
/// <returns>The integer ID of the texture, or -1 if not found.</returns>
public int GetTextureId(string textureName)
{
return _textureNameToIdMap.TryGetValue(textureName, out var id) ? id : -1;
}
public void Dispose()
{
if (_disposed)
@@ -408,11 +516,12 @@ public unsafe class Shader : IDisposable
_pipelineState.Dispose();
_rootSignature.Dispose();
_samplerHeap.Dispose();
_constantBuffers.Clear();
_properties.Clear();
_textures.Clear();
_textureNameToIdMap.Clear();
_propertyNameToIdMap.Clear();
_regularTextures.Clear();
GC.SuppressFinalize(this);

View File

@@ -1,48 +0,0 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Helpers;
namespace Ghost.Graphics.Shading;
public enum ShaderPropertyType
{
Float,
Float2,
Float3,
Float4,
Color,
Matrix,
Texture2D,
Texture3D
}
public struct ShaderProperty : IDisposable
{
private UnsafeArray<byte> _value;
private FixedString128 _name;
private readonly uint _valueOffset;
internal readonly uint Offset => _valueOffset;
public readonly string Name => _name.Value;
public readonly ReadOnlySpan<byte> Value => _value.AsSpan();
public ShaderPropertyType PropertyType
{
get;
}
public ShaderProperty(Span<byte> value, uint offset, string name, ShaderPropertyType type)
{
_value = new(value.Length, Allocator.Persistent);
_valueOffset = offset;
_name = new(name);
PropertyType = type;
}
public void Dispose()
{
_value.Dispose();
_name.Dispose();
}
}