Add D3D12 descriptor upload foothold

This commit is contained in:
Meta Vens 2026-05-09 12:35:55 +01:00
commit e672042a3e
5 changed files with 131 additions and 14 deletions

View file

@ -9,11 +9,12 @@ has feature parity.
- `--renderer d3d11` is the default renderer and owns the live scene.
- `--renderer d3d12` creates a D3D12 device, command queue, flip-discard
swapchain, RTV heap, per-backbuffer command allocators, command list,
fence-backed present loop, fullscreen root signature, and a smoke-test PSO.
fence-backed present loop, shader-visible CBV/SRV/UAV descriptor arena,
per-frame upload buffers, fullscreen root signature, and a smoke-test PSO.
- The D3D12 path currently draws a diagnostic fullscreen pass. It exists to
prove backend lifecycle, shader compilation, root signature creation, PSO
creation, render target binding, and draw submission before real scene passes
migrate.
prove backend lifecycle, shader compilation, root signature creation,
descriptor-table binding, per-frame constant upload, PSO creation, render
target binding, and draw submission before real scene passes migrate.
- `IAquariumRenderer` is the host boundary. The host should not learn backend
internals as the D3D12 renderer grows teeth.
@ -21,8 +22,8 @@ has feature parity.
1. Keep D3D11 as the visual reference.
2. Move shared renderer contracts behind explicit backend-neutral types.
3. Port descriptor heaps, upload/ring buffers, and pass
resource lifetime.
3. Replace the smoke-only descriptor/upload code with general ring-buffer and
resource lifetime helpers.
4. Port the existing passes in visible order: grid height, froxel volume, scene,
bloom, resolve, overlay/debug.
5. Build the transparent stochastic surface pipe in D3D12 once descriptor and

View file

