Repository navigation
perf: let dx12 frames overlap instead of waiting on every frame - #202
Merged
Merged
Conversation
BeginRender now waits only for the frame that last used the slot it is about to reuse, against one monotonically increasing fence. Constant buffers and the light constants are copied into the per-frame upload ring at bind time, buffer and texture uploads are batched into one shared command list, PSOs come from a small cache, and DX12 textures get a mip chain.
…to perf/dx12-frames-in-flight # Conflicts: # include/private/Graphics/DirectX12/DirectX12Renderer.hpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
DX12 can now actually have two frames in flight.
BeginRenderwaits only for the frame that last recorded into the slot it is about to reuse, and everything the CPU rewrites per frame was moved somewhere that is safe to overlap:m_fenceValuethat only goes up.EndRendersignals++m_fenceValueand stores it infenceValues[frameIndex],BeginRenderwaits forfenceValues[frameIndex]only, andWaitForGPUsignals a fresh value instead of rewriting the per-slot ones.DirectX12Bufferkeeps the contents ofConstantbuffers on the CPU.BindConstantBuffercopies them into the current frame'sDirectX12UploadRingslice on the first bind after an update and caches that address for the rest of the frame. The light/fog constants (m_lightUBO) go the same way, andUpdateShadowLightUBOrebinds b2 when it lands mid frame.DeferCleanupentries are stamped with the fence value of the frame that queued them and released once that value completes, instead of "one frame later". Textures and cubemaps now go through it too.DirectX12UploadContext: one command list, recycled allocators, staging buffers kept alive until their batch's fence passes. Buffers, textures and cubemaps record into it instead of each creating an allocator, list and fence and blocking on the queue. The renderer submits it atBeginRender, right before the main list inEndRender, and inWaitForGPU. Code that builds these classes without an uploader still works, it just flushes a local one (same blocking behavior as before).CreateShaderno longer overwrites a singlepipelineState. PSOs come from a small cache keyed by shader bytecode hash, vertex format, cull/depth bias/depth clip, depth write/func, blend, topology and RTV/DSV format. Skybox, debug lines and the shadow pass each build their own explicit key, and the shadow pass compilesdefault_shader_dx12.hlslfor its VS instead of borrowing whatever shader was created last.DirectX12Shader::bindgoes throughBindShaderPipeline, which leaves a skybox or debug line PSO alone while that pass is active.DirectX12Texturebuilds a full box filtered mip chain on the CPU and uploads every level, like GL (glGenerateMipmap) and Vulkan already do.Why
The old
BeginRenderwaited on the max of both slot counters, which was meant to be "wait for everything" so the shared mapped constant buffers could be rewritten safely. With #165 the two counters were signalled into one fence and advanced independently, so on every second frame the max was a value the fence had already passed and the wait did nothing while the CPU overwrote mapped memory the previous frame was still reading. On the other frames it really did wait for everything, so the CPU and GPU never overlapped at all. Fixing only the counters would keep that full stall, so I fixed the shared memory instead and dropped the wait to the one slot.The constant buffer race was the same one #186 fixed for the shadow transform: every transform/material CB was one mapped UPLOAD buffer written with
memcpyand bound by GPU VA, so the GPU reads whatever was written last, from this frame or the next.MeshBatcheven rewritess_batchTransformBufferseveral times per frame. With the ring each bind sees the contents it had at bind time.The PSO slot was a correctness bug as much as a perf one. Skybox and debug line creation gated on
pipelineState,EndSkyboxPassrestored the last created shader's PSO, the shadow PSO used the last created shader's VS (a skybox VS there would put every caster at the far plane), andDebugLineRenderercallsshader->bind()afterBeginDebugLinePass, which swapped the line PSO back to a triangle PSO and drew LINELIST against it. I also restore the default PSO after the shadow pass so nothing draws with the depth-only PSO by accident.Not covered here:
ConfigureRenderFace/ConfigureRenderModeare still no-ops, the cache makes cull/fill variants cheap to add but no caller asks for them on DX12 today. Cubemaps stay at one mip (skybox only). DX12 still ignores custom vertex formats (vertexFormatis in the key, but only the defaultVertexlayout is built).Material::BindcallsUpdate()on its CB, which has always been a no-op on DX12, so the material CB at b1 is still never bound there. Separate bug.Fixes #165
How it was verified
I do not have a Windows machine, so this was only checked by reading and by the Windows CI build. Nothing was run on D3D12. Worth a look on Windows with the debug layer and GPU based validation on: frame time should drop now that CPU and GPU overlap, a scene with several moving objects should not flicker or show other objects' transforms, debug lines should draw as lines, and textured surfaces at a distance should stop shimmering now that they have mips.
Stacked on #186 (uses its
DirectX12UploadRing).Checklist
///comments (all touched headers are private, documented anyway)src/orinclude/private/in public headers