Document D3D12 best practices
This commit is contained in:
parent
bed74180b1
commit
9bb558d74f
6 changed files with 225 additions and 5 deletions
50
docs/d3d12-best-practices-audit.md
Normal file
50
docs/d3d12-best-practices-audit.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
28
research/d3d12/source-manifest.md
Normal file
28
research/d3d12/source-manifest.md
Normal 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
130
research/d3d12/synthesis.md
Normal 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.
|
||||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue