Skip to content

VRR flicker mitigations - #2326

Open
matte-schwartz wants to merge 3 commits into
ValveSoftware:masterfrom
matte-schwartz:vrr-flicker-mitigations
Open

VRR flicker mitigations#2326
matte-schwartz wants to merge 3 commits into
ValveSoftware:masterfrom
matte-schwartz:vrr-flicker-mitigations

Conversation

@matte-schwartz

Copy link
Copy Markdown
Collaborator

At present, when VRR is enabled but Steam is idle, displays drop to the bottom of their VRR-supported range on every idle. Then, when Steam exits an idle state, the refresh rate spikes back up to its maximum value, and on panels which are prone to VRR flicker, this process causes some pretty nasty flickering while navigating the Steam UI.

Mitigate this by holding the refresh rate while the Steam UI is focused. I checked power readings and there is minimal difference by doing this compared to keeping monitors at their minimum value, but the visible flicker still completely disappears. It's targeted only while the Steam UI is focused, as we want to make sure games still interact with VRR as cleanly as possible and keep LFC functioning correctly. The last commit skips recompositing for these repeated frames and presents the last composited image again, which is what keeps the power cost minimal.

Also, clamp the cursor to active content refresh rate when it's above a minimum threshold while VRR is enabled, mimicking KWin's implementation, as it offers a better user experience than spiking to max refresh rate every time the cursor moves while VRR is active in-game.

Cursor motion sets hasRepaint, and the VRR flip path paints on every
wakeup, so moving the cursor drives the display to its maximum refresh
rate even while the focused game presents at a lower rate. The refresh
rate then oscillates between the game rate and the maximum, which flickers
visibly on some VRR panels. The cursor auto-hide has the same effect
through the overlay path, spiking the refresh rate a fixed timeout
after the last cursor motion.

Track cursor damage in a separate hasCursorRepaint flag and only let it
force a flip when no content frame was painted within the last
1/adaptive_sync_cursor_min_fps seconds, defaulting to 30fps to match
the threshold KWin uses for its delayed VRR timer and
activeWindowControlsVrrRefreshRate. A game presenting at or above that
rate paces the display alone and the cursor rides along with its
frames, while slow or static content keeps an immediately repainted
cursor with the refresh rate pinned stably at the maximum. The
auto-hide repaint is classified as cursor damage too.
The Steam UI only presents on changes, so with adaptive sync active the
panel stretches toward its minimum refresh rate whenever the screen
goes still and snaps back to maximum on the next update. Every swing
shifts the panel's effective gamma, which shows as whole-screen
brightness flicker on OLED and VA panels, and the panel starts
stretching the moment a single frame is missed. Toggling VRR_ENABLED
off for the UI instead would take the full modeset path, which blanks
many sinks.

While the Steam UI is focused and adaptive sync is active, present on
every vblank timer wakeup, repeating the current frame when nothing
changed. The display then behaves exactly like a fixed refresh rate
display. The vblank timer keeps waking at the refresh interval while
nothing else is flipping, so the hold rides its wakeups directly, and
client frames still flip the moment they arrive on their own wakeups.
Games pace the display themselves and are never touched. The
adaptive_sync_idle_hold convar turns the hold off.
Adaptive sync idle holds re-present content that has not changed, so
whenever the frame can't pass direct scanout each hold runs a
full-screen composite whose output is identical to the image already on
screen, measured as a clear rise in average SoC power versus near zero
under scanout. The nested Wayland backend composites every present and
does not take the reuse, the host compositor owns that case.

Tag unchanged frames in FrameInfo_t and present the previously
composited image again instead of dispatching an identical composite.
The output image ring only advances when a present composite runs, so
the last image is still the one on screen. Reuse only applies to
consecutive full composites, frames with deferred cursor or overlay
damage still composite so the update is really drawn, and the
drm_reuse_last_composite convar turns it off for comparison.
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