From 8182389091126d97ac6af9bc0cdda4d1dec56602 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Wed, 24 Jun 2026 01:08:47 +0200 Subject: [PATCH] Detect and surface host device loss instead of spinning silently D3D11Window ignored the HRESULTs from Present, ResizeBuffers, and CreateRenderTargetView, so a host-side TDR / driver reset / GPU hang left the render loop presenting to a dead device forever with no error. Now note_device_loss() inspects those HRESULTs; on DXGI_ERROR_DEVICE_REMOVED/RESET it captures GetDeviceRemovedReason() and sets device_lost(). The main loop checks it after render_frame, shows a MessageBox with the reason, and stops cleanly. Per the agreed scope this is detect-surface-halt, not full device re-creation (which would have to re-init ImGui + the capture pipeline) -- that's future work. Not unit-testable (TDR isn't deterministically reproducible); fixed by inspection. Co-Authored-By: Claude Opus 4.8 --- README.md | 3 --- host/src/d3d11_window.cpp | 27 ++++++++++++++++++++++++--- host/src/d3d11_window.hpp | 18 ++++++++++++++++++ host/src/main.cpp | 14 ++++++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bf13360..e9bfd29 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,6 @@ From an in-depth review pass. Each item is fixed test-first (a failing test, the as its own commit; "verify" items are confirmed real before any change, and dropped if not. Confirmed bugs: -- **Host device loss unhandled** — `Present`/`ResizeBuffers`/`CreateRenderTargetView` HRESULTs are - ignored and a removed/reset device spins silently. Detect `DEVICE_REMOVED/RESET`, log - `GetDeviceRemovedReason`, surface it, and halt the render loop cleanly. - **Keyed-mutex `WAIT_ABANDONED` not handled** — `shared_texture` treats it as "skip", so a mirror never recovers after a host crash + reconnect. Treat it as acquired (copy + release). diff --git a/host/src/d3d11_window.cpp b/host/src/d3d11_window.cpp index d6c2762..07bc39b 100644 --- a/host/src/d3d11_window.cpp +++ b/host/src/d3d11_window.cpp @@ -169,12 +169,28 @@ bool D3D11Window::create_device() return true; } +bool D3D11Window::note_device_loss(HRESULT hr) +{ + if (hr != DXGI_ERROR_DEVICE_REMOVED && hr != DXGI_ERROR_DEVICE_RESET) + { + return false; + } + // GetDeviceRemovedReason gives the specific cause (HUNG / driver internal / removed); a plain + // RESET may report S_OK there, so fall back to the originating error in that case. + const HRESULT reason = device_ ? device_->GetDeviceRemovedReason() : hr; + device_lost_reason_ = (reason != S_OK) ? reason : hr; + device_lost_ = true; + return true; +} + void D3D11Window::create_render_target() { ComPtr back_buffer; if (SUCCEEDED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back_buffer.GetAddressOf())))) { - device_->CreateRenderTargetView(back_buffer.Get(), nullptr, rtv_.ReleaseAndGetAddressOf()); + const HRESULT hr = + device_->CreateRenderTargetView(back_buffer.Get(), nullptr, rtv_.ReleaseAndGetAddressOf()); + note_device_loss(hr); // a removed device surfaces here too; the render loop checks device_lost() } } @@ -190,7 +206,11 @@ void D3D11Window::handle_resize(UINT width, UINT height) return; } release_render_target(); - swap_chain_->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0); + const HRESULT hr = swap_chain_->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0); + if (note_device_loss(hr)) + { + return; // device gone; the render loop will see device_lost() and stop + } create_render_target(); } @@ -238,7 +258,8 @@ void D3D11Window::render_frame(const RenderCallback& render, UINT sync_interval) // sync_interval 1 (default) vsyncs to the monitor; 0 presents immediately so the // caller can pace the flip itself (frame-sync to the game's published frames). - swap_chain_->Present(sync_interval, 0); + const HRESULT hr = swap_chain_->Present(sync_interval, 0); + note_device_loss(hr); // a TDR/driver reset on the host surfaces here; the render loop halts on it } void D3D11Window::request_screenshot(std::wstring path) diff --git a/host/src/d3d11_window.hpp b/host/src/d3d11_window.hpp index 18e9c0e..d9752e3 100644 --- a/host/src/d3d11_window.hpp +++ b/host/src/d3d11_window.hpp @@ -47,6 +47,19 @@ public: // the result (so a confirmation shows once); otherwise returns an empty string. [[nodiscard]] std::wstring take_screenshot_result(); + // True once Present/ResizeBuffers reported DXGI_ERROR_DEVICE_REMOVED/RESET (a host-side TDR, + // driver reset, or GPU hang). The render loop is expected to stop and surface the error rather + // than spin forever on a dead device; full device re-creation is intentionally not attempted. + [[nodiscard]] bool device_lost() const + { + return device_lost_; + } + // The GetDeviceRemovedReason() HRESULT (or the originating error) when device_lost() is true. + [[nodiscard]] HRESULT device_lost_reason() const + { + return device_lost_reason_; + } + [[nodiscard]] HWND hwnd() const { return hwnd_; @@ -68,6 +81,9 @@ private: void release_render_target(); void handle_resize(UINT width, UINT height); bool save_backbuffer_png(const std::wstring& path); + // If `hr` is DEVICE_REMOVED/RESET, capture the removed reason and set device_lost_; returns true + // when the device is now lost so the caller can bail out of whatever it was doing. + bool note_device_loss(HRESULT hr); HWND hwnd_ = nullptr; std::wstring pending_screenshot_; // set by request_screenshot, consumed in render_frame @@ -75,6 +91,8 @@ private: bool resize_pending_ = false; UINT resize_width_ = 0; UINT resize_height_ = 0; + bool device_lost_ = false; + HRESULT device_lost_reason_ = S_OK; Microsoft::WRL::ComPtr device_; Microsoft::WRL::ComPtr context_; diff --git a/host/src/main.cpp b/host/src/main.cpp index 3958ccc..78e0d36 100644 --- a/host/src/main.cpp +++ b/host/src/main.cpp @@ -504,6 +504,20 @@ int run() }, sync_interval); + // A host-side TDR / driver reset / GPU hang surfaces as a lost device on Present. We don't + // attempt to recreate the device (it would have to re-init ImGui + the capture pipeline); + // surface it and stop cleanly rather than spin forever rendering nothing. + if (window.device_lost()) + { + wchar_t msg[320]; + swprintf_s(msg, + L"The graphics device was lost (0x%08lX) -- a driver reset, GPU hang, or TDR on " + L"this PC.\n\nThe mirror can't continue; please restart CoopAllTheThings.", + static_cast(window.device_lost_reason())); + MessageBoxW(window.hwnd(), msg, L"CoopAllTheThings -- graphics device lost", MB_ICONERROR | MB_OK); + break; + } + // render_frame saves a pending F10 screenshot just before Present; pick up the // result here so next frame shows the confirmation toast (kept out of the shot). if (std::wstring shot = window.take_screenshot_result(); !shot.empty())