Document D3D12 best practices

This commit is contained in:
Meta Vens 2026-05-09 12:49:27 +01:00
commit 9bb558d74f
6 changed files with 225 additions and 5 deletions

View file

@ -0,0 +1,50 @@
# D3D12 Best Practices Audit
## Verdict
The current D3D12 path is acceptable as a bring-up scaffold. It proves the right
classes of machinery: per-frame command allocators, fence-protected reuse,
shader-visible descriptors, persistent mapped constants, explicit render target
state, and an offscreen target path.
It is not yet a production renderer architecture. The next risk is letting
smoke-pass helpers become permanent resource policy by accident.
## Good Current Decisions
- `D3D12Renderer` keeps D3D11 intact as the visual reference backend.
- Command allocators are per swapchain frame and waited on before reset.
- Shader-visible descriptor slots are not overwritten while in flight.
- Constant upload memory is persistently mapped and 256-byte aligned.
- Offscreen render target state is tracked and transitions are skipped when
redundant.
- The D3D12 smoke path exercises shader compilation, root signature, PSO,
descriptor table, RTV binding, barriers, copy, and present.
## Temporary Acceptable Debt
- Committed resources are used directly. Replace with D3D12MA or placed-resource
suballocation before the renderer owns many textures/buffers.
- Descriptor arena is fixed-size and not fence-reclaimed. It should split into
static descriptors and transient per-frame ranges.
- Upload helper is one allocation per buffer. Replace with an upload ring before
porting large per-frame scene data.
- Backbuffer state is assumed. Wrap swapchain buffers in state-tracked frame
resources before resize and multi-pass presentation.
- The offscreen smoke target copies to the swapchain. Keep this only if the real
post-process graph needs it; otherwise present from the final render target
path with no ornamental copy.
## Required Before Real Pass Migration
- A named D3D12 resource registry for render targets and buffers.
- Static and transient descriptor allocation APIs.
- Upload ring with fence-based reclamation.
- Debug names for D3D12 resources, descriptors by owner, and command lists.
- PIX markers around passes.
- Backbuffer state tracking and resize-safe recreation.
## Rule
Do not port Aquarium's scene/froxel/history passes into ad hoc D3D12 resource
creation. Build the resource machine first, then move pixels through it.

View file

@ -19,16 +19,20 @@ has feature parity.
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.
- `docs/d3d12-best-practices-audit.md` and `research/d3d12/synthesis.md`
capture the current D3D12 doctrine and audit.
## Migration Order
1. Keep D3D11 as the visual reference.
2. Move shared renderer contracts behind explicit backend-neutral types.
3. Replace the smoke-only resource code with general render-target, SRV/UAV,
and upload ring helpers.
4. Port the existing passes in visible order: grid height, froxel volume, scene,
3. Replace the smoke-only resource code with a named resource registry,
static/transient descriptors, SRV/UAV creation, and an upload ring.
4. Add debug names and PIX markers while the backend is still small enough to
make mistakes obvious.
5. 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
6. Build the transparent stochastic surface pipe in D3D12 once descriptor and
candidate-buffer ownership is explicit.
## Invariants

View file

@ -0,0 +1,28 @@
# D3D12 Source Manifest
## Primary Sources
- Microsoft Learn, Resource binding overview:
https://learn.microsoft.com/en-us/windows/win32/direct3d12/resource-binding-flow-of-control
- Microsoft Learn, Fence-based resource management:
https://learn.microsoft.com/en-us/windows/win32/direct3d12/fence-based-resource-management
- Microsoft Learn, Memory Management in Direct3D 12:
https://learn.microsoft.com/en-us/windows/win32/direct3d12/memory-management
- Microsoft Learn, Direct3D 12 core interfaces:
https://learn.microsoft.com/en-us/windows/win32/direct3d12/direct3d-12-interfaces
- Microsoft DirectX Developer Blog, A Look Inside D3D12 Resource State Barriers:
https://devblogs.microsoft.com/directx/a-look-inside-d3d12-resource-state-barriers/
- NVIDIA Technical Blog, Advanced API Performance: Barriers:
https://developer.nvidia.com/blog/advanced-api-performance-barriers/
- AMD GPUOpen, RDNA Performance Guide:
https://gpuopen.com/learn/rdna-performance-guide/
- AMD GPUOpen, D3D12 Memory Allocator:
https://gpuopen.com/d3d12-memory-allocator/
- AMD GPUOpen, Effective Use of the New D3D12_HEAP_TYPE_GPU_UPLOAD:
https://gpuopen.com/learn/using-d3d12-heap-type-gpu-upload/
## Scope
This pass focused on D3D12 renderer architecture, not beginner API syntax:
descriptor heaps, root signatures, command allocators, fences, upload/ring
buffers, memory allocation, resource barriers, and GPU/CPU synchronization.

