From 16cc37ca10ebb64db1e714c6a53d7f21b13766b4 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 15 Jun 2026 23:39:36 -0600 Subject: [PATCH] Android: Call Surface.setFrameRate & update it --- files.cmake | 2 + .../com/twilitrealm/dusk/DuskActivity.java | 82 +++++++++++++++++++ src/dusk/android_frame_rate.cpp | 74 +++++++++++++++++ src/dusk/android_frame_rate.hpp | 7 ++ src/dusk/ui/settings.cpp | 16 +++- src/m_Do/m_Do_main.cpp | 2 + 6 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 src/dusk/android_frame_rate.cpp create mode 100644 src/dusk/android_frame_rate.hpp diff --git a/files.cmake b/files.cmake index 715622a495..f891024e8a 100644 --- a/files.cmake +++ b/files.cmake @@ -1418,6 +1418,8 @@ set(DUSK_FILES include/dusk/scope_guard.hpp src/dusk/dvd_asset.cpp src/d/actor/d_a_alink_dusk.cpp + src/dusk/android_frame_rate.hpp + src/dusk/android_frame_rate.cpp src/dusk/asserts.cpp src/dusk/batch.cpp src/dusk/batch.hpp diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java index cc1c985193..96fc302d9e 100644 --- a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java +++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java @@ -4,6 +4,7 @@ import android.app.ActionBar; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ClipData; +import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; @@ -14,12 +15,16 @@ import android.provider.DocumentsContract; import android.provider.OpenableColumns; import android.provider.Settings; import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceHolder; import android.view.View; import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController; import org.libsdl.app.SDLActivity; +import org.libsdl.app.SDLSurface; import java.io.File; import java.util.ArrayList; @@ -27,6 +32,7 @@ import java.util.List; public class DuskActivity extends SDLActivity { private static final String TAG = "DuskActivity"; + private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f; private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455; private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456; private static final String EXTERNAL_STORAGE_AUTHORITY = @@ -88,6 +94,11 @@ public class DuskActivity extends SDLActivity { hideSystemBars(); } + @Override + protected SDLSurface createSDLSurface(Context context) { + return new DuskSurface(context); + } + @Override protected void onResume() { super.onResume(); @@ -139,6 +150,77 @@ public class DuskActivity extends SDLActivity { }; } + public void setPreferredSurfaceFrameRate(float frameRate) { + runOnUiThread(() -> { + if (mSurface instanceof DuskSurface) { + ((DuskSurface)mSurface).setPreferredFrameRate(frameRate); + } + }); + } + + private static final class DuskSurface extends SDLSurface { + private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE; + + DuskSurface(Context context) { + super(context); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + super.surfaceChanged(holder, format, width, height); + setTargetFrameRate(holder); + } + + void setPreferredFrameRate(float frameRate) { + preferredFrameRate = frameRate; + setTargetFrameRate(getHolder()); + } + + private void setTargetFrameRate(SurfaceHolder holder) { + if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return; + } + + Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface(); + if (surface == null || !surface.isValid()) { + return; + } + + float targetFrameRate = getMaxSupportedFrameRate(); + if (preferredFrameRate > 0.0f) { + targetFrameRate = preferredFrameRate; + } + if (targetFrameRate <= 0.0f) { + return; + } + + try { + surface.setFrameRate( + targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT); + Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps"); + } catch (RuntimeException e) { + Log.w(TAG, "Failed to request surface frame rate", e); + } + } + + private float getMaxSupportedFrameRate() { + if (mDisplay == null) { + return 0.0f; + } + + float maxFrameRate = mDisplay.getRefreshRate(); + Display.Mode[] modes = mDisplay.getSupportedModes(); + if (modes == null) { + return maxFrameRate; + } + + for (Display.Mode mode : modes) { + maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate()); + } + return maxFrameRate; + } + } + @Override protected String[] getArguments() { Intent intent = getIntent(); diff --git a/src/dusk/android_frame_rate.cpp b/src/dusk/android_frame_rate.cpp new file mode 100644 index 0000000000..bf889a9482 --- /dev/null +++ b/src/dusk/android_frame_rate.cpp @@ -0,0 +1,74 @@ +#include "dusk/android_frame_rate.hpp" + +#if defined(TARGET_ANDROID) || defined(__ANDROID__) || defined(ANDROID) +#include "dusk/settings.h" + +#include +#include + +namespace dusk::android { +namespace { + +float preferred_surface_frame_rate() { + switch (getSettings().game.enableFrameInterpolation.getValue()) { + case FrameInterpMode::Off: + return 30.0f; + case FrameInterpMode::Unlimited: + default: + return 0.0f; + case FrameInterpMode::Capped: + return static_cast(getSettings().video.maxFrameRate.getValue()); + } +} + +bool clear_pending_exception(JNIEnv* env) { + if (env == nullptr || !env->ExceptionCheck()) { + return false; + } + env->ExceptionClear(); + return true; +} + +} // namespace + +void update_surface_frame_rate() { + auto* env = static_cast(SDL_GetAndroidJNIEnv()); + if (env == nullptr) { + return; + } + + jobject activity = static_cast(SDL_GetAndroidActivity()); + if (activity == nullptr || clear_pending_exception(env)) { + if (activity != nullptr) { + env->DeleteLocalRef(activity); + } + return; + } + + jclass activityClass = env->GetObjectClass(activity); + if (activityClass == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return; + } + + jmethodID setPreferredFrameRate = + env->GetMethodID(activityClass, "setPreferredSurfaceFrameRate", "(F)V"); + env->DeleteLocalRef(activityClass); + if (setPreferredFrameRate == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return; + } + + jvalue args[1]{}; + args[0].f = preferred_surface_frame_rate(); + env->CallVoidMethodA(activity, setPreferredFrameRate, args); + env->DeleteLocalRef(activity); + clear_pending_exception(env); +} + +} // namespace dusk::android +#else +namespace dusk::android { +void update_surface_frame_rate() {} +} // namespace dusk::android +#endif diff --git a/src/dusk/android_frame_rate.hpp b/src/dusk/android_frame_rate.hpp new file mode 100644 index 0000000000..03a7876633 --- /dev/null +++ b/src/dusk/android_frame_rate.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace dusk::android { + +void update_surface_frame_rate(); + +} // namespace dusk::android diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 194ddd78a7..1a21d4ae34 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -6,6 +6,7 @@ #include "dusk/app_info.hpp" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" +#include "dusk/android_frame_rate.hpp" #include "dusk/config.hpp" #include "dusk/hotkeys.h" #include "dusk/data.hpp" @@ -478,14 +479,19 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar& var, Rml::String key, Rml::String helpText, int min, int max, int step = 5, - std::function isDisabled = {}, std::string suffix = "") { + std::function isDisabled = {}, std::function onChange = {}, + std::string suffix = "") { auto& button = leftPane.add_child(NumberButton::Props{ .key = std::move(key), .getValue = [&var] { return var; }, .setValue = - [&var, min, max](int value) { - var.setValue(std::clamp(value, min, max)); + [&var, min, max, callback = std::move(onChange)](int value) { + const int clampedValue = std::clamp(value, min, max); + var.setValue(clampedValue); config::Save(); + if (callback) { + callback(clampedValue); + } }, .isDisabled = std::move(isDisabled), .isModified = [&var] { return var.getValue() != var.getDefaultValue(); }, @@ -929,6 +935,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .on_pressed([i] { mDoAud_seStartMenu(kSoundItemChange); getSettings().game.enableFrameInterpolation.setValue(static_cast(i)); + android::update_surface_frame_rate(); config::Save(); }); } @@ -936,7 +943,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { }); config_int_select(leftPane, rightPane, getSettings().video.maxFrameRate, "Framerate Cap", "Limit the framerate to the specified value.", 30, 540, 1, - [] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; }); + [] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; }, + [](int) { android::update_surface_frame_rate(); }); config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground, { .key = "Enable Mini-Map Shadows", diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 4694146714..2cddcd0981 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -47,6 +47,7 @@ #include #include #include "SSystem/SComponent/c_API.h" +#include "dusk/android_frame_rate.hpp" #include "dusk/app_info.hpp" #include "dusk/crash_handler.h" #include "dusk/crash_reporting.h" @@ -555,6 +556,7 @@ int game_main(int argc, char* argv[]) { dusk::resetForSpeedrunMode(); } ApplyCVarOverrides(parsed_arg_options["cvar"]); + dusk::android::update_surface_frame_rate(); dusk::crash_reporting::initialize(); dusk::crash_handler::install(); // TODO: How to handle this?