diff --git a/src/d3d_helpers.cpp b/src/d3d_helpers.cpp index 6a1666a..2a27128 100644 --- a/src/d3d_helpers.cpp +++ b/src/d3d_helpers.cpp @@ -1,61 +1,120 @@ #include "d3d_helpers.h" + +#include +#include + #include +#include +#include -static void GetMonitorRefreshRate(HWND hwnd, UINT& numerator, UINT& denominator) +namespace { - numerator = 60; - denominator = 1; - HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - MONITORINFOEXW mi{}; - mi.cbSize = sizeof(mi); - if (!GetMonitorInfoW(mon, &mi)) - return; - DEVMODEW dm{}; - dm.dmSize = sizeof(dm); - if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) == 0) - return; - numerator = dm.dmDisplayFrequency; - denominator = 1; + const char* SwapEffectName(DXGI_SWAP_EFFECT effect) + { + switch (effect) + { + case DXGI_SWAP_EFFECT_FLIP_DISCARD: return "FLIP_DISCARD"; + case DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL: return "FLIP_SEQUENTIAL"; + case DXGI_SWAP_EFFECT_DISCARD: return "DISCARD"; + default: return "unknown"; + } + } + + void LogD3d(const std::string& message) + { + OutputDebugStringA(message.c_str()); + } } -bool D3DContext::Create(const HWND hwnd, const int refreshRatePreferred) +bool D3DContext::Create(const HWND hwnd) { - UINT num = 60, den = 1; - if (refreshRatePreferred == 0) - GetMonitorRefreshRate(hwnd, num, den); - else - num = static_cast(refreshRatePreferred); - + // Flip model (Windows 8+) is tried first; legacy DISCARD remains the fallback. + // A non-zero refresh rate makes FLIP_DISCARD and FLIP_SEQUENTIAL creation fail, + // and 0/0 is also the correct windowed value for the DISCARD fallback. + // ALLOW_MODE_SWITCH is omitted because the app never enters exclusive fullscreen + // and ResizeBuffers is called with flags 0. DXGI_SWAP_CHAIN_DESC sd{}; sd.BufferCount = 2; + sd.BufferDesc.Width = 0; + sd.BufferDesc.Height = 0; sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - sd.BufferDesc.RefreshRate.Numerator = num; - sd.BufferDesc.RefreshRate.Denominator = den; - sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; + sd.BufferDesc.RefreshRate.Numerator = 0; + sd.BufferDesc.RefreshRate.Denominator = 0; + sd.Flags = 0; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.OutputWindow = hwnd; sd.SampleDesc.Count = 1; + sd.SampleDesc.Quality = 0; sd.Windowed = TRUE; - sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + constexpr std::array effects = { + DXGI_SWAP_EFFECT_FLIP_DISCARD, + DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, + DXGI_SWAP_EFFECT_DISCARD, + }; constexpr std::array levels = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0}; - D3D_FEATURE_LEVEL obtained; + constexpr UINT levelCount = static_cast(levels.size()); + + HRESULT hr = E_FAIL; + DXGI_SWAP_EFFECT chosen = DXGI_SWAP_EFFECT_DISCARD; + for (const DXGI_SWAP_EFFECT effect : effects) + { + sd.SwapEffect = effect; + D3D_FEATURE_LEVEL obtained = D3D_FEATURE_LEVEL_11_0; + hr = D3D11CreateDeviceAndSwapChain( + nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, + levels.data(), levelCount, D3D11_SDK_VERSION, + &sd, swapChain.put(), device.put(), &obtained, deviceCtx.put()); + if (hr == DXGI_ERROR_UNSUPPORTED) + { + hr = D3D11CreateDeviceAndSwapChain( + nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0, + levels.data(), levelCount, D3D11_SDK_VERSION, + &sd, swapChain.put(), device.put(), &obtained, deviceCtx.put()); + } + if (hr == S_OK) + { + chosen = effect; + break; + } + + // A failed call should already null the outputs; drop anything leftover before the next try. + Cleanup(); + } + + if (hr != S_OK) + { + LogD3d(std::format( + "D3D11CreateDeviceAndSwapChain failed: {:#010x}\n", + static_cast(hr))); + return false; + } - if (D3D11CreateDeviceAndSwapChain( - nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, - levels.data(), levels.size(), D3D11_SDK_VERSION, - &sd, swapChain.put(), device.put(), &obtained, deviceCtx.put()) != S_OK) + hr = CreateRTV(); + if (FAILED(hr)) + { + LogD3d(std::format( + "CreateRenderTargetView failed: {:#010x}\n", + static_cast(hr))); + Cleanup(); return false; + } - CreateRTV(); + LogD3d(std::format("Created D3D11 swap chain with SwapEffect {}\n", SwapEffectName(chosen))); return true; } -void D3DContext::CreateRTV() +HRESULT D3DContext::CreateRTV() { + rtv.reset(); + if (!swapChain || !device) + return E_FAIL; + wil::com_ptr back; - swapChain->GetBuffer(0, IID_PPV_ARGS(back.put())); - device->CreateRenderTargetView(back.get(), nullptr, rtv.put()); + const HRESULT bufferHr = swapChain->GetBuffer(0, IID_PPV_ARGS(back.put())); + if (FAILED(bufferHr)) + return bufferHr; + return device->CreateRenderTargetView(back.get(), nullptr, rtv.put()); } void D3DContext::CleanupRTV() @@ -71,33 +130,21 @@ void D3DContext::Cleanup() device.reset(); } -void D3DContext::Resize(LPARAM lParam) +HRESULT D3DContext::Resize(const UINT width, const UINT height) { - CleanupRTV(); - swapChain->ResizeBuffers(0, - LOWORD(lParam), - HIWORD(lParam), - DXGI_FORMAT_UNKNOWN, 0); - CreateRTV(); -} + if (!swapChain || width == 0 || height == 0) + return E_INVALIDARG; -void D3DContext::Present(const bool vsync) const -{ - swapChain->Present(vsync ? 1 : 0, 0); + CleanupRTV(); + const HRESULT hr = swapChain->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0); + if (FAILED(hr)) + return hr; + return CreateRTV(); } -void D3DContext::SetRefreshRate(const int numerator, const int denominator) +HRESULT D3DContext::Present(const bool vsync, const UINT flags) const { - DXGI_SWAP_CHAIN_DESC desc{}; - if (swapChain->GetDesc(&desc) != S_OK) - return; - DXGI_MODE_DESC mode{}; - mode.Width = desc.BufferDesc.Width; - mode.Height = desc.BufferDesc.Height; - mode.RefreshRate.Numerator = numerator; - mode.RefreshRate.Denominator = denominator; - mode.Format = desc.BufferDesc.Format; - mode.ScanlineOrdering = desc.BufferDesc.ScanlineOrdering; - mode.Scaling = desc.BufferDesc.Scaling; - swapChain->ResizeTarget(&mode); + if (!swapChain) + return E_FAIL; + return swapChain->Present(vsync ? 1u : 0u, flags); } diff --git a/src/d3d_helpers.h b/src/d3d_helpers.h index 0c293b1..d41bab2 100644 --- a/src/d3d_helpers.h +++ b/src/d3d_helpers.h @@ -10,11 +10,12 @@ struct D3DContext wil::com_ptr swapChain; wil::com_ptr rtv; - [[nodiscard]] bool Create(HWND hwnd, int refreshRatePreferred = 60); - void CreateRTV(); + // Windowed creation uses refresh 0/0 so flip-model swap chains are accepted. + [[nodiscard]] bool Create(HWND hwnd); + [[nodiscard]] HRESULT CreateRTV(); void CleanupRTV(); void Cleanup(); - void Resize(LPARAM lParam); - void Present(bool vsync) const; - void SetRefreshRate(int numerator, int denominator); + [[nodiscard]] HRESULT Resize(UINT width, UINT height); + // flags may be DXGI_PRESENT_TEST; pass vsync false for that test. + [[nodiscard]] HRESULT Present(bool vsync, UINT flags = 0) const; }; diff --git a/src/main.cpp b/src/main.cpp index 186ddf9..11e179b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -234,6 +235,13 @@ static void GetMonitorRefreshRate(HWND hwnd, int& numerator, int& denominator) static D3DContext g_d3d; static std::vector>* g_backends = nullptr; +static bool g_minimized = false; +static bool g_swapChainOccluded = false; +static UINT g_resizeWidth = 0; +static UINT g_resizeHeight = 0; +// Extra frames after input so hover, clicks, and modal transitions settle. +static int g_framesToRender = 0; +static bool g_imguiDx11Ready = false; enum class SystemDialog : std::uint8_t { @@ -455,8 +463,14 @@ static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) } return 0; case WM_SIZE: - if (g_d3d.device && wParam != SIZE_MINIMIZED) - g_d3d.Resize(lParam); + if (wParam == SIZE_MINIMIZED) + { + g_minimized = true; + return 0; + } + g_minimized = false; + g_resizeWidth = LOWORD(lParam); + g_resizeHeight = HIWORD(lParam); return 0; case WM_SYSCOMMAND: if ((wParam & 0xfff0) == IDM_ABOUT) @@ -492,6 +506,61 @@ static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) return DefWindowProc(hWnd, msg, wParam, lParam); } +static bool IsDeviceLostHr(const HRESULT hr) +{ + return hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET; +} + +static void LogSwapChainStatus(const HRESULT hr) +{ + if (hr == S_OK) + return; + + static HRESULT lastLogged = S_OK; + if (hr == lastLogged) + return; + lastLogged = hr; + + OutputDebugStringA(std::format( + "Swap chain Present/Resize failed: {:#010x}\n", + static_cast(hr)).c_str()); +} + +/** + * @brief Frame interval for the interactive render pace, in milliseconds. + * + * Uses the saved refresh-rate preference. Zero means the current monitor rate. + * The swap chain itself stays at 0/0; this only limits how often the loop wakes. + */ +static DWORD PreferredFrameIntervalMs(const HWND hwnd) +{ + int hz = g_prefs.refreshRate; + if (hz <= 0) + { + int den = 1; + GetMonitorRefreshRate(hwnd, hz, den); + } + if (hz < 1) + hz = 60; + return static_cast(std::max(1, 1000 / hz)); +} + +static bool AnyControllerConnected() +{ + if (!g_backends) + return false; + + for (const auto& backend : *g_backends) + { + for (int slot = 0; slot < backend->GetMaxSlots(); ++slot) + { + if (backend->GetState(slot).connected) + return true; + } + } + return false; +} + /** * @brief Initialize application subsystems, run the main event/render loop, and perform shutdown. * @@ -501,7 +570,7 @@ static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) * @param hPrevInstance Reserved; typically unused. * @param lpCmdLine Command line for the application as a Unicode string. * @param nCmdShow Controls how the window is to be shown. - * @return int `0` on normal exit, non-zero on initialization failure (for example, Direct3D creation failure). + * @return int `0` on normal exit, non-zero on initialization failure or when a lost D3D device cannot be recreated. */ int APIENTRY wWinMain( _In_ HINSTANCE hInstance, @@ -544,7 +613,7 @@ int APIENTRY wWinMain( AppendMenuW(sysMenu, MF_SEPARATOR, 0, nullptr); AppendMenuW(sysMenu, MF_STRING, IDM_ABOUT, L"About..."); AppendMenuW(sysMenu, MF_STRING, IDM_PREFERENCES, L"Preferences..."); - if (!g_d3d.Create(hwnd, g_prefs.refreshRate)) + if (!g_d3d.Create(hwnd)) { g_d3d.Cleanup(); UnregisterClass(wc.lpszClassName, wc.hInstance); @@ -569,12 +638,28 @@ int APIENTRY wWinMain( io.Fonts->AddFontDefault(&fc); } - ImGui_ImplWin32_Init(hwnd); - ImGui_ImplDX11_Init(g_d3d.device.get(), g_d3d.deviceCtx.get()); + if (!ImGui_ImplWin32_Init(hwnd)) + { + ImGui::DestroyContext(); + g_d3d.Cleanup(); + DestroyWindow(hwnd); + UnregisterClass(wc.lpszClassName, wc.hInstance); + return 1; + } + if (!ImGui_ImplDX11_Init(g_d3d.device.get(), g_d3d.deviceCtx.get())) + { + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + g_d3d.Cleanup(); + DestroyWindow(hwnd); + UnregisterClass(wc.lpszClassName, wc.hInstance); + return 1; + } + g_imguiDx11Ready = true; wil::com_ptr controllerTextureXbox; wil::com_ptr controllerTextureDualSense; - { + auto loadControllerTextures = [&]() { auto loadBody = [&](int id) -> wil::com_ptr { HRSRC hrsrc = FindResourceW(hInstance, MAKEINTRESOURCEW(id), RT_RCDATA); if (!hrsrc) return {}; @@ -587,7 +672,52 @@ int APIENTRY wWinMain( }; controllerTextureXbox = loadBody(IDR_XBOX_BODY); controllerTextureDualSense = loadBody(IDR_DUALSENSE_BODY); - } + }; + loadControllerTextures(); + + auto recreateDevice = [&]() -> bool { + const HRESULT reason = g_d3d.device ? g_d3d.device->GetDeviceRemovedReason() : E_FAIL; + OutputDebugStringA(std::format( + "D3D device lost (GetDeviceRemovedReason={:#010x}), recreating\n", + static_cast(reason)).c_str()); + + controllerTextureXbox.reset(); + controllerTextureDualSense.reset(); + if (g_imguiDx11Ready) + { + ImGui_ImplDX11_Shutdown(); + g_imguiDx11Ready = false; + } + g_d3d.Cleanup(); + + if (!g_d3d.Create(hwnd)) + { + OutputDebugStringA("Failed to recreate D3D device after device loss\n"); + return false; + } + + if (!ImGui_ImplDX11_Init(g_d3d.device.get(), g_d3d.deviceCtx.get())) + { + OutputDebugStringA("ImGui_ImplDX11_Init failed after device recreation\n"); + g_d3d.Cleanup(); + return false; + } + g_imguiDx11Ready = true; + loadControllerTextures(); + g_resizeWidth = 0; + g_resizeHeight = 0; + g_swapChainOccluded = false; + g_framesToRender = 3; + return true; + }; + + auto handleDeviceLost = [&](const HRESULT hr) -> bool { + LogSwapChainStatus(hr); + if (IsDeviceLostHr(hr)) + return recreateDevice(); + // S_OK and DXGI status codes (for example OCCLUDED) continue; other failures are fatal. + return SUCCEEDED(hr); + }; std::vector> backends; backends.push_back(std::make_unique()); @@ -622,13 +752,92 @@ int APIENTRY wWinMain( constexpr float clearColor[4] = {0.06f, 0.06f, 0.07f, 1.0f}; - MSG msg{}; - while (msg.message != WM_QUIT) + using Clock = std::chrono::steady_clock; + auto lastPresent = Clock::now(); + DWORD timeoutMs = 0; + int exitCode = 0; + bool done = false; + while (!done) { - if (PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) + // MWMO_INPUTAVAILABLE is required because PeekMessage marks messages as seen; + // without it a leftover QS_* bit would make the next wait return immediately. + const DWORD waitResult = MsgWaitForMultipleObjectsEx( + 0, nullptr, timeoutMs, QS_ALLINPUT, MWMO_INPUTAVAILABLE); + if (waitResult == WAIT_FAILED) + { + OutputDebugStringA(std::format( + "MsgWaitForMultipleObjectsEx failed: {}\n", GetLastError()).c_str()); + Sleep(10); + } + + MSG msg{}; + bool dispatched = false; + while (PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); + dispatched = true; + if (msg.message == WM_QUIT) + { + done = true; + exitCode = static_cast(msg.wParam); + } + } + if (dispatched) + g_framesToRender = 3; + if (done) + break; + + if (g_minimized) + { + for (auto& b : backends) + b->Poll(); + timeoutMs = 250; + continue; + } + + // Legacy blt-model swap chains report occlusion; flip model uses g_minimized instead. + if (g_swapChainOccluded && g_d3d.swapChain) + { + const HRESULT testHr = g_d3d.Present(false, DXGI_PRESENT_TEST); + if (!handleDeviceLost(testHr)) + { + PostQuitMessage(1); + timeoutMs = 0; + continue; + } + if (testHr == DXGI_STATUS_OCCLUDED) + { + for (auto& b : backends) + b->Poll(); + timeoutMs = 250; + continue; + } + } + g_swapChainOccluded = false; + + if (g_resizeWidth != 0 || g_resizeHeight != 0) + { + const UINT width = g_resizeWidth; + const UINT height = g_resizeHeight; + g_resizeWidth = 0; + g_resizeHeight = 0; + if (width != 0 && height != 0 && g_d3d.swapChain) + { + const HRESULT resizeHr = g_d3d.Resize(width, height); + if (!handleDeviceLost(resizeHr)) + { + PostQuitMessage(1); + timeoutMs = 0; + continue; + } + } + } + + if (!g_d3d.rtv || !g_d3d.deviceCtx) + { + PostQuitMessage(1); + timeoutMs = 0; continue; } @@ -928,12 +1137,6 @@ int APIENTRY wWinMain( ImGui::Spacing(); if (ImGui::Button("OK", ImVec2(80, 0))) { - int num = 60, den = 1; - if (editRefreshRate == 0) - GetMonitorRefreshRate(hwnd, num, den); - else - num = editRefreshRate; - g_d3d.SetRefreshRate(num, den); g_prefs.refreshRate = editRefreshRate; g_prefs.vsync = editVsync; SaveConfig(g_prefs); @@ -964,7 +1167,39 @@ int APIENTRY wWinMain( g_d3d.deviceCtx->OMSetRenderTargets(1, &rtv, nullptr); g_d3d.deviceCtx->ClearRenderTargetView(rtv, clearColor); ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); - g_d3d.Present(g_prefs.vsync); + const HRESULT presentHr = g_d3d.Present(g_prefs.vsync); + lastPresent = Clock::now(); + if (!handleDeviceLost(presentHr)) + { + PostQuitMessage(1); + timeoutMs = 0; + continue; + } + if (IsDeviceLostHr(presentHr)) + { + timeoutMs = 0; + continue; + } + g_swapChainOccluded = (presentHr == DXGI_STATUS_OCCLUDED); + + const bool controllersConnected = AnyControllerConnected(); + if (g_minimized || g_swapChainOccluded) + { + timeoutMs = 250; + } + else if (g_framesToRender > 0 || controllersConnected) + { + if (g_framesToRender > 0) + --g_framesToRender; + const auto elapsedMs = std::chrono::duration_cast( + Clock::now() - lastPresent).count(); + const auto frameMs = static_cast(PreferredFrameIntervalMs(hwnd)); + timeoutMs = elapsedMs >= frameMs ? 0 : static_cast(frameMs - elapsedMs); + } + else + { + timeoutMs = 100; + } } g_updateCheckSession.reset(); @@ -972,12 +1207,16 @@ int APIENTRY wWinMain( g_backends = nullptr; - ImGui_ImplDX11_Shutdown(); + controllerTextureXbox.reset(); + controllerTextureDualSense.reset(); + if (g_imguiDx11Ready) + ImGui_ImplDX11_Shutdown(); + g_imguiDx11Ready = false; ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); g_d3d.Cleanup(); DestroyWindow(hwnd); UnregisterClass(wc.lpszClassName, wc.hInstance); - return 0; + return exitCode; }