@ -6,6 +6,8 @@ using Vortice.Direct3D;
using Vortice.Direct3D12;
using Vortice.DXGI;
using Vortice.Mathematics;
using System.Numerics;
using System.Runtime.InteropServices;
namespace Aquarium.Engine.Render;
@ -19,6 +21,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
private readonly ID3D12CommandQueue commandQueue;
private readonly IDXGISwapChain3 swapChain;
private readonly ID3D12DescriptorHeap renderTargetViewHeap;
private readonly DescriptorArena shaderDescriptorArena;
private readonly FrameResources[] frames = new FrameResources[BackBufferCount];
private readonly ID3D12GraphicsCommandList commandList;
private readonly ID3D12RootSignature fullscreenRootSignature;
@ -66,9 +69,10 @@ public sealed class D3D12Renderer : IAquariumRenderer
renderTargetViewHeap = device.CreateDescriptorHeap(new DescriptorHeapDescription(DescriptorHeapType.RenderTargetView, BackBufferCount));
renderTargetViewDescriptorSize = (int)device.GetDescriptorHandleIncrementSize(DescriptorHeapType.RenderTargetView);
shaderDescriptorArena = new DescriptorArena(device, DescriptorHeapType.ConstantBufferViewShaderResourceViewUnorderedAccessView, BackBufferCount, DescriptorHeapFlags.ShaderVisible);
for (var index = 0; index < frames.Length; index++)
{
frames[index] = new FrameResources(device.CreateCommandAllocator(CommandListType.Direct));
frames[index] = CreateFrameResources(index);
}
CreateRenderTargetViews();
@ -119,6 +123,13 @@ public sealed class D3D12Renderer : IAquariumRenderer
{
var frameResources = frames[frameIndex];
WaitForFrame(frameResources);
frameResources.WriteSmokeConstants(new SmokeConstants(
new Vector4(
0.020f + settings.SceneExposure * 0.08f,
0.048f + settings.BloomIntensity * 0.55f,
0.066f + settings.BloomVeilIntensity * 1.2f,
1.0f),
new Vector4(frame.TimeSeconds, RenderDebugMode, DebugUiVisible ? 1.0f : 0.0f, 0.0f)));
frameResources.CommandAllocator.Reset();
commandList.Reset(frameResources.CommandAllocator, null);
@ -137,10 +148,13 @@ public sealed class D3D12Renderer : IAquariumRenderer
fenceEvent.Dispose();
fence.Dispose();
commandList.Dispose();
smokePipelineState.Dispose();
fullscreenRootSignature.Dispose();
for (var index = 0; index < frames.Length; index++)
{
frames[index].Dispose();
}
shaderDescriptorArena.Dispose();
renderTargetViewHeap.Dispose();
swapChain.Dispose();
commandQueue.Dispose();
@ -168,8 +182,10 @@ public sealed class D3D12Renderer : IAquariumRenderer
context.CommandList.ResourceBarrier(ResourceBarrier.BarrierTransition(context.BackBuffer, ResourceStates.Present, ResourceStates.RenderTarget));
context.CommandList.ClearRenderTargetView(context.RenderTargetView, clearColor);
context.CommandList.SetDescriptorHeaps(shaderDescriptorArena.Heap);
context.CommandList.SetPipelineState(smokePipelineState);
context.CommandList.SetGraphicsRootSignature(fullscreenRootSignature);
context.CommandList.SetGraphicsRootDescriptorTable(0, frames[frameIndex].SmokeConstantsGpuDescriptor);
context.CommandList.RSSetViewports(viewport);
context.CommandList.RSSetScissorRects(scissorRect);
context.CommandList.OMSetRenderTargets(context.RenderTargetView, null);
@ -215,13 +231,41 @@ public sealed class D3D12Renderer : IAquariumRenderer
private ID3D12RootSignature CreateFullscreenRootSignature()
{
var constantBufferRange = new DescriptorRange(
DescriptorRangeType.ConstantBufferView,
1,
0,
0,
D3D12.DescriptorRangeOffsetAppend);
var rootParameters = new[]
{
new RootParameter(new RootDescriptorTable([constantBufferRange]), ShaderVisibility.Pixel),
};
var description = new RootSignatureDescription(
RootSignatureFlags.AllowInputAssemblerInputLayout,
[],
rootParameters,
[]);
return device.CreateRootSignature(0, in description, RootSignatureVersion.Version1);
}
private FrameResources CreateFrameResources(int index)
{
var commandAllocator = device.CreateCommandAllocator(CommandListType.Direct);
var uploadBuffer = device.CreateCommittedResource(
HeapType.Upload,
ResourceDescription.Buffer(D3D12.ConstantBufferDataPlacementAlignment),
ResourceStates.GenericRead,
null);
var constantBufferSlot = shaderDescriptorArena.Allocate();
device.CreateConstantBufferView(
new ConstantBufferViewDescription(
uploadBuffer.GPUVirtualAddress,
D3D12.ConstantBufferDataPlacementAlignment),
constantBufferSlot.Cpu);
return new FrameResources(commandAllocator, uploadBuffer, constantBufferSlot.Gpu);
}
private ID3D12PipelineState CreateSmokePipelineState(string path)
{
var vertexShader = CompileShader(path, "FullscreenTriangleVS", "vs_5_0");
@ -258,18 +302,74 @@ public sealed class D3D12Renderer : IAquariumRenderer
ID3D12Resource BackBuffer,
CpuDescriptorHandle RenderTargetView);
private sealed class FrameResources(ID3D12CommandAllocator commandAllocator) : IDisposable
[StructLayout(LayoutKind.Sequential)]
private readonly record struct SmokeConstants(Vector4 Tint, Vector4 Params);
private sealed class FrameResources(
ID3D12CommandAllocator commandAllocator,
ID3D12Resource smokeConstantBuffer,
GpuDescriptorHandle smokeConstantsGpuDescriptor) : IDisposable
{
public ID3D12CommandAllocator CommandAllocator { get; } = commandAllocator;
public ID3D12Resource SmokeConstantBuffer { get; } = smokeConstantBuffer;
public GpuDescriptorHandle SmokeConstantsGpuDescriptor { get; } = smokeConstantsGpuDescriptor;
public ID3D12Resource BackBuffer { get; set; } = null!;
public ulong FenceValue { get; set; }
public void WriteSmokeConstants(SmokeConstants constants)
{
SmokeConstantBuffer.Map<SmokeConstants>(0, 1)[0] = constants;
}
public void Dispose()
{
BackBuffer.Dispose();
SmokeConstantBuffer.Dispose();
CommandAllocator.Dispose();
}
}
private sealed class DescriptorArena : IDisposable
{
private readonly int descriptorSize;
private int used;
public DescriptorArena(ID3D12Device device, DescriptorHeapType type, int capacity, DescriptorHeapFlags flags)
{
Type = type;
Capacity = capacity;
Heap = device.CreateDescriptorHeap(new DescriptorHeapDescription(type, (uint)capacity, flags));
descriptorSize = (int)device.GetDescriptorHandleIncrementSize(type);
}
public ID3D12DescriptorHeap Heap { get; }
public DescriptorHeapType Type { get; }
public int Capacity { get; }
public DescriptorSlot Allocate()
{
if (used >= Capacity)
{
throw new InvalidOperationException($"D3D12 {Type} descriptor arena exhausted ({Capacity} descriptors).");
}
var index = used++;
return new DescriptorSlot(
Heap.GetCPUDescriptorHandleForHeapStart() + (index * descriptorSize),
Heap.GetGPUDescriptorHandleForHeapStart() + (index * descriptorSize));
}
public void Dispose()
{
Heap.Dispose();
}
}
private readonly record struct DescriptorSlot(CpuDescriptorHandle Cpu, GpuDescriptorHandle Gpu);
}

View file

