Refactor pipeline state and render output abstractions

- Replace old pipeline enums/structs with new strongly-typed PipelineState and enums (ZTest, ZWrite, Cull, Blend, ColorWriteMask)
- Redesign pipeline keying: introduce 128-bit GraphicsPipelineKey, MaterialPipelineKey, and PassPipelineKey for robust PSO caching
- Replace IRenderTargetStrategy with IRenderOutput; add SwapChainRenderOutput and TextureRenderOutput
- Update renderer and window code to use new render output abstraction and handle viewport/scissor updates
- Make ShaderPass a readonly struct and Shader a struct; use ID-based pass lookup for efficiency
- Materials now support per-pass pipeline overrides with new keying
- Add defensive checks in D3D12CommandBuffer; update D3D12PipelineLibrary for new keying/state
- Move test shader to test.gsdef and update for new pipeline state syntax
- Remove obsolete files/interfaces and perform general code cleanups
- Update all usages and parsing logic for new pipeline state system
This commit is contained in:
2025-12-24 19:06:34 +09:00
parent b8ce824292
commit a89719bfc9
25 changed files with 575 additions and 363 deletions

View File

@@ -26,7 +26,9 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
private readonly D3D12DescriptorAllocator _descriptorAllocator;
private readonly CommandBufferType _type;
#if !DEBUG
private CommandError _lastError;
#endif
private ushort _commandCount;
private bool _isRecording;
private bool _disposed;
@@ -166,10 +168,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
_commandList.Get()->Close();
_isRecording = false;
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return Result.Failure($"Command buffer ended with errors at {_lastError.CommandIndex}, command '{_lastError.CommandName}': {_lastError.Status}");
}
#endif
return Result.Success();
}
@@ -178,6 +182,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var d3d12Rect = new RECT((int)rect.Left, (int)rect.Top, (int)rect.Right, (int)rect.Bottom);
@@ -188,6 +198,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var count = 0u;
@@ -238,6 +254,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
if (stateBefore == stateAfter)
@@ -264,6 +286,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var recordResult = _resourceDatabase.GetResourceRecord(resource);
@@ -290,6 +318,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var pRtvHandles = stackalloc D3D12_CPU_DESCRIPTOR_HANDLE[renderTargets.Length];
@@ -337,6 +371,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var pRtvDescs = stackalloc D3D12_RENDER_PASS_RENDER_TARGET_DESC[rtDescs.Length];
@@ -419,6 +459,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
_commandList.Get()->EndRenderPass();
@@ -428,6 +474,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var d3d12Viewport = new D3D12_VIEWPORT(viewport.X, viewport.Y, viewport.Width, viewport.Height, viewport.MinDepth, viewport.MaxDepth);
@@ -438,14 +490,18 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var psor = _pipelineLibrary.GetGraphicsPSO(pipelineKey);
if (psor.Error != ErrorStatus.None)
{
#if DEBUG || GHOST_EDITOR
Logger.LogError($"Failed to get graphics pipeline state object for key {pipelineKey}: {psor.Error}");
#endif
RecordError(nameof(SetPipelineState), psor.Error);
return;
}
@@ -457,6 +513,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var resource = _resourceDatabase.GetResource(buffer.AsResource());
@@ -467,6 +529,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var recordResult = _resourceDatabase.GetResourceRecord(buffer.AsResource());
@@ -491,6 +559,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var resource = _resourceDatabase.GetResource(buffer.AsResource());
@@ -508,6 +582,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var d3d12Topology = topology switch
@@ -525,6 +605,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
_commandList.Get()->DrawInstanced(vertexCount, instanceCount, startVertex, startInstance);
@@ -534,6 +620,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
_commandList.Get()->DrawIndexedInstanced(indexCount, instanceCount, startIndex, baseVertex, startInstance);
@@ -543,6 +635,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
_commandList.Get()->Dispatch(threadGroupCountX, threadGroupCountY, threadGroupCountZ);
@@ -552,6 +650,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
_commandList.Get()->DispatchMesh(threadGroupCountX, threadGroupCountY, threadGroupCountZ);
@@ -573,6 +677,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var sizeInBytes = (uint)(data.Length * sizeof(T));
@@ -599,6 +709,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var resource = _resourceDatabase.GetResource(texture.AsResource());
@@ -634,6 +750,12 @@ internal unsafe class D3D12CommandBuffer : ICommandBuffer
{
ThrowIfDisposed();
ThrowIfNotRecording();
#if !DEBUG
if (_lastError.Status != ErrorStatus.None)
{
return;
}
#endif
IncrementCommandCount();
var pDestResource = _resourceDatabase.GetResource(dest.AsResource());

View File

@@ -233,23 +233,23 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary
return Result.Success(cbufferInfo);
}
private static D3D12_COMPARISON_FUNC ToD3DCompare(ZTestOptions z) => z switch
private static D3D12_COMPARISON_FUNC ToD3DCompare(ZTest z) => z switch
{
ZTestOptions.Disabled => D3D12_COMPARISON_FUNC_NEVER,
ZTestOptions.Less => D3D12_COMPARISON_FUNC_LESS,
ZTestOptions.LessEqual => D3D12_COMPARISON_FUNC_LESS_EQUAL,
ZTestOptions.Equal => D3D12_COMPARISON_FUNC_EQUAL,
ZTestOptions.GreaterEqual => D3D12_COMPARISON_FUNC_GREATER_EQUAL,
ZTestOptions.Greater => D3D12_COMPARISON_FUNC_GREATER,
ZTestOptions.NotEqual => D3D12_COMPARISON_FUNC_NOT_EQUAL,
ZTestOptions.Always => D3D12_COMPARISON_FUNC_ALWAYS,
ZTest.Disabled => D3D12_COMPARISON_FUNC_NEVER,
ZTest.Less => D3D12_COMPARISON_FUNC_LESS,
ZTest.LessEqual => D3D12_COMPARISON_FUNC_LESS_EQUAL,
ZTest.Equal => D3D12_COMPARISON_FUNC_EQUAL,
ZTest.GreaterEqual => D3D12_COMPARISON_FUNC_GREATER_EQUAL,
ZTest.Greater => D3D12_COMPARISON_FUNC_GREATER,
ZTest.NotEqual => D3D12_COMPARISON_FUNC_NOT_EQUAL,
ZTest.Always => D3D12_COMPARISON_FUNC_ALWAYS,
_ => D3D12_COMPARISON_FUNC_LESS_EQUAL
};
private static D3D12_DEPTH_STENCIL_DESC BuildDepthStencil(ZTestOptions ztest, ZWriteOptions zwrite)
private static D3D12_DEPTH_STENCIL_DESC BuildDepthStencil(ZTest ztest, ZWrite zwrite)
{
var depthEnabled = ztest != ZTestOptions.Disabled;
var writeEnabled = zwrite == ZWriteOptions.On;
var depthEnabled = ztest != ZTest.Disabled;
var writeEnabled = zwrite == ZWrite.On;
var cmp = ToD3DCompare(ztest);
return D3D12Utility.D3D12_DEPTH_STENCIL_DESC_CREATE(depthEnabled, writeEnabled, cmp);
}
@@ -295,23 +295,16 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary
return psr.Value;
}
var hash = new GraphicsPipelineHash
if (descriptor.RtvFormats.Length > D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT)
{
Id = descriptor.PassId,
RtvCount = (uint)descriptor.RtvFormats.Length,
DsvFormat = descriptor.DsvFormat,
};
var rtvCount = (uint)Math.Min(descriptor.RtvFormats.Length, D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT);
for (var i = 0; i < rtvCount && i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT; i++)
{
hash.RtvFormats[i] = descriptor.RtvFormats[i];
return Result.Failure($"RTV format count exceeds the maximum supported render target count of {D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT}.");
}
var key = hash.GetKey();
var passPipelineKey = new PassPipelineKey(descriptor.RtvFormats, descriptor.DsvFormat);
var materialPipelineKey = new MaterialPipelineKey(descriptor.PassId, descriptor.PipelineOption);
var pipelineKey = GraphicsPipelineKey.Combine(materialPipelineKey, passPipelineKey);
if (!_pipelineCache.ContainsKey(key))
if (!_pipelineCache.ContainsKey(pipelineKey))
{
var result = ValidatePassReflectionData(in compiled);
if (result.IsFailure)
@@ -327,26 +320,26 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary
PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE,
SampleMask = UINT32_MAX,
SampleDesc = new DXGI_SAMPLE_DESC(1, 0),
NumRenderTargets = rtvCount,
NumRenderTargets = (uint)descriptor.RtvFormats.Length,
DSVFormat = descriptor.DsvFormat.ToDXGIFormat(),
DepthStencilState = BuildDepthStencil(descriptor.ZTest, descriptor.ZWrite),
DepthStencilState = BuildDepthStencil(descriptor.PipelineOption.ZTest, descriptor.PipelineOption.ZWrite),
NodeMask = 0,
Flags = D3D12_PIPELINE_STATE_FLAG_NONE,
BlendState = descriptor.Blend switch
BlendState = descriptor.PipelineOption.Blend switch
{
BlendOptions.Opaque => D3D12Utility.D3D12_BLEND_DESC_OPAQUE,
BlendOptions.Alpha => D3D12Utility.D3D12_BLEND_DESC_ALPHA_BLEND,
BlendOptions.Additive => D3D12Utility.D3D12_BLEND_DESC_ADDITIVE,
BlendOptions.Multiply => D3D12Utility.D3D12_BLEND_DESC_MULTIPLY,
BlendOptions.PremultipliedAlpha => D3D12Utility.D3D12_BLEND_DESC_PREMULTIPLIED,
Blend.Opaque => D3D12Utility.D3D12_BLEND_DESC_OPAQUE,
Blend.Alpha => D3D12Utility.D3D12_BLEND_DESC_ALPHA_BLEND,
Blend.Additive => D3D12Utility.D3D12_BLEND_DESC_ADDITIVE,
Blend.Multiply => D3D12Utility.D3D12_BLEND_DESC_MULTIPLY,
Blend.PremultipliedAlpha => D3D12Utility.D3D12_BLEND_DESC_PREMULTIPLIED,
_ => D3D12Utility.D3D12_BLEND_DESC_OPAQUE
},
RasterizerState = descriptor.Cull switch
RasterizerState = descriptor.PipelineOption.Cull switch
{
CullOptions.Off => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_NONE,
CullOptions.Front => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_CLOCKWISE,
CullOptions.Back => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_COUNTER_CLOCKWISE,
Cull.Off => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_NONE,
Cull.Front => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_CLOCKWISE,
Cull.Back => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_COUNTER_CLOCKWISE,
_ => D3D12Utility.D3D12_RASTERIZER_DESC_CULL_NONE
},
};
@@ -356,10 +349,10 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary
desc.AS = new D3D12_SHADER_BYTECODE(compiled.tsResult.bytecode.GetUnsafePtr(), (nuint)compiled.tsResult.bytecode.Count);
}
for (var i = 0; i < rtvCount && i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT; i++)
for (var i = 0; i < descriptor.RtvFormats.Length; i++)
{
desc.RTVFormats[i] = descriptor.RtvFormats[i].ToDXGIFormat();
desc.BlendState.RenderTarget[i].RenderTargetWriteMask = (byte)(descriptor.ColorMask & 0x0F);
desc.BlendState.RenderTarget[i].RenderTargetWriteMask = (byte)((int)descriptor.PipelineOption.ColorMask & 0x0F);
}
var meshStream = new CD3DX12_PIPELINE_MESH_STATE_STREAM(in desc);
@@ -373,7 +366,7 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary
var pKeyStr = stackalloc char[GraphicsPipelineKey.KEY_STRING_LENGTH];
var keySpan = new Span<char>(pKeyStr, GraphicsPipelineKey.KEY_STRING_LENGTH);
var kr = key.GetString(keySpan);
var kr = pipelineKey.GetString(keySpan);
if (kr.IsFailure)
{
return kr;
@@ -396,10 +389,15 @@ internal unsafe class D3D12PipelineLibrary : IPipelineLibrary
pso.psoDesc = desc;
pso.pso.Attach(pPipelineState);
_pipelineCache[key] = pso;
_pipelineCache[pipelineKey] = pso;
}
return key;
return pipelineKey;
}
public bool HasPipeline(GraphicsPipelineKey key)
{
return _pipelineCache.ContainsKey(key);
}
public Result<SharedPtr<ID3D12PipelineState>, ErrorStatus> GetGraphicsPSO(GraphicsPipelineKey key)

