From 6b05944116b25c1f748baf29525f645a84c941d8 Mon Sep 17 00:00:00 2001 From: lenore Date: Tue, 9 Dec 2025 17:21:09 +0100 Subject: [PATCH] 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 --- .../citra_emu/activities/EmulationActivity.kt | 4 ++ .../features/settings/ui/SettingsActivity.kt | 4 ++ .../citra/citra_emu/ui/main/MainActivity.kt | 4 ++ .../citra/citra_emu/utils/RefreshRateUtil.kt | 53 +++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/RefreshRateUtil.kt diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index f23147dd8..d060c59bb 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -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) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt index 070a8f487..064fa700e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt @@ -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) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt index 37bff3396..71e07225b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt @@ -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 { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/RefreshRateUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/RefreshRateUtil.kt new file mode 100644 index 000000000..675fe6702 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/RefreshRateUtil.kt @@ -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 + } + } +} \ No newline at end of file