@ -4,6 +4,12 @@ struct VertexOut
float2 uv : TEXCOORD0;
};
cbuffer SmokeConstants : register(b0)
{
float4 smokeTint;
float4 smokeParams;
};
VertexOut FullscreenTriangleVS(uint vertexId : SV_VertexID)
{
float2 uv = float2((vertexId << 1) & 2, vertexId & 2);
@ -15,7 +21,8 @@ VertexOut FullscreenTriangleVS(uint vertexId : SV_VertexID)
float4 D3D12SmokePS(VertexOut input) : SV_Target0
{
float3 baseColor = lerp(float3(0.012, 0.022, 0.032), float3(0.018, 0.040, 0.058), input.uv.y);
float3 baseColor = lerp(float3(0.012, 0.022, 0.032), smokeTint.rgb, input.uv.y);
float vignette = smoothstep(0.86, 0.18, length(input.uv - 0.5));
return float4(baseColor * (0.72 + 0.28 * vignette), 1.0);
float pulse = 0.92 + 0.08 * sin(smokeParams.x);
return float4(baseColor * (0.72 + 0.28 * vignette) * pulse, 1.0);
}

View file

@ -62,3 +62,4 @@
{"area":"renderer/d3d12","lesson":"Before building the transparent stochastic surface pipe, keep D3D11 as the reference backend and add D3D12 beside it behind IAquariumRenderer. The first D3D12 pass should prove device, command queue, swapchain, RTV heap, command list, fence, clear, and present lifecycle with headless execution before porting scene passes.","files":["src/Aquarium.Engine/AquariumHost.cs","src/Aquarium.Engine/Render/IAquariumRenderer.cs","src/Aquarium.Engine/Render/D3D11Renderer.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","docs/d3d12-migration.md","state/scratch.md"],"verification":["dotnet build src\\Aquarium.Engine\\Aquarium.Engine.csproj","Aquarium.Engine.exe --headless --renderer d3d12 with latest known-good live assembly exited 0","Aquarium.Engine.exe --headless --renderer d3d11 with latest known-good live assembly exited 0"],"ts":"2026-05-09"}
{"area":"renderer/backend-selection","lesson":"Backend-neutral startup logging matters once D3D12 exists beside D3D11. Live runtime messages should describe Aquarium host invariants, not claim a specific graphics API that may be false for the selected renderer.","files":["src/Aquarium.Engine.Live/AquariumRuntime.cs"],"verification":["dotnet build Aquarium.Engine.slnx",".\\scripts\\dev-reload.ps1 -Headless -RetainSlots 4 -HeadlessTimeoutSeconds 60","Latest slot Aquarium.Engine.exe --headless --renderer d3d12 exited 0 and logged D3D12 device/swapchain creation"],"ts":"2026-05-09"}
{"area":"renderer/d3d12","lesson":"A useful D3D12 foothold must exercise shader and PSO machinery, not just ClearRenderTargetView. The D3D12 backend now uses per-swapchain-buffer command allocators, a pass context, fullscreen root signature, compiled smoke shader, PSO, viewport/scissor/RTV binding, and DrawInstanced fullscreen triangle submission.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Smoke.hlsl","docs/d3d12-migration.md","state/scratch.md"],"verification":["dotnet build src\\Aquarium.Engine\\Aquarium.Engine.csproj","Aquarium.Engine.exe --headless --renderer d3d12 exited 0 and logged fullscreen smoke pipeline creation"],"ts":"2026-05-09"}
{"area":"renderer/d3d12","lesson":"D3D12 descriptor/upload ownership is now represented in the backend instead of hiding inside a draw call. The smoke pass uses a shader-visible CBV/SRV/UAV descriptor arena, one upload constant buffer per swapchain frame, a CBV descriptor table at b0, and per-frame writes before command recording. Generalize this foothold into renderer-wide descriptor and upload ring helpers before porting Aquarium scene resources.","files":["src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Smoke.hlsl","docs/d3d12-migration.md","state/scratch.md"],"verification":["dotnet build src\\Aquarium.Engine\\Aquarium.Engine.csproj","Aquarium.Engine.exe --headless --renderer d3d12 exited 0 with CBV descriptor table smoke pass"],"ts":"2026-05-09"}

View file

@ -5,9 +5,11 @@
D3D12 migration foothold. D3D11 remains the visual reference renderer; the host
now talks through `IAquariumRenderer`, and `--renderer d3d12` selects a D3D12
backend with device/queue/swapchain/RTV heap/per-frame command allocators/fence,
plus a fullscreen root signature and smoke-test PSO that draws a diagnostic
fullscreen triangle. Next renderer work should port descriptor/upload/resource
ownership into D3D12 before building the stochastic transparent surface pipe.
plus a shader-visible descriptor arena, per-frame upload constant buffers,
fullscreen root signature, and smoke-test PSO that draws a diagnostic fullscreen
triangle. Next renderer work should generalize this into renderer-owned
descriptor/upload/resource helpers before building the stochastic transparent
surface pipe.
## Hot Context
@ -220,3 +222,9 @@ ownership into D3D12 before building the stochastic transparent surface pipe.
PSO, binds viewport/scissor/RTV, and draws a fullscreen triangle. This proves
shader, root signature, PSO, and draw submission before real Aquarium passes
migrate.
- D3D12 descriptor/upload foothold: the D3D12 path now owns a shader-visible
CBV/SRV/UAV descriptor arena and per-frame upload constant buffers. The smoke
pass binds a CBV descriptor table at b0 and uploads frame/debug/HDR-tinted
constants before drawing. This is still diagnostic, but it proves descriptor
heap visibility, root descriptor table binding, and per-frame CPU-to-GPU
upload ownership.