From 8a9dbb39854a35cdd54e6ab4f2ea0ddf7ea1275f Mon Sep 17 00:00:00 2001 From: Schogol Date: Sat, 25 Jul 2026 22:29:24 +0200 Subject: [PATCH 1/2] 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 ) { From 3022a113d2a9b92b4801f644c0ffb6cc2564e26c Mon Sep 17 00:00:00 2001 From: Schogol Date: Fri, 11 Sep 2026 00:09:03 +0200 Subject: [PATCH 2/2] Address review: accumulate quasirandom occlusion samples before throttling - Sample positions come from the R2 quasirandom sequence (plastic ratio) instead of 5 fixed points. The window counts as hidden only after 64 consecutive covered samples (8 per 250ms tick, ~2s fully covered), and a single visible sample resets the run, so a partially visible window cannot be misdetected from one unlucky batch of points. - One EnumWindows pass per tick collects the rects of opaque windows above us in Z-order (enumeration stops at our own window); sample points are then tested against the rect list with an early-out. - Skip DWM-cloaked windows (suspended UWP apps keep a WS_VISIBLE top-level window that draws nothing) so they cannot count as occluders. - Points outside every monitor still count as covered, preserving the off-screen behaviour of the previous topmost-window-at-point check. - Minimized remains an immediate short-circuit so the poll result never fights the WM_SIZE/SIZE_MINIMIZED handler. --- trinity/UI/Tr2MainWindow_Windows.cpp | 136 +++++++++++++++++++++------ 1 file changed, 105 insertions(+), 31 deletions(-) diff --git a/trinity/UI/Tr2MainWindow_Windows.cpp b/trinity/UI/Tr2MainWindow_Windows.cpp index d7c1b76df..2599fcdd7 100644 --- a/trinity/UI/Tr2MainWindow_Windows.cpp +++ b/trinity/UI/Tr2MainWindow_Windows.cpp @@ -9,6 +9,11 @@ #include "Tr2MouseCursor.h" #include "TriDevice.h" +#include +#include + +#pragma comment( lib, "dwmapi.lib" ) + extern CcpMeanStatisticsEntry g_activeFrametimeMean; extern CcpStdDevStatisticsEntry g_activeFrametimeStdDev; @@ -20,60 +25,123 @@ 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. +// swapchains (mandatory on DX12) under DWM composition, so "covered by another window" is detected from +// Win32 window geometry. Each poll tick enumerates the opaque top-level windows above us in Z-order once, +// then tests a batch of quasirandomly placed points against their rects. The window only counts as hidden +// once every sample of OCCLUSION_SAMPLES_REQUIRED consecutive samples was covered (or it is minimized), so +// a partially visible window cannot be misdetected from one unlucky batch of points; a single visible +// sample resets the run, so un-covering the window unthrottles on the next tick. const UINT_PTR OCCLUSION_TIMER_ID = 0xE7E0; +const UINT OCCLUSION_POLL_MS = 250; +const int OCCLUSION_SAMPLES_PER_TICK = 8; +const int OCCLUSION_SAMPLES_REQUIRED = 64; // 8 fully covered ticks (~2s) before the throttle engages + +// Sample positions come from the R2 quasirandom sequence (generalised golden ratio phi2, the "plastic +// ratio"): successive points are evenly distributed over the window without aligning to window geometry. +const float INVERSE_PHI2 = 1.0f / 1.32471795724474602596f; +float g_occlusionSeqX = 0.5f; +float g_occlusionSeqY = 0.5f; +int g_occlusionCoveredSamples = 0; + +// Suspended UWP/store apps keep a WS_VISIBLE top-level window in the Z-order that draws nothing; without +// this check such a window would count as an occluder and could throttle a perfectly visible client. +bool IsWindowCloaked( HWND hwnd ) +{ + DWORD cloaked = 0; + return SUCCEEDED( ::DwmGetWindowAttribute( hwnd, DWMWA_CLOAKED, &cloaked, sizeof( cloaked ) ) ) && cloaked != 0; +} -bool PointTopIsWindow( HWND target, int px, int py ) +// One EnumWindows pass per tick: collect the rects of every opaque, uncloaked window above hwnd in +// Z-order. EnumWindows enumerates topmost first, so collection stops at hwnd - nothing below it can +// occlude it - and each sample point then costs only a few rect tests. +void CollectOccludersAbove( HWND hwnd, std::vector& rects ) { struct Ctx { - int px, py; - HWND result; - } ctx = { px, py, nullptr }; + HWND target; + std::vector* rects; + } ctx = { hwnd, &rects }; ::EnumWindows( - []( HWND hwnd, LPARAM lp ) -> BOOL { + []( HWND other, LPARAM lp ) -> BOOL { Ctx* c = reinterpret_cast( lp ); - if( !::IsWindowVisible( hwnd ) || ::IsIconic( hwnd ) ) + if( other == c->target ) + return FALSE; + if( !::IsWindowVisible( other ) || ::IsIconic( other ) ) return TRUE; - if( ::GetWindowLongW( hwnd, GWL_EXSTYLE ) & ( WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW ) ) + if( ::GetWindowLongW( other, GWL_EXSTYLE ) & ( WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW ) ) return TRUE; - RECT r; - if( !::GetWindowRect( hwnd, &r ) ) + if( IsWindowCloaked( other ) ) return TRUE; - if( c->px >= r.left && c->px < r.right && c->py >= r.top && c->py < r.bottom ) - { - c->result = hwnd; - return FALSE; - } + RECT r; + if( ::GetWindowRect( other, &r ) ) + c->rects->push_back( r ); return TRUE; }, reinterpret_cast( &ctx ) ); - return ctx.result == target; } -bool IsWindowOccluded( HWND hwnd ) +// Per-tick occlusion sample; returns whether the window should currently count as hidden. Stateful: the +// covered-sample run accumulates across ticks (see block comment above). +bool SampleWindowOcclusion( HWND hwnd ) { if( !hwnd ) + { + g_occlusionCoveredSamples = 0; return false; + } + // Minimized is unambiguous and must bypass sample accumulation: WM_SIZE/SIZE_MINIMIZED has already + // set WINDOW_HIDDEN, and returning false while the sample run is still short would clear it again. if( ::IsIconic( hwnd ) ) + { + g_occlusionCoveredSamples = 0; return true; + } RECT r; if( !::GetWindowRect( hwnd, &r ) ) + { + g_occlusionCoveredSamples = 0; return false; + } const int w = r.right - r.left; const int h = r.bottom - r.top; if( w <= 0 || h <= 0 ) + { + g_occlusionCoveredSamples = 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 ) + } + + std::vector occluders; + CollectOccludersAbove( hwnd, occluders ); + + for( int i = 0; i < OCCLUSION_SAMPLES_PER_TICK; ++i ) { - if( PointTopIsWindow( hwnd, r.left + int( fx[i] * w ), r.top + int( fy[i] * h ) ) ) + g_occlusionSeqX += INVERSE_PHI2; + g_occlusionSeqX -= std::floor( g_occlusionSeqX ); + g_occlusionSeqY += INVERSE_PHI2 * INVERSE_PHI2; + g_occlusionSeqY -= std::floor( g_occlusionSeqY ); + const POINT pt = { r.left + LONG( g_occlusionSeqX * w ), r.top + LONG( g_occlusionSeqY * h ) }; + bool covered = false; + for( const RECT& occluder : occluders ) + { + if( ::PtInRect( &occluder, pt ) ) + { + covered = true; + break; + } + } + // A point no occluder covers can still be invisible: outside every monitor (window dragged or + // stranded off-screen). Count those as covered, as the topmost-window-at-point variant did. + if( !covered && ::MonitorFromPoint( pt, MONITOR_DEFAULTTONULL ) == nullptr ) + covered = true; + if( !covered ) + { + g_occlusionCoveredSamples = 0; return false; + } } - return true; + if( g_occlusionCoveredSamples < OCCLUSION_SAMPLES_REQUIRED ) + g_occlusionCoveredSamples += OCCLUSION_SAMPLES_PER_TICK; + return g_occlusionCoveredSamples >= OCCLUSION_SAMPLES_REQUIRED; } struct MonitorRects @@ -181,10 +249,11 @@ 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 ); + // Drive the WINDOW_HIDDEN throttle from actual window occlusion (see SampleWindowOcclusion 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 gets throttled too. + g_occlusionCoveredSamples = 0; + ::SetTimer( m_hwnd, OCCLUSION_TIMER_ID, OCCLUSION_POLL_MS, nullptr ); } void Tr2MainWindow::DestroyOSWindow() @@ -668,9 +737,14 @@ LRESULT Tr2MainWindow::WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara 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 ); + // A focused window is on top and cannot be occluded, so the window scan only ever runs on a + // backgrounded client (near-zero cost on the active one). The focused path still resets the + // sample run so a later unfocus starts a fresh accumulation. + bool occluded = false; + if( HasFocus() ) + g_occlusionCoveredSamples = 0; + else + occluded = SampleWindowOcclusion( m_hwnd ); gTriDev->SetThrottling( TriDevice::WINDOW_HIDDEN, occluded ); } break;