130
research/d3d12/synthesis.md Normal file
View file

@ -0,0 +1,130 @@
# D3D12 Synthesis
## Core Model
D3D12 is an explicit ownership API. The renderer owns command memory, resource
states, descriptor lifetime, upload lifetime, and GPU/CPU synchronization. The
driver stops hiding most of that work. Good D3D12 code is therefore less about
calling the right incantations and more about making ownership impossible to
misread.
The source material converges on a few hard rules:
- Keep root signatures stable and few. Bind a root signature once per command
list or pass family, not as routine draw noise.
- Use descriptor heaps deliberately. Shader-visible CBV/SRV/UAV heaps are
application-versioned memory; never overwrite descriptors still referenced by
in-flight GPU work.
- Treat RTV/DSV descriptors differently from shader-visible descriptors. RTV/DSV
bindings are versioned during command recording, but shader-visible heap
contents are not protected by the runtime.
- Reset command allocators only after the GPU has completed the work recorded
into them.
- Use fences to reclaim transient upload/descriptors/resources. A frame fence is
the coarse tool; finer fences can protect suballocations later.
- Prefer ring/suballocation systems for transient uploads and descriptors. Fixed
one-resource-per-use committed allocations are acceptable scaffolding, not the
renderer we keep.
- Track resource states explicitly and emit the minimum correct barriers. Avoid
broad generic states and redundant transitions.
- Batch or group barriers when that becomes practical; barriers and fences can
reduce GPU parallelism.
- Keep command submissions reasonably chunky. Tiny command lists and frequent
queue submissions pay CPU and GPU scheduling costs.
- Use PIX and vendor validation early. D3D12 mistakes are often legal-looking
until the GPU catches fire behind a curtain.
## Descriptor Doctrine
Microsoft's binding model puts descriptor heap content lifetime squarely on the
application for shader-visible heaps. The long-term Aquarium shape should be:
- One large shader-visible CBV/SRV/UAV heap for the frame, or one persistent
heap plus suballocated dynamic ranges.
- A CPU-visible/staging descriptor store for reusable descriptors.
- Per-frame transient descriptor ranges that are reclaimed by fence value.
- Stable descriptor ranges for long-lived textures/buffers.
- Minimal `SetDescriptorHeaps` calls. Heap changes can invalidate descriptor
table bindings, so pass code should bind the heap once before draws.
Current Aquarium D3D12 status:
- Good: shader-visible descriptor arena exists; per-frame CBV descriptors are
not overwritten while in flight.
- Temporary: arena is fixed and smoke-only. It must become a fence-reclaimed
allocator before real pass/material resources arrive.
## Upload Doctrine
Transient CPU-to-GPU data belongs in persistent mapped upload memory, typically
suballocated as a ring and reclaimed by fences. Constant buffers require 256-byte
alignment. Reading directly from upload memory is acceptable for small constant
data, but large frequently-read resources should be copied to default heap
memory before shader use.
Current Aquarium D3D12 status:
- Good: upload buffer is persistently mapped and 256-byte aligned for constant
buffer use.
- Temporary: helper is still one allocation per buffer. It should become an
upload ring with typed allocations and fence reclamation.
- Guardrail: keep direct shader reads from upload memory limited to small
constants. Structured/large scene data should move to default memory.
## Memory Doctrine
Committed resources are fine while bringing up a backend. They are not a
production memory manager. Microsoft recommends classifying resources and
budgeting/streaming residency; AMD recommends large heaps and suballocation,
with D3D12 Memory Allocator as the practical library path.
Current Aquarium D3D12 status:
- Good: committed default resources make ownership explicit during bring-up.
- Temporary: once scene/history/froxel resources grow, add D3D12MA or an
equivalent placed-resource allocator instead of keeping one committed resource
per texture/buffer.
## Barrier Doctrine
Barrier correctness is not optional, but barrier volume is a performance risk.
The renderer should track state per resource, use the narrowest valid states,
avoid redundant transitions, and group barriers when pass complexity grows.
Current Aquarium D3D12 status:
- Good: the offscreen render target tracks its current state and transitions
only on change.
- Temporary: backbuffer state is still assumed around present/copy. That is fine
in the current single path, but should become tracked or wrapped once resize,
multiple passes, or alternate present paths exist.
- Watch item: rendering to an offscreen target then copying to the swapchain is
useful for proving texture ownership, but final composition should avoid
unnecessary full-frame copies unless a post-process graph requires them.
## Command/Fence Doctrine
Per-frame command allocators must not be reset until the GPU is done with their
recorded work. Frame resources should carry fence values, and transient
descriptor/upload/resource allocations should tie reclamation to those values.
Current Aquarium D3D12 status:
- Good: one command allocator per swapchain frame and fence wait before reuse.
- Temporary: the renderer still uses a single graphics command list and a coarse
frame fence. That is fine until multi-threaded recording or async copy/compute
becomes real.
## Aquarium Next Steps
1. Add a D3D12 frame graph/resource map for named render targets and buffers.
2. Replace the fixed descriptor arena with static + transient descriptor
allocation, reclaimed by frame fence.
3. Replace one-off upload buffers with a per-frame or global upload ring.
4. Add D3D12MA or a deliberate placed-resource allocator before the resource
count becomes serious.
5. Add debug names and PIX markers while the graph is still small.
6. Track backbuffer resources with the same state wrapper used for offscreen
targets.
7. Port the first real pass only after descriptors, upload, render targets, and
state tracking have durable ownership.

