EDR-7560 - Throttle rendering when the window is occluded, not just minimized - #34
EDR-7560 - Throttle rendering when the window is occluded, not just minimized#34Schogol wants to merge 1 commit into
Conversation
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.
|
MacOS already has occlusion-detection in Tr2MainWindow_MacOS.mm |
…rbonengine#34) Co-authored-by: rebecca <1+rebecca@noreply.localhost>
|
Thanks for opening this! We've seen it and notified the relevant team. One of our engineers will take a look soon. We appreciate you taking the time to contribute, and we'll follow up here once someone has had a chance to review. Thanks for your patience in the meantime. |
|
Hi, @Schogol, sorry for the very late reply. We're still working on our open source processes here. While the feature is nice, we're worried that only checking 5 points is not going to be accurate enough and that it might be inadvertently triggered for windows that are only partially occluded. I think we would be able to accept this if it was able to more accurately determine occlusion. My suggestion would be to do semi-random sampling over time, and only determine that the window is occluded if no samples pass for a certain number of ticks. A well-distributed sequence of samples can be generated using the plastic ratio described here: http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/, although that link seems to be dead at the moment... Initialization: const float INVERSE_PHI = 1.0f / 1.32471795724474602596f;
float g1 = 0.5f;
float g2 = 0.5f;To get the next position in the sequence: //add phi and phi^2
g1 += INVERSE_PHI;
g2 += INVERSE_PHI*INVERSE_PHI;
//reduce down to the [0, 1) range, aka fract()
g1 -= floor(g1);
g2 -= floor(g2);
//scale by window size and truncate to int
int occlusionX = (int)(g1 * windowWidth);
int occlusionY = (int)(g2 * windowHeight);If all previous samples (say 32+ or so) over some time have been determined to be occluded, it should be safe to assume that the window as a whole is occluded. I would also like to suggest that you swap the nested loops. Currently, you loop through the points, then call EnumWindows() for each of the points to go through the windows. It would be more efficient to call EnumWindows() once, then check all sample points against each window, which would also allow you to early out sooner. Since each point would be so cheap to test that way, it would be possible to test more than 5 points each tick as well. |
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:
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:
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.