From d217da22d1defac5e49fc3d94cbdc6e1ece9cd54 Mon Sep 17 00:00:00 2001 From: Robin Morgan <60886158+ro8inmorgan@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:46:04 +0200 Subject: [PATCH] Added code to request Android to set the display refresh rate based on Retroarch vertical refresh rate config value --- configuration.c | 28 +++- pkg/android/phoenix-common/jni/Android.mk | 22 +++- .../phoenix-common/jni/ra_android_bridge.c | 79 ++++++++++++ .../phoenix-common/jni/ra_android_bridge.h | 13 ++ .../retroactivity/RetroActivityFuture.java | 121 ++++++++++++++++-- 5 files changed, 246 insertions(+), 17 deletions(-) create mode 100644 pkg/android/phoenix-common/jni/ra_android_bridge.c create mode 100644 pkg/android/phoenix-common/jni/ra_android_bridge.h diff --git a/configuration.c b/configuration.c index 6ecfc5fe59c9..45b433122a56 100644 --- a/configuration.c +++ b/configuration.c @@ -56,6 +56,10 @@ #include "list_special.h" +#if defined(ANDROID) +#include "pkg/android/phoenix-common/jni/ra_android_bridge.h" +#endif + #if defined(__WINRT__) || defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP #include "uwp/uwp_func.h" #endif @@ -2383,7 +2387,11 @@ static struct config_float_setting *populate_settings_float( #endif *size = count; - +#if defined(__ANDROID__) || defined(ANDROID) + if (settings && settings->floats.video_refresh_rate > 0.0f) { + ra_notify_refresh_rate(settings->floats.video_refresh_rate); + } +#endif return tmp; } @@ -2995,6 +3003,7 @@ void config_set_defaults(void *data) g_defaults.settings_video_refresh_rate != DEFAULT_REFRESH_RATE) settings->floats.video_refresh_rate = g_defaults.settings_video_refresh_rate; + if (DEFAULT_AUDIO_DEVICE) configuration_set_string(settings, settings->arrays.audio_device, @@ -4543,6 +4552,12 @@ static bool config_load_file(global_t *global, if (size_settings) free(size_settings); first_load = false; + +#if defined(__ANDROID__) || defined(ANDROID) + if (settings && settings->floats.video_refresh_rate > 0.0f) { + ra_notify_refresh_rate(settings->floats.video_refresh_rate); + } +#endif return true; } @@ -4746,6 +4761,12 @@ bool config_load_override(void *data) else runloop_state_get_ptr()->flags &= ~RUNLOOP_FLAG_OVERRIDES_ACTIVE; + +#if defined(__ANDROID__) || defined(ANDROID) + if (settings && settings->floats.video_refresh_rate > 0.0f) { + ra_notify_refresh_rate(settings->floats.video_refresh_rate); + } +#endif return true; } @@ -4788,6 +4809,11 @@ bool config_load_override_file(const char *config_path) else runloop_state_get_ptr()->flags &= ~RUNLOOP_FLAG_OVERRIDES_ACTIVE; +#if defined(__ANDROID__) || defined(ANDROID) + if (settings && settings->floats.video_refresh_rate > 0.0f) { + ra_notify_refresh_rate(settings->floats.video_refresh_rate); + } +#endif return true; } diff --git a/pkg/android/phoenix-common/jni/Android.mk b/pkg/android/phoenix-common/jni/Android.mk index 9c1232ed03ea..2ea43786bf0a 100644 --- a/pkg/android/phoenix-common/jni/Android.mk +++ b/pkg/android/phoenix-common/jni/Android.mk @@ -3,6 +3,7 @@ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) RARCH_DIR := ../../../.. +RA_ROOT := $(LOCAL_PATH)/$(RARCH_DIR) HAVE_NEON := 1 HAVE_LOGGER := 0 @@ -57,6 +58,14 @@ LOCAL_MODULE := retroarch-activity LOCAL_SRC_FILES += $(RARCH_DIR)/griffin/griffin.c \ $(RARCH_DIR)/griffin/griffin_cpp.cpp +LOCAL_SRC_FILES += \ + ra_android_bridge.c + +LOCAL_C_INCLUDES += \ + $(RA_ROOT) \ + $(RA_ROOT)/libretro-common/include \ + $(RA_ROOT)/gfx/include + ifeq ($(HAVE_LOGGER), 1) DEFINES += -DHAVE_LOGGER endif @@ -173,13 +182,14 @@ LOCAL_CPPFLAGS := -fexceptions -fpermissive -std=gnu++11 -fno-rtti -Wno-reorder LOCAL_CFLAGS := $(subst -O3,-O2,$(LOCAL_CFLAGS)) LOCAL_LDLIBS := -landroid -lEGL $(GLES_LIB) $(LOGGER_LDLIBS) -ldl -LOCAL_C_INCLUDES := \ - $(LOCAL_PATH)/$(RARCH_DIR)/libretro-common/include \ - $(LOCAL_PATH)/$(RARCH_DIR)/deps \ - $(LOCAL_PATH)/$(RARCH_DIR)/deps/stb \ - $(LOCAL_PATH)/$(RARCH_DIR)/deps/7zip +LOCAL_C_INCLUDES += \ + $(RA_ROOT)/libretro-common/include \ + $(RA_ROOT)/deps \ + $(RA_ROOT)/deps/stb \ + $(RA_ROOT)/deps/7zip + -INCLUDE_DIRS := \ +INCLUDE_DIRS := \ -I$(LOCAL_PATH)/$(DEPS_DIR)/stb/ \ -I$(LOCAL_PATH)/$(DEPS_DIR)/7zip/ \ -I$(LOCAL_PATH)/$(DEPS_DIR)/libFLAC/include diff --git a/pkg/android/phoenix-common/jni/ra_android_bridge.c b/pkg/android/phoenix-common/jni/ra_android_bridge.c new file mode 100644 index 000000000000..eead86c9d205 --- /dev/null +++ b/pkg/android/phoenix-common/jni/ra_android_bridge.c @@ -0,0 +1,79 @@ +#include +#include +#include "runloop.h" +#include "verbosity.h" +#include "configuration.h" + +// ---------- Cached JNI handles ---------- +static JavaVM *g_vm = NULL; +static jclass g_cls_RetroActivityFuture = NULL; // GlobalRef +static jmethodID g_mid_nativePushContentFps = NULL; // (F)V + +// Small helper: get/attach JNIEnv for the current thread +static JNIEnv* ra_get_env(bool *out_attached) +{ + if (out_attached) *out_attached = false; + if (!g_vm) return NULL; + + JNIEnv *env = NULL; + jint r = (*g_vm)->GetEnv(g_vm, (void**)&env, JNI_VERSION_1_6); + if (r == JNI_OK && env) return env; + + // Not attached -> attach + if ((*g_vm)->AttachCurrentThread(g_vm, &env, NULL) != 0) + return NULL; + if (out_attached) *out_attached = true; + return env; +} + +// ---------- JNI load/unload ---------- +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) +{ + g_vm = vm; + JNIEnv *env = NULL; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK || !env) + return JNI_ERR; + + jclass local = (*env)->FindClass(env, "com/retroarch/browser/retroactivity/RetroActivityFuture"); + if (!local) return JNI_ERR; + + g_cls_RetroActivityFuture = (jclass)(*env)->NewGlobalRef(env, local); + (*env)->DeleteLocalRef(env, local); + if (!g_cls_RetroActivityFuture) return JNI_ERR; + + // Cache static push method: public static void nativePushContentFps(float) + g_mid_nativePushContentFps = (*env)->GetStaticMethodID(env, g_cls_RetroActivityFuture, + "nativePushContentFps", "(F)V"); + // It's fine if Java side doesn't have it yet; we just won't call it + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) +{ + JNIEnv *env = NULL; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) == JNI_OK && env) { + if (g_cls_RetroActivityFuture) { + (*env)->DeleteGlobalRef(env, g_cls_RetroActivityFuture); + g_cls_RetroActivityFuture = NULL; + } + } + g_vm = NULL; + g_mid_nativePushContentFps = NULL; +} + +// ---------- Public C API: push fresh FPS to Java (optional) ---------- +void ra_notify_refresh_rate(float fps) +{ + if (fps <= 0.f) return; + if (!g_vm || !g_cls_RetroActivityFuture || !g_mid_nativePushContentFps) return; + + bool attached = false; + JNIEnv *env = ra_get_env(&attached); + if (!env) return; + + (*env)->CallStaticVoidMethod(env, g_cls_RetroActivityFuture, + g_mid_nativePushContentFps, (jfloat)fps); + + if (attached) + (*g_vm)->DetachCurrentThread(g_vm); +} \ No newline at end of file diff --git a/pkg/android/phoenix-common/jni/ra_android_bridge.h b/pkg/android/phoenix-common/jni/ra_android_bridge.h new file mode 100644 index 000000000000..4b2615cd18ae --- /dev/null +++ b/pkg/android/phoenix-common/jni/ra_android_bridge.h @@ -0,0 +1,13 @@ +#ifndef RA_ANDROID_BRIDGE_H +#define RA_ANDROID_BRIDGE_H + +#ifdef __cplusplus +extern "C" { +#endif + +void ra_notify_refresh_rate(float fps); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java b/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java index f74c9d14580d..7e6197ef4fc3 100644 --- a/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java +++ b/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java @@ -14,14 +14,21 @@ import android.os.Message; import com.retroarch.browser.preferences.util.ConfigFile; import com.retroarch.browser.preferences.util.UserPreferences; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import android.view.Display; +import android.view.Window; +import java.lang.ref.WeakReference; // <-- added +import java.io.*; +import java.util.regex.*; public final class RetroActivityFuture extends RetroActivityCamera { // Tracks activity lifecycle state for MainMenuActivity resume detection public static volatile boolean isRunning = false; + // Keep a weak reference to the current activity so static JNI can reach instance safely + private static volatile WeakReference sLastActivity = new WeakReference<>(null); // <-- added + // If set to true then RetroArch will completely exit when it loses focus private boolean quitfocus = false; @@ -37,6 +44,9 @@ public final class RetroActivityFuture extends RetroActivityCamera { private static final int HANDLER_ARG_FALSE = 0; private static final int HANDLER_MESSAGE_DELAY_DEFAULT_MS = 300; + // Main-thread handler for static callbacks from native + private static final Handler MAIN = new Handler(Looper.getMainLooper()); // <-- added + // Handler used for UI events private final Handler mHandler = new Handler(Looper.getMainLooper()) { @Override @@ -58,7 +68,10 @@ public void handleMessage(Message msg) { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + + // Track current instance for static callbacks + sLastActivity = new WeakReference<>(this); // <-- added + isRunning = true; mDecorView = getWindow().getDecorView(); @@ -69,18 +82,16 @@ public void onCreate(Bundle savedInstanceState) { @Override public void onNewIntent(Intent intent) { super.onNewIntent(intent); - // Extract game parameters from new intent String newRom = intent.getStringExtra("ROM"); String newCore = intent.getStringExtra("LIBRETRO"); - + // Get current intent parameters for comparison Intent currentIntent = getIntent(); String currentRom = currentIntent != null ? currentIntent.getStringExtra("ROM") : null; String currentCore = currentIntent != null ? currentIntent.getStringExtra("LIBRETRO") : null; - - - // Check if we're trying to launch different content + + // Check if we're trying to launch different content if ((newRom != null && !newRom.equals(currentRom)) || (newCore != null && !newCore.equals(currentCore))) { // Different game content - exit cleanly and let launcher restart us @@ -95,7 +106,6 @@ public void onNewIntent(Intent intent) { @Override public void onResume() { super.onResume(); - setSustainedPerformanceMode(sustainedPerformanceMode); // Check for Android UI specific parameters @@ -115,7 +125,8 @@ public void onResume() { ConfigFile configFile = new ConfigFile(UserPreferences.getDefaultConfigPath(this)); try { if (configFile.getBoolean("video_notch_write_over_enable")) { - getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; } } catch (Exception e) { Log.w("Key doesn't exist yet.", e.getMessage()); @@ -135,6 +146,12 @@ public void onStop() { public void onDestroy() { super.onDestroy(); isRunning = false; + + // Clear ref if this instance is the one held + RetroActivityFuture held = sLastActivity.get(); + if (held == this) { + sLastActivity.clear(); // <-- added + } } @Override @@ -211,7 +228,8 @@ private void attemptToggleNvidiaCursorVisibility(boolean state) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { try { boolean cursorVisibility = !state; - Method mInputManager_setCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class); + Method mInputManager_setCursorVisibility = + InputManager.class.getMethod("setCursorVisibility", boolean.class); InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE); mInputManager_setCursorVisibility.invoke(inputManager, cursorVisibility); } catch (NoSuchMethodException e) { @@ -239,4 +257,87 @@ private void attemptTogglePointerIcon(boolean state) { } } } + +private void requestRefreshIfPossible(float targetHz) { + Log.w("[Retroarch FPS]", "setting target FPS"); + + final Window window = getWindow(); + if (window == null) return; + final WindowManager wm = getWindowManager(); + if (wm == null) return; + + Log.w("[Retroarch FPS]", "Window found setting target FPS"); + + final float desiredHz = targetHz; // keep caller's target + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // API 23+ + final Display display = wm.getDefaultDisplay(); + if (display == null) return; + + final Display.Mode current = display.getMode(); + final Display.Mode[] modes = display.getSupportedModes(); + + int bestId = current.getModeId(); + float bestHz = current.getRefreshRate(); + float bestScore = Math.abs(bestHz - desiredHz); + + // Find closest refresh among modes with identical resolution + for (Display.Mode m : modes) { + if (m.getPhysicalWidth() == current.getPhysicalWidth() + && m.getPhysicalHeight() == current.getPhysicalHeight()) { + final float hz = m.getRefreshRate(); + final float score = Math.abs(hz - desiredHz); + // prefer smaller score; on (near) tie, pick the higher Hz + if (score < bestScore - 0.05f || + (Math.abs(score - bestScore) <= 0.05f && hz > bestHz)) { + bestScore = score; + bestHz = hz; + bestId = m.getModeId(); + } + } + } + + // Apply the *chosen* refresh rate/mode + WindowManager.LayoutParams lp = window.getAttributes(); + try { lp.getClass().getField("preferredDisplayModeId").setInt(lp, bestId); } catch (Throwable ignored) {} + try { lp.getClass().getField("preferredRefreshRate").setFloat(lp, bestHz); } catch (Throwable ignored) {} + window.setAttributes(lp); + + Log.w("[Retroarch FPS]", "Window target Hz SET modeId=" + bestId + + " chosen=" + bestHz + "Hz (target=" + desiredHz + ", Δ=" + bestScore + ")"); + } else { + // Legacy fallback (may no-op on many builds) + try { + Window.class.getMethod("setPreferredRefreshRate", float.class) + .invoke(window, desiredHz); + Log.w("[Retroarch FPS]", "legacy setPreferredRefreshRate(" + desiredHz + ")"); + } catch (Throwable ignored) { + try { + WindowManager.LayoutParams lp = window.getAttributes(); + lp.getClass().getField("preferredRefreshRate").setFloat(lp, desiredHz); + window.setAttributes(lp); + Log.w("[Retroarch FPS]", "legacy LayoutParams preferredRefreshRate=" + desiredHz); + } catch (Throwable ignored2) {} + } + } + } catch (Throwable t) { + try { Log.w("Retroarch FPS", "Failed to request closest to " + desiredHz + "Hz: " + t); } catch (Throwable ignored) {} + } + } + + public static void nativePushContentFps(final float fps) { // <-- added + MAIN.post(() -> { + RetroActivityFuture a = sLastActivity.get(); + if (a != null && isRunning && fps > 0f) { + float f = fps; + // optional clamping to match your pull path + if (f < 65f) f = 60.0f; + else if (f > 64f && f < 115f) f = 90.0f; + else f = 120f; + Log.w("[Retroarch FPS]", "nativePushContentFps: " + f); + a.requestRefreshIfPossible(f); + } + }); + } }