Android: offer eeClampMode 4 as Ludicrous in the clamping pickers

The tier existed in the core but nowhere in the UI, on any frontend, so
reaching it meant hand-editing the settings file. Both Android pickers —
the Performance tab and the in-game pause menu — now carry a fifth
option, and the settings layer packs it.

The packing is the part that matters. emucore validates the four clamp
booleans as a cascade and silently resets an inconsistent set to the
defaults rather than rejecting it, so writing fpuExactMode without its
three predecessors would not fail loudly, it would quietly land the user
back on Normal. applyTo therefore writes all four cumulatively, and
readFromIni reads them back highest-first.

readFromIni treats a missing fpuExactMode as an older core rather than as
mode 3: a build without the key never wrote it, and inferring 3 there
would demote a Ludicrous setting every time the settings were reloaded
under a mixed pair of builds.

The chip row already scrolls horizontally, so a fifth option needs no
layout change.

⚠️ Not addressed here, and worth a decision: the GameDB overwrites the
whole tier for any title carrying an eeClampMode entry, and an entry
below 4 clears the exact bit outright. On those ~115 titles the new
option is inert unless game fixes are off — which is most of the titles
whose users would want it. The setting description says so; whether the
core should let a user's choice raise the database's is a separate call.
This commit is contained in:
Brian Degenhardt
2026-08-16 11:09:47 -07:00
parent 99299e556c
commit 541b1abfaf
4 changed files with 26 additions and 10 deletions
@@ -69,8 +69,15 @@ data class Settings(
val eeCycleRate: Int = 0,
/** EmuCore/Speedhacks/EECycleSkip — 0..3. 0 = no skip. */
val eeCycleSkip: Int = 0,
/** EE/FPU clamp mode — 0 None / 1 Normal / 2 Extra / 3 Full (PCSX2 default Normal).
* Unpacks to EmuCore/CPU/Recompiler fpuOverflow/fpuExtraOverflow/fpuFullMode. */
/** EE/FPU clamp mode — 0 None / 1 Normal / 2 Extra / 3 Full / 4 Ludicrous
* (PCSX2 default Normal). Unpacks to EmuCore/CPU/Recompiler
* fpuOverflow/fpuExtraOverflow/fpuFullMode/fpuExactMode.
* 4 is Full plus the rest of the EE multiplier's one-ULP deficit and a
* divide/sqrt/rsqrt that runs the unit's own recurrence out of line, so it
* costs a call per divide. ⚠️ The GameDB overwrites this whole tier for any
* title carrying an eeClampMode entry, and an entry below 4 CLEARS the
* exact bit — so on those titles the choice is inert unless game fixes are
* off. Keep any bound in the pickers in sync with this list. */
val eeClampMode: Int = 1,
/** VU clamp mode — 0 None / 1 Normal / 2 Extra / 3 Extra+Sign (PCSX2 default Normal).
* Unpacks to vu0/vu1 Overflow/ExtraOverflow/SignOverflow. */
@@ -729,6 +736,10 @@ data class Settings(
put("EmuCore/CPU/Recompiler", "fpuOverflow", "bool", (eeClampMode >= 1).toString())
put("EmuCore/CPU/Recompiler", "fpuExtraOverflow", "bool", (eeClampMode >= 2).toString())
put("EmuCore/CPU/Recompiler", "fpuFullMode", "bool", (eeClampMode >= 3).toString())
// The four are cumulative and emucore validates them as such: an
// inconsistent set is silently reset to defaults on load rather than
// rejected, so all four go out together or none of them mean anything.
put("EmuCore/CPU/Recompiler", "fpuExactMode", "bool", (eeClampMode >= 4).toString())
for (vu in arrayOf("vu0", "vu1")) {
put("EmuCore/CPU/Recompiler", "${vu}Overflow", "bool", (vuClampMode >= 1).toString())
put("EmuCore/CPU/Recompiler", "${vu}ExtraOverflow", "bool", (vuClampMode >= 2).toString())
@@ -960,14 +971,18 @@ data class Settings(
fun floatAt(key: String): Float? = ini[key]?.toFloatOrNull()
fun strAt(key: String): String? = ini[key]
// EE/FPU clamp (0 None / 1 Normal / 2 Extra / 3 Full) is packed by applyTo into
// three cumulative bool keys (fpuOverflow>=1, fpuExtraOverflow>=2, fpuFullMode>=3).
// EE/FPU clamp (0 None / 1 Normal / 2 Extra / 3 Full / 4 Ludicrous) is packed by
// applyTo into four cumulative bool keys (fpuOverflow>=1, fpuExtraOverflow>=2,
// fpuFullMode>=3, fpuExactMode>=4). Read them back highest-first, and treat the
// exact key's absence as an older core rather than as mode 3 — a build without it
// never wrote the key, and inferring 3 there would silently demote the setting.
val eeClamp = run {
val fo = boolAt("EmuCore/CPU/Recompiler/fpuOverflow")
val fe = boolAt("EmuCore/CPU/Recompiler/fpuExtraOverflow")
val ff = boolAt("EmuCore/CPU/Recompiler/fpuFullMode")
if (fo == null && fe == null && ff == null) this.eeClampMode
else if (ff == true) 3 else if (fe == true) 2 else if (fo == true) 1 else 0
val fx = boolAt("EmuCore/CPU/Recompiler/fpuExactMode")
if (fo == null && fe == null && ff == null && fx == null) this.eeClampMode
else if (fx == true) 4 else if (ff == true) 3 else if (fe == true) 2 else if (fo == true) 1 else 0
}
// VU clamp (0 None / 1 Normal / 2 Extra / 3 Extra+Sign) — same packing on vu0*
// (applyTo writes vu0 and vu1 identically, so reading vu0 recovers the mode).
@@ -1022,6 +1022,7 @@ val EN: Map<String, String> = mapOf(
"perf.clamp.extra" to "Extra",
"perf.clamp.extraSign" to "Extra+Sign",
"perf.clamp.full" to "Full",
"perf.clamp.ludicrous" to "Ludicrous",
"perf.clamp.none" to "None",
"perf.clamp.normal" to "Normal",
"perf.displayFpsCap.description" to "Caps the PRESENTED (display) frame rate — emulation keeps full speed, so this is a display cap, not true emulation FPS. Any value works (use with Speed Limit % to fine-tune per game). 0 = off; values between the clean rates (60/30/20/15) pace a little unevenly.",
@@ -1039,7 +1040,7 @@ val EN: Map<String, String> = mapOf(
"perf.eeCycleRate.label" to "EE Cycle Rate",
"perf.eeCycleSkip.description" to "Skips EE cycles for speed. Can cause stutter, physics bugs, or crashes.",
"perf.eeCycleSkip.label" to "EE Cycle Skip",
"perf.eeFpuClamping.description" to "FPU overflow/rounding accuracy. Restart the game to apply.",
"perf.eeFpuClamping.description" to "FPU overflow/rounding accuracy. Ludicrous reproduces the EE's multiplier and divider bit-for-bit, for the few games that check results exactly — it is the slowest setting by some way. Games that carry their own clamping entry in the game database keep that entry instead of this. Restart the game to apply.",
"perf.eeFpuClamping.label" to "EE/FPU Clamping",
"perf.eeFpuRoundMode.description" to "EE FPU float rounding. Chop (toward zero) is the PS2 default — change only if a game needs it. Restart the game to apply.",
"perf.eeFpuRoundMode.label" to "EE FPU Round Mode",
@@ -1031,7 +1031,7 @@ private fun PerformancePane(state: EmulationMenuUiState, viewModel: EmulationMen
)
HorizontalOptions(
title = str("perf.eeFpuClamping.label"),
options = listOf(str("perf.clamp.none"), str("perf.clamp.normal"), str("perf.clamp.extra"), str("perf.clamp.full"))
options = listOf(str("perf.clamp.none"), str("perf.clamp.normal"), str("perf.clamp.extra"), str("perf.clamp.full"), str("perf.clamp.ludicrous"))
.mapIndexed { index, label -> index to label },
selected = settings.eeClampMode,
onSelect = { value -> viewModel.updateSettings { it.copy(eeClampMode = value) } },
@@ -244,8 +244,8 @@ fun PerformanceTab(state: MutableState<Settings>) {
// at a speed cost. Needs a recompiler reset, so restart the game.
SegmentedRow(
label = str("perf.eeFpuClamping.label"),
options = listOf(str("perf.clamp.none"), str("perf.clamp.normal"), str("perf.clamp.extra"), str("perf.clamp.full")),
selectedIndex = s.eeClampMode.coerceIn(0, 3),
options = listOf(str("perf.clamp.none"), str("perf.clamp.normal"), str("perf.clamp.extra"), str("perf.clamp.full"), str("perf.clamp.ludicrous")),
selectedIndex = s.eeClampMode.coerceIn(0, 4),
description = str("perf.eeFpuClamping.description"),
onChange = { apply(s.copy(eeClampMode = it)) },
)