Skip to content

Commit

Permalink
Implement speed limit option (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ishan09811 authored Jan 5, 2025
1 parent 0a710ea commit 5302dad
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 16 deletions.
2 changes: 2 additions & 0 deletions app/src/main/cpp/skyline/common/android_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/cpp/skyline/common/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ namespace skyline {

public:
// System
Setting<bool> enableSpeedLimit;
Setting<float> speedLimit;
Setting<bool> isDocked; //!< If the emulated Switch should be handheld or docked
Setting<std::string> usernameValue; //!< The user name to be supplied to the guest
Setting<std::string> profilePictureValue; //!< The profile picture path to be supplied to the guest
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/cpp/skyline/gpu/presentation_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ namespace skyline::gpu {
} else {
frameTimestamp = timestamp;
}
if (*state.settings->enableSpeedLimit) LimitSpeed(constant::NsInSecond / 60);
}

void PresentationEngine::PresentationThread() {
Expand Down Expand Up @@ -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<i64>(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.");
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/cpp/skyline/gpu/presentation_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/cpp/skyline/jvm.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ namespace skyline {
JniString GetString(const std::string_view &key) {
return {env, static_cast<jstring>(env->GetObjectField(settingsInstance, env->GetFieldID(settingsClass, key.data(), "Ljava/lang/String;")))};
}

float GetFloat(const std::string_view &key) {
return static_cast<float>(env->GetFloatField(settingsInstance, env->GetFieldID(settingsClass, key.data(), "F")));
}
};

/**
Expand Down
35 changes: 28 additions & 7 deletions app/src/main/java/emu/skyline/preference/SeekBarPreference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Slider>(R.id.seekBar)
val valueText = dialogView.findViewById<MaterialTextView>(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()
Expand All @@ -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 {
Expand All @@ -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()
}
Expand All @@ -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
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/emu/skyline/settings/EmulationSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 26 additions & 7 deletions app/src/main/java/emu/skyline/settings/GameSettingsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,23 @@ class GameSettingsFragment : PreferenceFragmentCompat() {
findPreference("category_debug")
).forEach { it?.dependency = "use_custom_settings" }

findPreference<Preference>("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<Preference>("validation_layer")?.isVisible = true

if (!GpuDriverHelper.supportsForceMaxGpuClocks()) {
val forceMaxGpuClocksPref = findPreference<TwoStatePreference>("force_max_gpu_clocks")!!
forceMaxGpuClocksPref.isSelectable = false
forceMaxGpuClocksPref.isChecked = false
forceMaxGpuClocksPref.summary = context!!.getString(R.string.force_max_gpu_clocks_desc_unsupported)
}

findPreference<GpuDriverPreference>("gpu_driver")?.item = item

findPreference<SwitchPreferenceCompat>("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<Preference>("profile_picture_value")
prefToRemove?.parent?.removePreference(prefToRemove)
Expand All @@ -76,4 +79,20 @@ class GameSettingsFragment : PreferenceFragmentCompat() {
if (BuildConfig.BUILD_TYPE == "release")
findPreference<PreferenceCategory>("category_debug")?.isVisible = false
}

private fun disablePreference(
preferenceId: String,
isDisabled: Boolean,
disabledSummary: String? = null
) {
val preference = findPreference<Preference>(preferenceId)!!
preference.isSelectable = !isDisabled
preference.isEnabled = !isDisabled
if (preference is TwoStatePreference && isDisabled) {
preference.isChecked = false
}
if (isDisabled && disabledSummary != null) {
preference.summary = disabledSummary
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class GlobalSettingsFragment : PreferenceFragmentCompat() {
true
}

findPreference<Preference>("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) {
Expand All @@ -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<SwitchPreferenceCompat>("enable_speed_limit")?.isChecked?.let {
disablePreference("speed_limit", !it, null)
}
resources.getStringArray(R.array.credits_entries).asIterable().shuffled().forEach {
findPreference<PreferenceCategory>("category_credits")?.addPreference(Preference(context!!).apply {
title = it
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/emu/skyline/settings/NativeSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@
<string name="copy_global_settings_warning">Are you sure you want to copy global settings to this game? <b>Current settings will be lost</b></string>
<!-- Settings - System -->
<string name="system">System</string>
<string name="enable_speed_limit">Enable Speed Limit</string>
<string name="enable_speed_limit_desc">Emulation speed will be limited</string>
<string name="speed_limit">Speed Limit</string>
<string name="speed_limit_desc">Set the emulation speed limit</string>
<string name="use_docked">Use Docked Mode</string>
<string name="handheld_enabled">The system will emulate being in handheld mode</string>
<string name="docked_enabled">The system will emulate being in docked mode</string>
Expand Down
17 changes: 15 additions & 2 deletions app/src/main/res/xml/emulation_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
<PreferenceCategory
android:key="category_system"
android:title="@string/system">
<SwitchPreferenceCompat
android:defaultValue="false"
android:summary="@string/enable_speed_limit_desc"
app:key="enable_speed_limit"
app:title="@string/enable_speed_limit" />
<emu.skyline.preference.SeekBarPreference
android:summary="@string/speed_limit_desc"
android:defaultValue="100"
android:key="speed_limit"
android:title="@string/speed_limit"
app:maxValue="200"
app:minValue="0"
app:isPercentage="true" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:summaryOff="@string/handheld_enabled"
Expand Down Expand Up @@ -148,16 +161,16 @@
app:title="@string/vsync_mode"
app:useSimpleSummaryProvider="true"/>
<emu.skyline.preference.SeekBarPreference
android:defaultValue="4"
android:summary="@string/executor_slot_count_scale_desc"
android:defaultValue="4"
android:key="executor_slot_count_scale"
android:title="@string/executor_slot_count_scale"
app:maxValue="6"
app:minValue="1"
app:isPercentage="false" />
<emu.skyline.preference.SeekBarPreference
android:defaultValue="256"
android:summary="@string/executor_flush_threshold_desc"
android:defaultValue="256"
android:key="executor_flush_threshold"
android:title="@string/executor_flush_threshold"
app:maxValue="1024"
Expand Down

0 comments on commit 5302dad

Please sign in to comment.