View file

@ -64,3 +64,4 @@
{"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"}
{"area":"renderer/d3d12","lesson":"D3D12 now has reusable renderer-side primitives instead of smoke-pass-local plumbing: a descriptor arena, persistent mapped upload buffer, and default-heap render target with explicit state tracking. The smoke pass renders into the offscreen target and copies to the swapchain, proving render target lifetime and copy barriers before porting Aquarium scene/history/froxel textures.","files":["src/Aquarium.Engine/Aquarium.Engine.csproj","src/Aquarium.Engine/Render/D3D12DescriptorArena.cs","src/Aquarium.Engine/Render/D3D12UploadBuffer.cs","src/Aquarium.Engine/Render/D3D12RenderTarget.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 exited 0 with offscreen render target copy path"],"ts":"2026-05-09"}
{"area":"renderer/d3d12","lesson":"D3D12 best-practice pass distilled Microsoft, NVIDIA, and AMD guidance. Current Aquarium D3D12 backend is acceptable bring-up scaffolding, but real pass migration should wait for a named resource registry, static/transient descriptor allocation, fence-reclaimed upload ring, debug names, PIX markers, and backbuffer state tracking. Committed resources and fixed arenas must not become production policy by inertia.","files":["research/d3d12/source-manifest.md","research/d3d12/synthesis.md","docs/d3d12-best-practices-audit.md","docs/d3d12-migration.md","state/scratch.md"],"verification":["Research sources reviewed via Microsoft Learn, DirectX Developer Blog, NVIDIA Technical Blog, and AMD GPUOpen","Current D3D12 helper code audited against descriptor, upload, barrier, memory, and fence guidance"],"ts":"2026-05-09"}

View file

@ -9,7 +9,8 @@ plus a shader-visible descriptor arena, per-frame upload constant buffers,
renderer-owned offscreen render target, fullscreen root signature, and
smoke-test PSO that draws a diagnostic fullscreen triangle into the offscreen
target before copying to the swapchain. Next renderer work should generalize
this into renderer-owned SRV/UAV/upload-ring helpers before building the
this into a named resource registry, static/transient descriptor allocators,
SRV/UAV creation, upload ring, debug names, and PIX markers before building the
stochastic transparent surface pipe.
## Hot Context
@ -234,3 +235,9 @@ stochastic transparent surface pipe.
tracked state transitions, then copies that texture into the swapchain
backbuffer. This proves render-target resource lifetime and copy barriers
before Aquarium scene/history/froxel targets are ported.
- D3D12 research pass: `research/d3d12/synthesis.md` and
`docs/d3d12-best-practices-audit.md` distill Microsoft/NVIDIA/AMD guidance.
Current D3D12 path is valid bring-up scaffolding, but do not port real passes
until descriptor allocation is split into static/transient ranges, upload data
uses a fence-reclaimed ring, resource ownership is named/tracked, and debug
names/PIX markers exist.