add fps limiting option

This commit is contained in:
Chloe committed 2025-01-19 04:27:23 -07:00
1 parent 327e9b4d39
commit 53cd339213
8 files changed
+272 -10

No files matched your search

@@ -67,6 +67,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private var displayMode = DisplayMode.DEFAULT private var displayMode = DisplayMode.DEFAULT
private var forceRefreshRate = false private var forceRefreshRate = false
private var mLimitedRefreshRate: Int? = null
private var mAspectRatio = 0.0f private var mAspectRatio = 0.0f
private var mScreenZoom = 1.0f private var mScreenZoom = 1.0f
private var mScreenZoomFit = false private var mScreenZoomFit = false
@@ -425,10 +426,17 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
val renderer = Cocos2dxRenderer(glSurfaceView) val renderer = Cocos2dxRenderer(glSurfaceView)
glSurfaceView.setCocos2dxRenderer(renderer) glSurfaceView.setCocos2dxRenderer(renderer)
if (forceRefreshRate) { val frameRate = mLimitedRefreshRate
// force refresh rate causes some issues if the limited framerate isn't supported by device
if (forceRefreshRate && frameRate == null) {
renderer.setFrameRate = true renderer.setFrameRate = true
} }
if (frameRate != null) {
renderer.limitFrameRate(frameRate)
}
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
glSurfaceView.cocos2dxEditText = editText glSurfaceView.cocos2dxEditText = editText
@@ -449,6 +457,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
mScreenZoomFit = preferenceUtils.getBoolean(PreferenceUtils.Key.SCREEN_ZOOM_FIT) mScreenZoomFit = preferenceUtils.getBoolean(PreferenceUtils.Key.SCREEN_ZOOM_FIT)
forceRefreshRate = preferenceUtils.getBoolean(PreferenceUtils.Key.FORCE_HRR) forceRefreshRate = preferenceUtils.getBoolean(PreferenceUtils.Key.FORCE_HRR)
mLimitedRefreshRate = preferenceUtils.getInt(PreferenceUtils.Key.LIMIT_FRAME_RATE).takeIf { it != 0 }
if (forceRefreshRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (forceRefreshRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val maxRefreshRate = display?.supportedModes?.maxBy { it.refreshRate }?.refreshRate val maxRefreshRate = display?.supportedModes?.maxBy { it.refreshRate }?.refreshRate
@@ -9,6 +9,7 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcher
@@ -52,13 +53,13 @@ import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.GamePackageUtils import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import java.net.ConnectException import java.net.ConnectException
import java.net.UnknownHostException import java.net.UnknownHostException
import kotlin.math.roundToInt
class SettingsActivity : ComponentActivity() { class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -356,6 +357,7 @@ fun SettingsScreen(
if (currentDisplayMode == 1) { if (currentDisplayMode == 1) {
SettingsStringSelectCard( SettingsStringSelectCard(
title = stringResource(R.string.preference_custom_aspect_ratio_name), title = stringResource(R.string.preference_custom_aspect_ratio_name),
description = stringResource(R.string.preference_custom_aspect_ratio_description),
dialogTitle = stringResource(R.string.preference_custom_aspect_ratio_select), dialogTitle = stringResource(R.string.preference_custom_aspect_ratio_select),
preferenceKey = PreferenceUtils.Key.CUSTOM_ASPECT_RATIO, preferenceKey = PreferenceUtils.Key.CUSTOM_ASPECT_RATIO,
options = linkedMapOf( options = linkedMapOf(
@@ -369,6 +371,7 @@ fun SettingsScreen(
SettingsRangeCard( SettingsRangeCard(
title = stringResource(R.string.preference_screen_zoom_name), title = stringResource(R.string.preference_screen_zoom_name),
description = stringResource(R.string.preference_screen_zoom_description),
dialogTitle = stringResource(R.string.preference_screen_zoom_select), dialogTitle = stringResource(R.string.preference_screen_zoom_select),
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM, preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM,
labelSuffix = "x", labelSuffix = "x",
@@ -382,6 +385,22 @@ fun SettingsScreen(
asCard = false asCard = false
) )
} }
val maxFrameRate = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.display.supportedModes.maxOf { it.refreshRate }
} else {
@Suppress("DEPRECATION")
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.refreshRate
}.roundToInt()
}
SettingsFPSCard(
title = stringResource(R.string.preference_limit_framerate_name),
dialogTitle = stringResource(R.string.preference_limit_framerate_select),
preferenceKey = PreferenceUtils.Key.LIMIT_FRAME_RATE,
maxFrameRate = maxFrameRate
)
} }
OptionsGroup(context.getString(R.string.preference_category_updater)) { OptionsGroup(context.getString(R.string.preference_category_updater)) {
@@ -3,17 +3,21 @@ package com.geode.launcher.preferences
import android.content.Context import android.content.Context
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
@@ -38,6 +42,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@@ -51,6 +56,8 @@ import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.LabelledText import com.geode.launcher.utils.LabelledText
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import kotlin.math.log10 import kotlin.math.log10
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -133,14 +140,15 @@ fun SettingsStringSelectCard(
dialogTitle: String, dialogTitle: String,
preferenceKey: PreferenceUtils.Key, preferenceKey: PreferenceUtils.Key,
options: Map<String, String>, options: Map<String, String>,
extraSelectBehavior: ((String?) -> Unit)? = null extraSelectBehavior: ((String?) -> Unit)? = null,
description: String? = null,
) { ) {
val preferenceValue by PreferenceUtils.useStringPreference(preferenceKey) val preferenceValue by PreferenceUtils.useStringPreference(preferenceKey)
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
OptionsCard( OptionsCard(
title = { OptionsTitle(title = title) }, title = { OptionsTitle(title = title, description = description) },
modifier = Modifier modifier = Modifier
.clickable( .clickable(
onClick = { onClick = {
@@ -322,6 +330,170 @@ fun RangeDialog(
) )
} }
@Composable
fun FrameRateDialog(
title: String,
onDismissRequest: () -> Unit,
onSelect: (Int) -> Unit,
initialValue: Int,
maxFrameRate: Int,
) {
var enteredValue by remember {
mutableStateOf(
if (initialValue == 0) maxFrameRate.toString()
else initialValue.toString()
)
}
var minFrameRate = 5
val currentValue = enteredValue.toIntOrNull()
var maximumReached = currentValue != null && currentValue > maxFrameRate
var minimumReached = currentValue != null && currentValue < minFrameRate
AlertDialog(
onDismissRequest = { onDismissRequest() },
title = {
Text(title)
},
text = {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1.0f, false)
) {
IconButton(onClick = {
val prevValue = max((currentValue ?: 0) - 5, minFrameRate)
enteredValue = prevValue.toString()
}, enabled = currentValue == null || currentValue > minFrameRate) {
Icon(
painterResource(R.drawable.icon_remove),
contentDescription = stringResource(R.string.preference_limit_framerate_subtract)
)
}
Spacer(Modifier.size(8.dp))
OutlinedTextField(
value = enteredValue.toString(),
onValueChange = {
enteredValue = it
},
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
suffix = {
Text(stringResource(R.string.preference_limit_framerate_suffix))
},
label = {
Text(stringResource(R.string.preference_limit_framerate_label))
},
isError = minimumReached || maximumReached,
modifier = Modifier.weight(1.0f),
)
Spacer(Modifier.size(8.dp))
IconButton(onClick = {
val nextValue = min((currentValue ?: 0) + 5, maxFrameRate)
enteredValue = nextValue.toString()
}, enabled = currentValue == null || currentValue < maxFrameRate) {
Icon(
Icons.Filled.Add,
contentDescription = stringResource(R.string.preference_limit_framerate_add)
)
}
IconButton(onClick = { onSelect(0) }) {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = stringResource(R.string.preference_limit_framerate_reset)
)
}
}
if (minimumReached) {
Spacer(Modifier.size(8.dp))
Text(
stringResource(R.string.preference_limit_framerate_error_min, minFrameRate),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.labelLarge,
)
} else if (maximumReached) {
Spacer(Modifier.size(8.dp))
Text(
stringResource(
R.string.preference_limit_framerate_error_max,
maxFrameRate
),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.labelLarge,
)
}
}
},
confirmButton = {
TextButton(
onClick = { onSelect(currentValue ?: 0) },
enabled = !minimumReached && !maximumReached && currentValue != null
) {
Text(stringResource(R.string.message_box_accept))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
},
)
}
@Composable
fun SettingsFPSCard(
title: String,
dialogTitle: String,
preferenceKey: PreferenceUtils.Key,
maxFrameRate: Int,
description: String? = null,
) {
var preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
var showDialog by remember { mutableStateOf(false) }
OptionsCard(
title = { OptionsTitle(title = title, description = description) },
modifier = Modifier
.clickable(
onClick = {
showDialog = true
},
role = Role.Button
)
) {
if (preferenceValue == 0) {
Text(stringResource(R.string.preference_limit_framerate_default))
} else {
Text(stringResource(R.string.preference_limit_framerate_value, preferenceValue))
}
}
if (showDialog) {
FrameRateDialog(
title = dialogTitle,
onDismissRequest = { showDialog = false },
onSelect = {
preferenceValue = it
showDialog = false
},
initialValue = preferenceValue,
maxFrameRate = maxFrameRate
)
}
}
@Composable @Composable
fun SettingsRangeCard( fun SettingsRangeCard(
title: String, title: String,
@@ -331,6 +503,7 @@ fun SettingsRangeCard(
range: IntRange, range: IntRange,
scale: Int, scale: Int,
step: Int, step: Int,
description: String? = null,
children: @Composable () -> Unit = {} children: @Composable () -> Unit = {}
) { ) {
var preferenceValue by PreferenceUtils.useIntPreference(preferenceKey) var preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
@@ -338,7 +511,9 @@ fun SettingsRangeCard(
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
OptionsCard( OptionsCard(
title = { OptionsTitle(title = title) }, title = {
OptionsTitle(title = title, description = description)
},
modifier = Modifier modifier = Modifier
.clickable( .clickable(
onClick = { onClick = {
@@ -469,7 +644,8 @@ fun OptionsButton(title: String, description: String? = null, icon: (@Composable
) )
}, },
modifier = Modifier modifier = Modifier
.clickable(onClick = onClick, role = Role.Button) .clickable(onClick = onClick, role = Role.Button),
wrapContent = true
) { ) {
if (displayInline && description != null) { if (displayInline && description != null) {
Text(description, textAlign = TextAlign.End) Text(description, textAlign = TextAlign.End)
@@ -487,7 +663,6 @@ fun SettingsCard(title: String, description: String? = null, icon: (@Composable
OptionsCard( OptionsCard(
title = { title = {
OptionsTitle( OptionsTitle(
Modifier.fillMaxWidth(0.75f),
title = title, title = title,
description = description, description = description,
icon = icon icon = icon
@@ -531,7 +706,7 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
} }
@Composable @Composable
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) { fun OptionsCard(modifier: Modifier = Modifier, wrapContent: Boolean = false, title: @Composable () -> Unit, content: @Composable () -> Unit) {
Row( Row(
modifier modifier
.fillMaxWidth() .fillMaxWidth()
@@ -540,7 +715,15 @@ fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, co
Arrangement.SpaceBetween, Arrangement.SpaceBetween,
Alignment.CenterVertically, Alignment.CenterVertically,
) { ) {
if (!wrapContent) {
Box(modifier = Modifier.weight(1.0f)) {
title() title()
}
Spacer(modifier = Modifier.size(12.dp))
} else {
title()
}
content() content()
} }
} }
@@ -146,6 +146,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
CUSTOM_ASPECT_RATIO, CUSTOM_ASPECT_RATIO,
SCREEN_ZOOM, SCREEN_ZOOM,
SCREEN_ZOOM_FIT, SCREEN_ZOOM_FIT,
LIMIT_FRAME_RATE
} }
private fun defaultValueForBooleanKey(key: Key): Boolean { private fun defaultValueForBooleanKey(key: Key): Boolean {
@@ -192,6 +193,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.CUSTOM_ASPECT_RATIO -> "PreferenceCustomAspectRatio" Key.CUSTOM_ASPECT_RATIO -> "PreferenceCustomAspectRatio"
Key.SCREEN_ZOOM -> "PreferenceScreenZoom" Key.SCREEN_ZOOM -> "PreferenceScreenZoom"
Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit" Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit"
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
} }
} }
@@ -115,9 +115,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
fun updateRefreshRate() { fun updateRefreshRate() {
val maxRefreshRate = display.supportedModes.maxBy { it.refreshRate } val maxRefreshRate = display.supportedModes.maxBy { it.refreshRate }.refreshRate
holder.surface.setFrameRate( holder.surface.setFrameRate(
maxRefreshRate.refreshRate, maxRefreshRate,
Surface.FRAME_RATE_COMPATIBILITY_DEFAULT Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
) )
} }
@@ -5,6 +5,9 @@ import android.os.Build
import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10 import javax.microedition.khronos.opengles.GL10
private const val NANOSECONDS_PER_SECOND = 1000000000L
private const val NANOSECONDS_PER_MICROSECOND = 1000000L
@Suppress("unused", "KotlinJniMissingFunction") @Suppress("unused", "KotlinJniMissingFunction")
class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceView.Renderer { class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceView.Renderer {
companion object { companion object {
@@ -62,6 +65,11 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
private var screenHeight = 0 private var screenHeight = 0
var sendResizeEvents = false var sendResizeEvents = false
var setFrameRate = false var setFrameRate = false
private var mAnimationInterval: Long? = null
fun limitFrameRate(rate: Int) {
mAnimationInterval = NANOSECONDS_PER_SECOND/rate
}
fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) { fun setScreenWidthAndHeight(surfaceWidth: Int, surfaceHeight: Int) {
screenWidth = surfaceWidth screenWidth = surfaceWidth
@@ -80,7 +88,23 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
} }
override fun onDrawFrame(gl: GL10?) { override fun onDrawFrame(gl: GL10?) {
val animationInterval = mAnimationInterval
if (animationInterval == null) {
nativeRender() nativeRender()
} else {
nativeRender()
var now = System.nanoTime()
var interval = now - this.lastTickInNanoSeconds
if (interval < animationInterval) {
try {
Thread.sleep((animationInterval - interval) / NANOSECONDS_PER_MICROSECOND)
} catch (e: Exception) {}
}
lastTickInNanoSeconds = System.nanoTime()
}
} }
fun handleActionDown(id: Int, x: Float, y: Float) { fun handleActionDown(id: Int, x: Float, y: Float) {
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M200,520L200,440L760,440L760,520L200,520Z"/>
</vector>
+15
View File
@@ -141,6 +141,7 @@
<string name="preference_display_mode_default">Default</string> <string name="preference_display_mode_default">Default</string>
<string name="preference_custom_aspect_ratio_name">Aspect ratio</string> <string name="preference_custom_aspect_ratio_name">Aspect ratio</string>
<string name="preference_custom_aspect_ratio_description">Limits aspect ratio of the game.</string>
<string name="preference_custom_aspect_ratio_select">Select aspect ratio</string> <string name="preference_custom_aspect_ratio_select">Select aspect ratio</string>
<string name="preference_custom_aspect_ratio_16x9">16:9</string> <string name="preference_custom_aspect_ratio_16x9">16:9</string>
<string name="preference_custom_aspect_ratio_16x10">16:10</string> <string name="preference_custom_aspect_ratio_16x10">16:10</string>
@@ -148,9 +149,23 @@
<string name="preference_custom_aspect_ratio_1x1">1:1</string> <string name="preference_custom_aspect_ratio_1x1">1:1</string>
<string name="preference_screen_zoom_name">Screen zoom</string> <string name="preference_screen_zoom_name">Screen zoom</string>
<string name="preference_screen_zoom_description">Reduces the size of the game window.</string>
<string name="preference_screen_zoom_select">Edit screen zoom</string> <string name="preference_screen_zoom_select">Edit screen zoom</string>
<string name="preference_screen_zoom_fit_name">Fit to screen</string> <string name="preference_screen_zoom_fit_name">Fit to screen</string>
<string name="preference_limit_framerate_name">Limit max framerate</string>
<string name="preference_limit_framerate_description">Limits framerate during rendering.</string>
<string name="preference_limit_framerate_select">Edit maximum framerate</string>
<string name="preference_limit_framerate_default">Disabled</string>
<string name="preference_limit_framerate_label">Framerate</string>
<string name="preference_limit_framerate_suffix">FPS</string>
<string name="preference_limit_framerate_value">%1$d FPS</string>
<string name="preference_limit_framerate_error_max">This device supports at most %1$s FPS.</string>
<string name="preference_limit_framerate_error_min">Framerate must be at least %1$s FPS.</string>
<string name="preference_limit_framerate_subtract">Decrement</string>
<string name="preference_limit_framerate_add">Increment</string>
<string name="preference_limit_framerate_reset">Disable</string>
<string name="preference_category_updater">Updates</string> <string name="preference_category_updater">Updates</string>
<string name="preference_update_automatically_name">Enable automatic updates</string> <string name="preference_update_automatically_name">Enable automatic updates</string>
<string name="preference_release_channel_name">Use nightly release channel</string> <string name="preference_release_channel_name">Use nightly release channel</string>