Mobile Development 30 min read

Understanding Variable Refresh Rate on Android Devices and How to Control It

This article explains what variable refresh rate is on Android, how the platform determines the final refresh rate when multiple apps request different rates, and provides detailed SDK and NDK methods—including DisplayManager listeners, Surface.setFrameRate, and AChoreographer callbacks—to query and set refresh rates, as well as the underlying SurfaceFlinger vote and policy mechanisms that enforce these settings.

Coolpad Technology Team
Coolpad Technology Team
Coolpad Technology Team
Understanding Variable Refresh Rate on Android Devices and How to Control It

Variable refresh rate (VRR) on Android is controlled by the platform, which must satisfy the refresh‑rate requirements of all visible applications; for example, a 24 fps video player may look fine, but UI animations at that rate feel sluggish.

Applications can obtain the current refresh rate via two main approaches:

SDK: Register a DisplayManager.DisplayListener and call Display.getRefreshRate() .

NDK: Use AChoreographer_registerRefreshRateCallback (API level 30) to receive callbacks.

Since Android 11, apps can influence the platform’s choice by setting a desired frame rate on their window or surface. The following APIs are available:

SDK: Surface.setFrameRate and SurfaceControl.Transaction.setFrameRate .

NDK: ANativeWindow_setFrameRate and ASurfaceTransaction_setFrameRate .

These calls are documented in the Android “Frame Rate Guide”. The system selects the most suitable refresh rate based on the values set on the window or surface.

When SurfaceFlinger initializes, it reads the supported refresh‑rate configurations (e.g., 50 fps, 60 fps, 90 fps, 120 fps) from the hardware composer. The mRefreshRateConfigs object holds these values.

Refresh‑rate limits can be changed via ADB commands:

adb shell settings put system min_refresh_rate 60   // sets the primary range lower bound
adb shell settings put system peak_refresh_rate 120 // sets the app‑request range upper bound

Changing these values triggers DisplayModeDirector to receive an OnChange() event, which calls updateRefreshRateSettingLocked() and ultimately updates the Vote objects for each priority level.

A Vote records a width, height, and a refresh‑rate range ( minRefreshRate , maxRefreshRate ) for a specific priority. All votes are stored in a SparseArray<int, Vote> called Votes . The method updateVoteLocked() replaces the existing vote for a given priority.

The system then aggregates votes using summarizeVotes() , which computes the intersection of the ranges for all priorities above a certain threshold ( lowestConsideredPriority ). The resulting RefreshRateRange is stored in a RefreshRateRange object.

Two key structures are used during aggregation:

RefreshRateRange : Holds min and max refresh‑rate values.

VoteSummary : Holds the combined minRefreshRate , maxRefreshRate , width, and height after merging all votes.

The final desired display mode specifications are built in DesiredDisplayConfigSpecs , which contains:

primaryRefreshRateMin/Max : The range the platform may select.

appRequestRefreshRateMin/Max : The broader range apps can request via the frame‑rate APIs.

defaultConfig : The fallback configuration.

When the aggregated specifications change, DisplayModeDirector notifies listeners via notifyDesiredDisplayModeSpecsChangedLocked() , which posts a MSG_REFRESH_RATE_RANGE_CHANGED message to the global mDesiredDisplayModeSpecsListener . The listener’s onDesiredDisplayModeSpecsChanged() method ultimately calls setDesiredDisplayModeSpecsLocked() , propagating the new policy to the native layer.

In the native layer, RefreshRateConfigs defines RefreshRate (fps and config ID) and Policy (primary and app‑request ranges). The method calculateRefreshRateConfigIndexType() selects the appropriate config ID based on current signals (touch, idle, content detection) and the policy.

The core algorithm for picking the best refresh rate is RefreshRateConfigs::getBestRefreshRate() . It scores each candidate refresh rate against the votes of all layers, considering vote types such as Max , Min , ExplicitDefault , and ExplicitExactOrMultiple . The highest‑scoring rate is chosen, with touch‑boost logic applied when appropriate.

Overall, the article provides a complete walkthrough of how Android determines and applies variable refresh rates, from high‑level policy down to low‑level vote aggregation and scoring.

SDKAndroidSurfaceFlingerrefresh-rateNDKvariable-refresh-rate
Coolpad Technology Team
Written by

Coolpad Technology Team

Committed to advancing technology and supporting innovators. The Coolpad Technology Team regularly shares forward‑looking insights, product updates, and tech news. Tech experts are welcome to join; everyone is invited to follow us.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.