View File

@@ -22,7 +22,7 @@ internal class D3D12Renderer : IRenderer
// NOTE: Testing only.
private readonly MeshRenderPass _pass;
public IRenderTargetStrategy? RenderTargetStrategy
public IRenderOutput? RenderOutput
{
get; set;
}
@@ -47,31 +47,31 @@ internal class D3D12Renderer : IRenderer
public Result Render(ICommandAllocator commandAllocator)
{
if (RenderTargetStrategy is null)
if (RenderOutput is null)
{
return Result.Failure("Render target strategy is not set.");
}
var target = RenderTargetStrategy.GetRenderTarget();
var target = RenderOutput.GetRenderTarget();
if (target.IsInvalid)
{
return Result.Failure("Render target is invalid.");
}
_commandBuffer.Begin(commandAllocator);
RenderTargetStrategy.BeginRender(_commandBuffer);
RenderOutput.BeginRender(_commandBuffer);
// NOTE: Temperary solution: render directly to the swap chain back buffer if available.
// HACK: This is hard coded for testing purposes only.
var error = RenderScene(target, RenderTargetStrategy.Viewport, RenderTargetStrategy.Scissor);
var error = RenderScene(target, RenderOutput.Viewport, RenderOutput.Scissor);
if (error != ErrorStatus.None)
{
_commandBuffer.End();
return Result.Failure(error);
}
RenderTargetStrategy.EndRender(_commandBuffer);
RenderOutput.EndRender(_commandBuffer);
var r = _commandBuffer.End();
if (r.IsFailure)
{
@@ -79,7 +79,7 @@ internal class D3D12Renderer : IRenderer
}
_graphicsEngine.Device.GraphicsQueue.Submit(_commandBuffer);
RenderTargetStrategy.Present();
RenderOutput.Present();
return Result.Success();
}

