Skip to content

EDR-7560 - Throttle rendering when the window is occluded, not just minimized - #34

Open
Schogol wants to merge 1 commit into
carbonengine:mainfrom
Schogol:fix/throttle-when-occluded
Open

EDR-7560 - Throttle rendering when the window is occluded, not just minimized#34
Schogol wants to merge 1 commit into
carbonengine:mainfrom
Schogol:fix/throttle-when-occluded

Conversation

@Schogol

@Schogol Schogol commented Jul 25, 2026

Copy link
Copy Markdown

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 (only possible in Window mode), 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.

Disclaimer:
This issue was discovered by me on my local machine and I used Claude to investigate the cause and help me create this fix. I was able to build it fine against Python 3.12 but unable to test on a live client.
I did create a TrinityAL probe which confirmed the occlusion-detection to be working as expected.

This fix should be most noticeable by people running multiple clients on the same machine and lower the overall GPU usage.

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.
@Schogol

Schogol commented Jul 25, 2026

Copy link
Copy Markdown
Author

MacOS already has occlusion-detection in Tr2MainWindow_MacOS.mm

void Tr2MainWindow::OnWindowOcclusionChanged_MacOS()
{
	auto window = (NSWindow*)GetWindowID();
	auto visible = ( window.occlusionState & NSWindowOcclusionStateVisible ) != 0;
	gTriDev->SetThrottling( TriDevice::WINDOW_HIDDEN, !visible );
}

@Schogol Schogol changed the title Throttle rendering when the window is occluded, not just minimized EDR-7560 - Throttle rendering when the window is occluded, not just minimized Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant