From cbe53e789f84118c0f428e5f8651350c63893ba6 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Tue, 23 Jun 2026 11:20:22 -0400 Subject: [PATCH] Graphics: acquire D3D12 back buffer per-present (F11 fix) The backend cached the swapchain back buffers for the process lifetime, so the game's ResizeBuffers failed (DXGI_ERROR_INVALID_CALL) and UE4 crashed on any fullscreen/resolution change. Acquire the back buffer fresh in Begin() and release it in End() so no reference is held across frames. --- code/framework/src/graphics/backend/d3d12.cpp | 36 ++++++++++++++++--- code/framework/src/graphics/backend/d3d12.h | 15 +++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/code/framework/src/graphics/backend/d3d12.cpp b/code/framework/src/graphics/backend/d3d12.cpp index 03dfc9fa2..88d97661e 100644 --- a/code/framework/src/graphics/backend/d3d12.cpp +++ b/code/framework/src/graphics/backend/d3d12.cpp @@ -75,10 +75,9 @@ namespace Framework::Graphics { const auto rtvDescriptorSize = pD3DDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = _rtvHeap->GetCPUDescriptorHandleForHeapStart(); + // reserve one RTV slot per frame; the back buffer is bound fresh in Begin() for (UINT i = 0; i < _frameBufferCount; i++) { _frameContext[i]._mainRenderTargetDescriptor = rtvHandle; - swapChain->GetBuffer(i, IID_PPV_ARGS(&_frameContext[i]._mainRenderTargetResource)); - pD3DDevice->CreateRenderTargetView(_frameContext[i]._mainRenderTargetResource, nullptr, rtvHandle); rtvHandle.ptr += rtvDescriptorSize; } } @@ -108,13 +107,16 @@ namespace Framework::Graphics { void D3D12Backend::Shutdown() { // release objects + if (_currentBackBuffer) { + _currentBackBuffer->Release(); + _currentBackBuffer = nullptr; + } _rtvHeap->Release(); _srvHeap->Release(); _srvHeap = nullptr; _commandList->Release(); for (const auto &frameContext : _frameContext) { frameContext._commandAllocator->Release(); - frameContext._mainRenderTargetResource->Release(); } if (_device) { _device->Release(); @@ -123,12 +125,28 @@ namespace Framework::Graphics { } void D3D12Backend::Begin() { - const auto ¤tFrameContext = _frameContext[_swapChain->GetCurrentBackBufferIndex()]; + const UINT idx = _swapChain->GetCurrentBackBufferIndex(); + // _frameContext/RTV heap are sized to Init's buffer count; a replacement + // swapchain (SetSwapChain) with a larger count would index out of range + if (idx >= _frameContext.size()) { + return; + } + const auto ¤tFrameContext = _frameContext[idx]; currentFrameContext._commandAllocator->Reset(); + // acquire the back buffer fresh, release in End(); never held across + // frames so our ref doesn't block the game's ResizeBuffers + _currentBackBuffer = nullptr; + if (FAILED(_swapChain->GetBuffer(idx, IID_PPV_ARGS(&_currentBackBuffer)))) { + _currentBackBuffer = nullptr; + Framework::Logging::GetLogger(FRAMEWORK_INNER_GRAPHICS)->error("D3D12Backend::Begin, GetBuffer failed; skipping frame"); + return; + } + _device->CreateRenderTargetView(_currentBackBuffer, nullptr, currentFrameContext._mainRenderTargetDescriptor); + _barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; _barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; - _barrier.Transition.pResource = currentFrameContext._mainRenderTargetResource; + _barrier.Transition.pResource = _currentBackBuffer; _barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; _barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; _barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; @@ -140,11 +158,19 @@ namespace Framework::Graphics { } void D3D12Backend::End() { + if (!_currentBackBuffer) { + return; // Begin() failed to acquire this frame + } + _barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; _barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; _commandList->ResourceBarrier(1, &_barrier); _commandList->Close(); _commandQueue->ExecuteCommandLists(1, (ID3D12CommandList **)&_commandList); + + // Drop our per-frame reference; the swapchain still owns the buffer. + _currentBackBuffer->Release(); + _currentBackBuffer = nullptr; } void D3D12Backend::Update() {} diff --git a/code/framework/src/graphics/backend/d3d12.h b/code/framework/src/graphics/backend/d3d12.h index c567685fa..7e1d931b3 100644 --- a/code/framework/src/graphics/backend/d3d12.h +++ b/code/framework/src/graphics/backend/d3d12.h @@ -37,13 +37,16 @@ namespace Framework::Graphics { struct FrameContext { ID3D12CommandAllocator *_commandAllocator = nullptr; - ID3D12Resource *_mainRenderTargetResource = nullptr; D3D12_CPU_DESCRIPTOR_HANDLE _mainRenderTargetDescriptor; }; std::vector _frameContext; D3D12_RESOURCE_BARRIER _barrier {}; + // acquired fresh in Begin(), released in End(); never held across frames + // so the game can resize/recreate the swapchain freely + ID3D12Resource *_currentBackBuffer = nullptr; + public: bool Init(const Framework::Graphics::RendererConfiguration &opts) override; void Shutdown() override; @@ -52,6 +55,16 @@ namespace Framework::Graphics { void End(); int NumFramesInFlight() const; + // Repoint after the game recreates its swapchain (fullscreen / mode + // toggles). Assumes the same buffer count as Init — Begin() guards against + // a larger one rather than rebuilding the frame state. + IDXGISwapChain3 *GetSwapChain() const { + return _swapChain; + } + void SetSwapChain(IDXGISwapChain3 *swapChain) { + _swapChain = swapChain; + } + // Bounded, shader-visible SRV slot pool shared with ImGui's heap (so handles // double as ImTextureID). AllocateSRVSlot returns -1 when exhausted; the // getters return a null handle for any out-of-range slot.