From 5302dad837f2f2b346658ace7610b989cf2e6483 Mon Sep 17 00:00:00 2001 From: Ishan09811 <156402647+Ishan09811@users.noreply.github.com> Date: Sun, 5 Jan 2025 19:37:02 +0530 Subject: [PATCH] Implement speed limit option (#51) --- .../cpp/skyline/common/android_settings.h | 2 ++ app/src/main/cpp/skyline/common/settings.h | 2 ++ .../cpp/skyline/gpu/presentation_engine.cpp | 18 ++++++++++ .../cpp/skyline/gpu/presentation_engine.h | 2 ++ app/src/main/cpp/skyline/jvm.h | 4 +++ .../skyline/preference/SeekBarPreference.kt | 35 +++++++++++++++---- .../emu/skyline/settings/EmulationSettings.kt | 2 ++ .../skyline/settings/GameSettingsFragment.kt | 33 +++++++++++++---- .../settings/GlobalSettingsFragment.kt | 8 +++++ .../emu/skyline/settings/NativeSettings.kt | 4 +++ app/src/main/res/values/strings.xml | 4 +++ .../main/res/xml/emulation_preferences.xml | 17 +++++++-- 12 files changed, 115 insertions(+), 16 deletions(-) diff --git a/app/src/main/cpp/skyline/common/android_settings.h b/app/src/main/cpp/skyline/common/android_settings.h index 3fea5cec..7fa8df6d 100644 --- a/app/src/main/cpp/skyline/common/android_settings.h +++ b/app/src/main/cpp/skyline/common/android_settings.h @@ -31,6 +31,8 @@ namespace skyline { } void Update() override { + enableSpeedLimit = ktSettings.GetBool("enableSpeedLimit"); + speedLimit = ktSettings.GetFloat("speedLimit"); isDocked = ktSettings.GetBool("isDocked"); usernameValue = std::move(ktSettings.GetString("usernameValue")); profilePictureValue = ktSettings.GetString("profilePictureValue"); diff --git a/app/src/main/cpp/skyline/common/settings.h b/app/src/main/cpp/skyline/common/settings.h index 82abfea9..b1df5875 100644 --- a/app/src/main/cpp/skyline/common/settings.h +++ b/app/src/main/cpp/skyline/common/settings.h @@ -61,6 +61,8 @@ namespace skyline { public: // System + Setting enableSpeedLimit; + Setting speedLimit; Setting isDocked; //!< If the emulated Switch should be handheld or docked Setting usernameValue; //!< The user name to be supplied to the guest Setting profilePictureValue; //!< The profile picture path to be supplied to the guest diff --git a/app/src/main/cpp/skyline/gpu/presentation_engine.cpp b/app/src/main/cpp/skyline/gpu/presentation_engine.cpp index ad319792..5e582ac4 100644 --- a/app/src/main/cpp/skyline/gpu/presentation_engine.cpp +++ b/app/src/main/cpp/skyline/gpu/presentation_engine.cpp @@ -238,6 +238,7 @@ namespace skyline::gpu { } else { frameTimestamp = timestamp; } + if (*state.settings->enableSpeedLimit) LimitSpeed(constant::NsInSecond / 60); } void PresentationEngine::PresentationThread() { @@ -448,6 +449,23 @@ namespace skyline::gpu { return nextFrameId++; } + void PresentationEngine::LimitSpeed(i64 targetFrameTimeNs) { + static i64 lastFrameTime = 0; + i64 currentTime = util::GetTimeNs(); + + i64 adjustedFrameTimeNs = static_cast(targetFrameTimeNs / (*state.settings->speedLimit / 100.0f)); + + if (lastFrameTime != 0) { + i64 elapsedTime = currentTime - lastFrameTime; + if (elapsedTime < adjustedFrameTimeNs) { + // Sleep for the remaining time to meet the adjusted frame time + std::this_thread::sleep_for(std::chrono::nanoseconds(adjustedFrameTimeNs - elapsedTime)); + } + } + + lastFrameTime = util::GetTimeNs(); // Update last frame time + } + void PresentationEngine::Pause() { paused.store(true, std::memory_order_release); LOGI("PresentationEngine paused."); diff --git a/app/src/main/cpp/skyline/gpu/presentation_engine.h b/app/src/main/cpp/skyline/gpu/presentation_engine.h index 14e5c4e8..d37ecff8 100644 --- a/app/src/main/cpp/skyline/gpu/presentation_engine.h +++ b/app/src/main/cpp/skyline/gpu/presentation_engine.h @@ -105,6 +105,8 @@ namespace skyline::gpu { */ void UpdateSwapchain(texture::Format format, texture::Dimensions extent); + void LimitSpeed(i64 targetFrameTimeNs); + public: PresentationEngine(const DeviceState &state, GPU &gpu); diff --git a/app/src/main/cpp/skyline/jvm.h b/app/src/main/cpp/skyline/jvm.h index 8a7993bc..da94561b 100644 --- a/app/src/main/cpp/skyline/jvm.h +++ b/app/src/main/cpp/skyline/jvm.h @@ -68,6 +68,10 @@ namespace skyline { JniString GetString(const std::string_view &key) { return {env, static_cast(env->GetObjectField(settingsInstance, env->GetFieldID(settingsClass, key.data(), "Ljava/lang/String;")))}; } + + float GetFloat(const std::string_view &key) { + return static_cast(env->GetFloatField(settingsInstance, env->GetFieldID(settingsClass, key.data(), "F"))); + } }; /** diff --git a/app/src/main/java/emu/skyline/preference/SeekBarPreference.kt b/app/src/main/java/emu/skyline/preference/SeekBarPreference.kt index 74a88e40..3ffddb16 100644 --- a/app/src/main/java/emu/skyline/preference/SeekBarPreference.kt +++ b/app/src/main/java/emu/skyline/preference/SeekBarPreference.kt @@ -2,7 +2,9 @@ package emu.skyline.preference import android.content.Context +import android.content.res.TypedArray import android.util.AttributeSet +import android.util.Log import android.view.LayoutInflater import android.view.View import androidx.preference.DialogPreference @@ -35,13 +37,17 @@ class SeekBarPreference(context: Context, attrs: AttributeSet) : DialogPreferenc } } + override fun onGetDefaultValue(a: TypedArray, index: Int): Any { + return a.getInt(index, 0) + } + override fun onClick() { showMaterialDialog() } private fun showMaterialDialog() { val dialogView = LayoutInflater.from(context).inflate(R.layout.preference_dialog_seekbar, null) val slider = dialogView.findViewById(R.id.seekBar) val valueText = dialogView.findViewById(R.id.value) - + // Configure slider slider.valueFrom = if (isPercentage) minValue.toFloat() else minValue.toInt().toFloat() slider.valueTo = if (isPercentage) maxValue.toFloat() else maxValue.toInt().toFloat() @@ -56,11 +62,14 @@ class SeekBarPreference(context: Context, attrs: AttributeSet) : DialogPreferenc currentValue = if (isPercentage) value else value.toInt() } + var dismissTrigger: String? = null + // Build and show the MaterialAlertDialog MaterialAlertDialogBuilder(context) .setTitle(title) .setView(dialogView) .setPositiveButton(android.R.string.ok) { _, _ -> + dismissTrigger = "positive_button" if (isPercentage) { persistFloat(currentValue.toFloat()) } else { @@ -69,8 +78,10 @@ class SeekBarPreference(context: Context, attrs: AttributeSet) : DialogPreferenc updateSummary() callChangeListener(currentValue) } - .setNegativeButton(android.R.string.cancel) { _, _ -> - slider.value = summary.toString().replace("%", "").toInt().toFloat() + .setNegativeButton(android.R.string.cancel, null) + .setOnDismissListener { + if (dismissTrigger != "positive_button") + slider.value = summary?.toString()?.replace("%", "")?.toIntOrNull()?.toFloat() ?: minValue.toFloat() } .show() } @@ -92,11 +103,21 @@ class SeekBarPreference(context: Context, attrs: AttributeSet) : DialogPreferenc } override fun onSetInitialValue(defaultValue: Any?) { - currentValue = if (isPercentage) { - getPersistedFloat((defaultValue as? Float) ?: minValue.toFloat()).toFloat() - } else { - getPersistedInt((defaultValue as? Int) ?: minValue.toInt()) + val actualDefaultValue = when (defaultValue) { + is String -> defaultValue.toIntOrNull() ?: minValue.toInt() + is Int -> defaultValue ?: minValue.toInt() + is Float -> defaultValue.toInt() + else -> minValue.toInt() // fallback to minValue if default is invalid } + currentValue = if (!isPercentage) getPersistedInt(actualDefaultValue!!)!! else getPersistedFloat(actualDefaultValue.toFloat()!!).toInt()!! updateSummary() } + + fun setMaxValue(max: Any) { + if (isPercentage) maxValue = max as Float else maxValue = max as Int + } + + fun setMinValue(min: Any) { + if (isPercentage) minValue = min as Float else minValue = min as Int + } } diff --git a/app/src/main/java/emu/skyline/settings/EmulationSettings.kt b/app/src/main/java/emu/skyline/settings/EmulationSettings.kt index d135528a..bfb9b385 100644 --- a/app/src/main/java/emu/skyline/settings/EmulationSettings.kt +++ b/app/src/main/java/emu/skyline/settings/EmulationSettings.kt @@ -24,6 +24,8 @@ class EmulationSettings private constructor(context : Context, prefName : String var useCustomSettings by sharedPreferences(context, false, prefName = prefName) // System + var enableSpeedLimit by sharedPreferences(context, false, prefName = prefName) + var speedLimit by sharedPreferences(context, 100f, prefName = prefName) var isDocked by sharedPreferences(context, true, prefName = prefName) var usernameValue by sharedPreferences(context, context.getString(R.string.username_default), prefName = prefName) var profilePictureValue by sharedPreferences(context, "", prefName = prefName) diff --git a/app/src/main/java/emu/skyline/settings/GameSettingsFragment.kt b/app/src/main/java/emu/skyline/settings/GameSettingsFragment.kt index e32090aa..d0e045b2 100644 --- a/app/src/main/java/emu/skyline/settings/GameSettingsFragment.kt +++ b/app/src/main/java/emu/skyline/settings/GameSettingsFragment.kt @@ -50,20 +50,23 @@ class GameSettingsFragment : PreferenceFragmentCompat() { findPreference("category_debug") ).forEach { it?.dependency = "use_custom_settings" } + findPreference("enable_speed_limit")?.setOnPreferenceChangeListener { _, newValue -> + disablePreference("speed_limit", !(newValue as Boolean), null) + true + } + // Only show validation layer setting in debug builds @Suppress("SENSELESS_COMPARISON") if (BuildConfig.BUILD_TYPE != "release") findPreference("validation_layer")?.isVisible = true - if (!GpuDriverHelper.supportsForceMaxGpuClocks()) { - val forceMaxGpuClocksPref = findPreference("force_max_gpu_clocks")!! - forceMaxGpuClocksPref.isSelectable = false - forceMaxGpuClocksPref.isChecked = false - forceMaxGpuClocksPref.summary = context!!.getString(R.string.force_max_gpu_clocks_desc_unsupported) - } - findPreference("gpu_driver")?.item = item + findPreference("enable_speed_limit")?.isChecked?.let { + disablePreference("speed_limit", !it, null) + } + disablePreference("force_max_gpu_clocks", !GpuDriverHelper.supportsForceMaxGpuClocks(), context?.getString(R.string.force_max_gpu_clocks_desc_unsupported)) + // Hide settings that don't support per-game configuration var prefToRemove = findPreference("profile_picture_value") prefToRemove?.parent?.removePreference(prefToRemove) @@ -76,4 +79,20 @@ class GameSettingsFragment : PreferenceFragmentCompat() { if (BuildConfig.BUILD_TYPE == "release") findPreference("category_debug")?.isVisible = false } + + private fun disablePreference( + preferenceId: String, + isDisabled: Boolean, + disabledSummary: String? = null + ) { + val preference = findPreference(preferenceId)!! + preference.isSelectable = !isDisabled + preference.isEnabled = !isDisabled + if (preference is TwoStatePreference && isDisabled) { + preference.isChecked = false + } + if (isDisabled && disabledSummary != null) { + preference.summary = disabledSummary + } + } } diff --git a/app/src/main/java/emu/skyline/settings/GlobalSettingsFragment.kt b/app/src/main/java/emu/skyline/settings/GlobalSettingsFragment.kt index 6708bce1..fda5e710 100644 --- a/app/src/main/java/emu/skyline/settings/GlobalSettingsFragment.kt +++ b/app/src/main/java/emu/skyline/settings/GlobalSettingsFragment.kt @@ -53,6 +53,11 @@ class GlobalSettingsFragment : PreferenceFragmentCompat() { true } + findPreference("enable_speed_limit")?.setOnPreferenceChangeListener { _, newValue -> + disablePreference("speed_limit", !(newValue as Boolean), null) + true + } + CoroutineScope(Dispatchers.IO).launch { WindowInfoTracker.getOrCreate(requireContext()).windowLayoutInfo(requireActivity()).collect { newLayoutInfo -> withContext(Dispatchers.Main) { @@ -68,6 +73,9 @@ class GlobalSettingsFragment : PreferenceFragmentCompat() { disablePreference("use_material_you", Build.VERSION.SDK_INT < Build.VERSION_CODES.S, null) disablePreference("force_max_gpu_clocks", !GpuDriverHelper.supportsForceMaxGpuClocks(), context!!.getString(R.string.force_max_gpu_clocks_desc_unsupported)) + findPreference("enable_speed_limit")?.isChecked?.let { + disablePreference("speed_limit", !it, null) + } resources.getStringArray(R.array.credits_entries).asIterable().shuffled().forEach { findPreference("category_credits")?.addPreference(Preference(context!!).apply { title = it diff --git a/app/src/main/java/emu/skyline/settings/NativeSettings.kt b/app/src/main/java/emu/skyline/settings/NativeSettings.kt index b4035c13..a1cafb9d 100644 --- a/app/src/main/java/emu/skyline/settings/NativeSettings.kt +++ b/app/src/main/java/emu/skyline/settings/NativeSettings.kt @@ -17,6 +17,8 @@ import kotlinx.serialization.Serializable @Suppress("unused") data class NativeSettings( // System + var enableSpeedLimit : Boolean, + var speedLimit : Float, var isDocked : Boolean, var usernameValue : String, var profilePictureValue : String, @@ -50,6 +52,8 @@ data class NativeSettings( var validationLayer : Boolean ) { constructor(context : Context, pref : EmulationSettings) : this( + pref.enableSpeedLimit, + pref.speedLimit, pref.isDocked, pref.usernameValue, pref.profilePictureValue, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7a794e6..7db798e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -97,6 +97,10 @@ Are you sure you want to copy global settings to this game? Current settings will be lost System + Enable Speed Limit + Emulation speed will be limited + Speed Limit + Set the emulation speed limit Use Docked Mode The system will emulate being in handheld mode The system will emulate being in docked mode diff --git a/app/src/main/res/xml/emulation_preferences.xml b/app/src/main/res/xml/emulation_preferences.xml index 1f56570a..7929f6c4 100644 --- a/app/src/main/res/xml/emulation_preferences.xml +++ b/app/src/main/res/xml/emulation_preferences.xml @@ -3,6 +3,19 @@ + +