From 9bb85a8e023ddc1bd00788990f4f27b87d63399a Mon Sep 17 00:00:00 2001 From: Schogol Date: Sat, 25 Jul 2026 22:29:24 +0200 Subject: [PATCH] Throttle rendering when the window is occluded, not just minimized The WINDOW_HIDDEN throttle (TriDevice::Throttle: 20ms sleep + ShouldSkipFrame, which renders ~1 frame in 50) is only engaged for a minimized window (WM_SIZE / SIZE_MINIMIZED). A window that is fully *covered* by another window is not minimized, so it falls through to the weaker WINDOW_OUT_OF_FOCUS path (10ms sleep, no frame-skip) and keeps rendering the full scene for a screen nobody can see. Measured in-game with a rendered space-view on my machine (NVIDIA GeForce RTX 2080, 60 Hz display, V-Sync on), with the window fully covered by another window: - DX11: ~64 fps, ~2.3 ms/frame of active rendering - DX12: ~72 fps, ~3.0 ms/frame A focused, visible client renders at 60 fps (V-Sync-capped). So with V-Sync on at 60 Hz the client spends MORE GPU on rendering while the window is not visible than while it is - a covered window bypasses the V-Sync cap and renders above it, so the throttle is effectively backwards for covered windows. A minimized window, by contrast, correctly drops to ~1 rendered frame/sec (~0.5 ms/frame). DXGI cannot supply the missing signal: DXGI_STATUS_OCCLUDED is not reported for flip-model swapchains (DXGI_SWAP_EFFECT_FLIP_DISCARD, mandatory on D3D12) under DWM composition - both Present() and Present(0, DXGI_PRESENT_TEST) return S_OK while the window is fully covered or minimized. Occlusion is therefore detected from window geometry. Tr2MainWindow now polls occlusion at 4 Hz (WM_TIMER) and drives the existing WINDOW_HIDDEN throttle: - IsWindowOccluded() samples 5 points on the window (centre + 4 inset corners) and, at each, finds the topmost opaque top-level window; the window is hidden iff every point is covered, or it is minimized. See-through overlays (WS_EX_LAYERED / _TRANSPARENT / _TOOLWINDOW, e.g. the Alt+Tab switcher) are skipped so they are not counted as occluders. - Focus-gated: a focused window is on top and cannot be occluded, so it skips the scan and pays only a HasFocus() check; the EnumWindows scan runs only on a backgrounded client. - Started from window creation, so a client launched straight into the background is throttled immediately. Result: a covered window now takes the same WINDOW_HIDDEN path as a minimized one, dropping active rendering to ~1 frame/sec while hidden, with no change when focused or visible-but-unfocused. --- trinity/UI/Tr2MainWindow_Windows.cpp | 72 ++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/trinity/UI/Tr2MainWindow_Windows.cpp b/trinity/UI/Tr2MainWindow_Windows.cpp index e4165cf1b..d7c1b76df 100644 --- a/trinity/UI/Tr2MainWindow_Windows.cpp +++ b/trinity/UI/Tr2MainWindow_Windows.cpp @@ -19,6 +19,63 @@ CCP_STATS_DECLARED_ELSEWHERE( frameTime ); namespace { +// Occlusion detection for the WINDOW_HIDDEN throttle. DXGI_STATUS_OCCLUDED is not reported for flip-model +// swapchains (mandatory on DX12) under DWM composition, so we detect "covered by another window" from Win32 +// window geometry: sample 5 points (centre + 4 inset corners); at each, find the topmost OPAQUE window +// (skipping see-through overlays like the Alt+Tab switcher). Hidden iff every point is covered, or minimized. +const UINT_PTR OCCLUSION_TIMER_ID = 0xE7E0; + +bool PointTopIsWindow( HWND target, int px, int py ) +{ + struct Ctx + { + int px, py; + HWND result; + } ctx = { px, py, nullptr }; + ::EnumWindows( + []( HWND hwnd, LPARAM lp ) -> BOOL { + Ctx* c = reinterpret_cast( lp ); + if( !::IsWindowVisible( hwnd ) || ::IsIconic( hwnd ) ) + return TRUE; + if( ::GetWindowLongW( hwnd, GWL_EXSTYLE ) & ( WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW ) ) + return TRUE; + RECT r; + if( !::GetWindowRect( hwnd, &r ) ) + return TRUE; + if( c->px >= r.left && c->px < r.right && c->py >= r.top && c->py < r.bottom ) + { + c->result = hwnd; + return FALSE; + } + return TRUE; + }, + reinterpret_cast( &ctx ) ); + return ctx.result == target; +} + +bool IsWindowOccluded( HWND hwnd ) +{ + if( !hwnd ) + return false; + if( ::IsIconic( hwnd ) ) + return true; + RECT r; + if( !::GetWindowRect( hwnd, &r ) ) + return false; + const int w = r.right - r.left; + const int h = r.bottom - r.top; + if( w <= 0 || h <= 0 ) + return true; + const double fx[5] = { 0.5, 0.12, 0.88, 0.12, 0.88 }; + const double fy[5] = { 0.5, 0.12, 0.12, 0.88, 0.88 }; + for( int i = 0; i < 5; ++i ) + { + if( PointTopIsWindow( hwnd, r.left + int( fx[i] * w ), r.top + int( fy[i] * h ) ) ) + return false; + } + return true; +} + struct MonitorRects { std::vector rcMonitors; @@ -123,12 +180,18 @@ void Tr2MainWindow::CreateOSWindow( Tr2MainWindowState::State& state ) } } SetPropW( m_hwnd, L"Tr2MainWindow", reinterpret_cast( this ) ); + + // Drive the WINDOW_HIDDEN throttle from actual window occlusion (see IsWindowOccluded above). Polled + // rather than event-driven because "covered by another window" produces no window message. Runs from + // creation so a client launched straight into the background is throttled immediately. + ::SetTimer( m_hwnd, OCCLUSION_TIMER_ID, 250, nullptr ); } void Tr2MainWindow::DestroyOSWindow() { if( m_hwnd ) { + ::KillTimer( m_hwnd, OCCLUSION_TIMER_ID ); ::DestroyWindow( m_hwnd ); m_hwnd = nullptr; } @@ -602,6 +665,15 @@ LRESULT Tr2MainWindow::WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara return HTCLIENT; } break; + case WM_TIMER: + if( wParam == OCCLUSION_TIMER_ID && gTriDev ) + { + // A focused window is on top and cannot be occluded, so skip the window scan when we have + // focus (near-zero cost on the active client); otherwise check whether we are covered/minimized. + const bool occluded = HasFocus() ? false : IsWindowOccluded( m_hwnd ); + gTriDev->SetThrottling( TriDevice::WINDOW_HIDDEN, occluded ); + } + break; case WM_ACTIVATE: if( wParam == WA_ACTIVE || wParam == WA_CLICKACTIVE ) {