View File

@@ -100,8 +100,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
private UnsafeHashMap<SamplerDesc, Identifier<Sampler>> _samplers;
private UnsafeSlotMap<Mesh> _meshes;
private UnsafeSlotMap<Material> _materials;
private readonly DynamicArray<Shader?> _shaders; // NOTE: We use a simple list since shader is not frequently added/removed. This can save 4 bytes for each ecs component.
// private UnsafeHashMap<ShaderPassKey, ShaderPass> _shaderPasses; // NOTE: The reason we use Dictionary here is that ShaderPassKey is a presistence identifier across multiple application sessions.
private readonly DynamicArray<Shader> _shaders; // TODO: Use SlotMap?
private bool _disposed;
@@ -116,7 +115,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
_samplers = new UnsafeHashMap<SamplerDesc, Identifier<Sampler>>(32, Allocator.Persistent);
_meshes = new UnsafeSlotMap<Mesh>(64, Allocator.Persistent, AllocationOption.Clear);
_materials = new UnsafeSlotMap<Material>(16, Allocator.Persistent, AllocationOption.Clear);
_shaders = new DynamicArray<Shader?>(16);
_shaders = new DynamicArray<Shader>(16);
// _shaderPasses = new UnsafeHashMap<ShaderPassKey, ShaderPass>(32, Allocator.Persistent);
}
@@ -410,10 +409,10 @@ internal class D3D12ResourceDatabase : IResourceDatabase
public bool HasShader(Identifier<Shader> id)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return id.Value >= 0 && id.Value < _shaders.Count && _shaders[id.Value] != null;
return id.Value >= 0 && id.Value < _shaders.Count;
}
public Shader GetShaderReference(Identifier<Shader> id)
public ref Shader GetShaderReference(Identifier<Shader> id)
{
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -422,8 +421,7 @@ internal class D3D12ResourceDatabase : IResourceDatabase
throw new ArgumentOutOfRangeException(nameof(id), $"Shader id {id} is invalid.");
}
var shader = _shaders[id.Value]!;
return shader;
return ref _shaders[id.Value];
}
public void ReleaseShader(Identifier<Shader> id)
@@ -470,11 +468,6 @@ internal class D3D12ResourceDatabase : IResourceDatabase
for (var i = 0; i < _shaders.Count; i++)
{
ref var shader = ref _shaders[i];
if (shader == null)
{
continue;
}
ReleaseResource(ref shader);
}