Skip to content

Fix: Add high refresh rate (90Hz/120Hz) optimization support (#2086)#2094

Closed
zaibihsn wants to merge 1 commit into
maxrave-dev:devfrom
zaibihsn:fix-high-refresh-rate
Closed

Fix: Add high refresh rate (90Hz/120Hz) optimization support (#2086)#2094
zaibihsn wants to merge 1 commit into
maxrave-dev:devfrom
zaibihsn:fix-high-refresh-rate

Conversation

@zaibihsn
Copy link
Copy Markdown

Fixes #2086

Description of the Bug:
Users with high refresh rate screens (90Hz, 120Hz, etc.) experience UI scrolling locked to 60Hz. This happens because many Android OEM skins (like MIUI, OneUI, and ColorOS) aggressively throttle refresh rates for standard apps to preserve battery, especially in Compose-heavy applications, unless the activity explicitly requests the higher rate.

Resolution:

Added a hardware display check in MainActivity.kt (for Android M and above).

The app now dynamically queries the device's supportedModes, finds the mode with the highest available refresh rate, and assigns it to window.attributes.preferredDisplayModeId.

This forces the OS to uncap the framerate, allowing for buttery-smooth scrolling and animations across the entire application without any negative impact on standard 60Hz devices.

@maxrave-dev maxrave-dev changed the base branch from main to dev June 1, 2026 02:58
@maxrave-dev
Copy link
Copy Markdown
Owner

Thanks for another contribution @zaibihsn — and thanks especially for #2091, which was a clean fix. I appreciate the effort to make scrolling and animations feel smoother here too.

I can't merge this one as-is though, because the approach has a correctness issue on real devices. Let me walk through it.

1. preferredDisplayModeId can silently downgrade resolution (main blocker)

Display.Mode is a (physicalWidth, physicalHeight, refreshRate) tuple, and supportedModes can contain modes at different resolutions. This line:

val maxRefreshRateMode = modes.maxByOrNull { it.refreshRate }

picks the highest refresh rate regardless of resolution, and preferredDisplayModeId then forces that entire mode — resolution included.

On some QHD-capable Samsung flagships this is an active regression. On the Galaxy S20 series, for example, the panel exposes high refresh rate only at the lower resolution: you get QHD+ at 60Hz, or FHD+ at 120Hz, not both. So if a user has chosen QHD+, this code forces the display down to FHD+ to chase the higher refresh rate, making the whole UI noticeably blurrier. (Newer panels like the S21 Ultra removed this tradeoff, so behavior varies by device — which is exactly why blindly forcing the max-refresh mode is risky.) The "no negative impact on standard 60Hz devices" note in the description is accurate, but it's the multi-resolution high-refresh devices — the ones this feature is meant to help — that can get hurt.

2. Wrong API for a refresh-rate-only goal

The Android docs are explicit that preferredDisplayModeId is for when you actually want to change the display mode / resolution, not just the refresh rate:

If apps only want to change preferred refresh rate, it is preferred to use preferredRefreshRate, rather than preferredDisplayModeId. In general use setFrameRate() if possible.

Frame rate / Display refresh rate

Since the intent here is purely refresh rate, preferredRefreshRate avoids the resolution coupling entirely and is the documented path. (It also degrades gracefully: if the requested rate isn't available at the current resolution, the platform just ignores it rather than switching resolution on the user.)

3. Battery / adaptive refresh

Pinning a specific mode also defeats VRR/LTPO adaptive refresh. On static content — a paused player, a lyrics screen — the platform would normally drop to a low rate to save power. Forcing a fixed high mode removes that freedom and increases power draw. preferredRefreshRate lets the platform keep optimizing.

4. Minor: deprecated API

windowManager.defaultDisplay is deprecated since API 30. Since MainActivity is an Activity, the modern replacement is getDisplay() (with a fallback only if you need pre-R support).

Suggested minimal version

Refresh-rate only, no resolution change:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    val rate = (display ?: windowManager.defaultDisplay).supportedModes.maxOf { it.refreshRate }
    window.attributes = window.attributes.apply { preferredRefreshRate = rate }
}

If preferredDisplayModeId is genuinely needed for some reason, then supportedModes should first be filtered to modes matching the current display.mode physicalWidth/physicalHeight, and the highest refresh rate picked only within that subset — otherwise the resolution downgrade in (1) comes back.


My read is this just hasn't been tested against multi-resolution high-refresh devices yet, and it diverges from the documented guidance. Could you switch to preferredRefreshRate along the lines above? Happy to take another look once it's updated. Thanks again for the contributions. 🙏

@maxrave-dev maxrave-dev closed this Jun 1, 2026
@zaibihsn
Copy link
Copy Markdown
Author

zaibihsn commented Jun 1, 2026

Thank you so much for the detailed explanation and the great catch on the resolution downgrade! I've updated the
│ logic to strictly use preferredRefreshRate via API 30+ as suggested. Let me know if everything looks good now!

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.

Improve high refresh rate support (90Hz / 120Hz optimization)

2 participants