android: Force app to use max available refresh rate in menus (#1492)

* [android]: Force app to use the displays max set refresh rate

Since Android 15, google automatically forces "games" to be 60 hrz. This ensures the display's max refresh rate is actually used. Tested on a Google Pixel 7 Pro with Android 16

Emulation Activity was excluded for battery usage concerns

* force60Hrz option

* Code cleanup

* Expanded refresh rate explaination comment

* Moved `enforceRefreshRate` calls to earlier in `onCreate`

This probably doesn't actually do anything, it's a bit of a nitpick

* Moved `enforceRefreshRate` SDK version check to within the function

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
This commit is contained in:
lenore 2025-12-09 17:21:09 +01:00 committed by GitHub
parent b0fe4d190d
commit 6b05944116
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 65 additions and 0 deletions

View File

@ -10,6 +10,7 @@ import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
@ -47,6 +48,7 @@ import org.citra.citra_emu.utils.FileBrowserHelper
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.RefreshRateUtil
import org.citra.citra_emu.utils.ThemeUtil
import org.citra.citra_emu.viewmodel.EmulationViewModel
@ -82,6 +84,8 @@ class EmulationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
requestWindowFeature(Window.FEATURE_NO_TITLE)
RefreshRateUtil.enforceRefreshRate(this, sixtyHz = true)
ThemeUtil.setTheme(this)
settingsViewModel.settings.loadSettings()
super.onCreate(savedInstanceState)

View File

@ -7,6 +7,7 @@ package org.citra.citra_emu.features.settings.ui
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
@ -37,6 +38,7 @@ import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.utils.DirectoryInitialization
import org.citra.citra_emu.utils.InsetsHelper
import org.citra.citra_emu.utils.RefreshRateUtil
import org.citra.citra_emu.utils.ThemeUtil
class SettingsActivity : AppCompatActivity(), SettingsActivityView {
@ -49,6 +51,8 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
override val settings: Settings get() = settingsViewModel.settings
override fun onCreate(savedInstanceState: Bundle?) {
RefreshRateUtil.enforceRefreshRate(this)
ThemeUtil.setTheme(this)
super.onCreate(savedInstanceState)

View File

@ -6,6 +6,7 @@ package org.citra.citra_emu.ui.main
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
@ -51,6 +52,7 @@ import org.citra.citra_emu.utils.CitraDirectoryUtils
import org.citra.citra_emu.utils.DirectoryInitialization
import org.citra.citra_emu.utils.FileBrowserHelper
import org.citra.citra_emu.utils.InsetsHelper
import org.citra.citra_emu.utils.RefreshRateUtil
import org.citra.citra_emu.utils.PermissionsHandler
import org.citra.citra_emu.utils.ThemeUtil
import org.citra.citra_emu.viewmodel.GamesViewModel
@ -66,6 +68,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
override var themeId: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
RefreshRateUtil.enforceRefreshRate(this)
val splashScreen = installSplashScreen()
CitraDirectoryUtils.attemptAutomaticUpdateDirectory()
splashScreen.setKeepOnScreenCondition {

View File

@ -0,0 +1,53 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.utils
import android.app.Activity
import android.os.Build
import androidx.annotation.RequiresApi
object RefreshRateUtil {
// Since Android 15, the OS automatically runs apps categorized as games with a
// 60hz refresh rate by default, regardless of the refresh rate set by the user.
//
// This function sets the refresh rate to either the maximum allowed refresh rate or
// 60hz depending on the value of the `sixtyHz` parameter.
//
// Note: This isn't always the maximum refresh rate that the display is *capable of*,
// but is instead the refresh rate chosen by the user in the Android system settings.
// For example, if the user selected 120hz in the settings, but the display is capable
// of 144hz, 120hz will be treated as the maximum within this function.
fun enforceRefreshRate(activity: Activity, sixtyHz: Boolean = false) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return
}
val display = activity.display
val window = activity.window
display?.let {
// Get all supported modes and find the one with the highest refresh rate
val supportedModes = it.supportedModes
val maxRefreshRate = supportedModes.maxByOrNull { mode -> mode.refreshRate }
if (maxRefreshRate == null) {
return
}
var newModeId: Int?
if (sixtyHz) {
newModeId = supportedModes.firstOrNull { mode -> mode.refreshRate == 60f }?.modeId
} else {
// Set the preferred display mode to the one with the highest refresh rate
newModeId = maxRefreshRate.modeId
}
if (newModeId == null) {
return
}
window.attributes.preferredDisplayModeId = newModeId
}
}
}