Let the user turn save states on

Save states could not be taken at all. Saving has to stop every SPU somewhere it
can be serialised from, which is what Compatible Savestate Mode does, and this
port wrote that setting to false on every boot -- so the save failed with
"missing SPU setting" no matter what the user did, and nothing on screen
connected the two.

The reasoning was sound: the mode costs SPU performance, and a PS3 state runs
500MB to 3GB, so a few saves fill a phone. Both of those are costs to disclose,
not reasons to decide for someone. It is now a setting, still off by default, so
nobody pays for a feature they did not ask for and installs from the window when
it was pushed as true are corrected by the same write.

Placed with the SPU rows rather than under a savestate heading, because that is
where the cost lands, and the description says what both costs are before the
switch is touched.
This commit is contained in:
jpolo1224
2026-08-10 11:47:41 -04:00
parent 431b6d0925
commit 44f62f310c
4 changed files with 32 additions and 13 deletions
@@ -99,6 +99,15 @@ data class Ps3Settings(
val spuCache: Boolean = true,
val llvmPrecompile: Boolean = true,
val accurateSpuDma: Boolean = false,
/** Locks every SPU thread into a state a savestate can be serialised from.
*
* Savestates cannot be taken without it: the save has to stop each SPU somewhere it can
* be written out, and with this off that fails on any title with SPU work running. It is
* off by default because it costs SPU performance -- which is why upstream defaults it
* off too -- and because a PS3 savestate runs 500MB to 3GB, so a handful of them fills a
* phone. Both costs are the user's to accept, which is the only reason this is a setting
* rather than a decision made for them. */
val savestateCompatibleMode: Boolean = false,
val clocksScale: Int = 100,
val resolutionScale: Int = 100,
/** 0 = Disabled. Off by default: mobile drivers routinely lack the MSAA
@@ -949,6 +958,7 @@ data class Settings(
put("PS3/Core", "SPU Cache", "bool", ps3.spuCache.toString())
put("PS3/Core", "LLVM Precompilation", "bool", ps3.llvmPrecompile.toString())
put("PS3/Core", "Accurate SPU DMA", "bool", ps3.accurateSpuDma.toString())
put("Savestate", "Compatible Savestate Mode", "bool", ps3.savestateCompatibleMode.toString())
put("PS3/Core", "Clocks scale", "int", ps3.clocksScale.toString())
// From upscaleFloat, which is the control that exists.
//
@@ -1248,19 +1258,12 @@ data class Settings(
// above it would already be 60; setting it explicitly means the cap does not depend
// on the vblank path holding, which it did not. Enum node, so the value is quoted.
runCatching { net.rpcsx.RPCSX.instance.settingsSet("Video@@Frame limit", "\"60\"") }
// Held at the upstream default, which is off.
//
// Savestates cannot work without it: saving has to lock every SPU thread into a state
// it can be serialised from, and with this off that lock fails on any title with SPU
// work running. It was turned on for exactly that reason and then turned back off,
// because a PS3 savestate runs 500MB to 3GB and the feature was dropped rather than
// ship something that fills a phone in a handful of saves.
//
// Written explicitly rather than left alone: it was pushed as true for a while, so
// installs from that window have true persisted in config.yml and would keep paying
// for it. It costs SPU performance, which is the whole reason upstream defaults it
// off, and nothing here uses what it buys.
runCatching { net.rpcsx.RPCSX.instance.settingsSet("Savestate@@Compatible Savestate Mode", "false") }
// Compatible Savestate Mode is no longer forced off here; applyTo writes it from
// ps3.savestateCompatibleMode above, so the two costs it carries -- SPU performance
// and a 500MB to 3GB state file -- are the user's to accept rather than a decision
// taken for them. Still off by default, so nobody pays for a feature they did not ask
// for, and installs from the window where it was pushed as true are corrected by the
// same write.
// Settings a specific title needs in order to run at all, then the user's own core
// edits on top. Order matters: game defaults are a floor, an explicit user choice
@@ -1925,6 +1928,7 @@ data class Settings(
put("ps3SpuCache", ps3.spuCache)
put("ps3LlvmPrecompile", ps3.llvmPrecompile)
put("ps3AccurateSpuDma", ps3.accurateSpuDma)
put("ps3SavestateCompatibleMode", ps3.savestateCompatibleMode)
put("ps3ClocksScale", ps3.clocksScale)
put("ps3ResolutionScale", ps3.resolutionScale)
put("ps3MsaaMode", ps3.msaaMode)
@@ -2259,6 +2263,7 @@ data class Settings(
spuCache = json.optBoolean("ps3SpuCache", def.ps3.spuCache),
llvmPrecompile = json.optBoolean("ps3LlvmPrecompile", def.ps3.llvmPrecompile),
accurateSpuDma = json.optBoolean("ps3AccurateSpuDma", def.ps3.accurateSpuDma),
savestateCompatibleMode = json.optBoolean("ps3SavestateCompatibleMode", def.ps3.savestateCompatibleMode),
clocksScale = json.optInt("ps3ClocksScale", def.ps3.clocksScale),
resolutionScale = json.optInt("ps3ResolutionScale", def.ps3.resolutionScale),
msaaMode = json.optInt("ps3MsaaMode", def.ps3.msaaMode),
@@ -2573,6 +2578,7 @@ data class Settings(
if (current.ps3.spuCache != base.ps3.spuCache) j.put("ps3SpuCache", current.ps3.spuCache)
if (current.ps3.llvmPrecompile != base.ps3.llvmPrecompile) j.put("ps3LlvmPrecompile", current.ps3.llvmPrecompile)
if (current.ps3.accurateSpuDma != base.ps3.accurateSpuDma) j.put("ps3AccurateSpuDma", current.ps3.accurateSpuDma)
if (current.ps3.savestateCompatibleMode != base.ps3.savestateCompatibleMode) j.put("ps3SavestateCompatibleMode", current.ps3.savestateCompatibleMode)
if (current.ps3.clocksScale != base.ps3.clocksScale) j.put("ps3ClocksScale", current.ps3.clocksScale)
if (current.ps3.resolutionScale != base.ps3.resolutionScale) j.put("ps3ResolutionScale", current.ps3.resolutionScale)
if (current.ps3.msaaMode != base.ps3.msaaMode) j.put("ps3MsaaMode", current.ps3.msaaMode)
@@ -2868,6 +2874,7 @@ data class Settings(
spuCache = if (overrides.has("ps3SpuCache")) overrides.getBoolean("ps3SpuCache") else base.ps3.spuCache,
llvmPrecompile = if (overrides.has("ps3LlvmPrecompile")) overrides.getBoolean("ps3LlvmPrecompile") else base.ps3.llvmPrecompile,
accurateSpuDma = if (overrides.has("ps3AccurateSpuDma")) overrides.getBoolean("ps3AccurateSpuDma") else base.ps3.accurateSpuDma,
savestateCompatibleMode = if (overrides.has("ps3SavestateCompatibleMode")) overrides.getBoolean("ps3SavestateCompatibleMode") else base.ps3.savestateCompatibleMode,
clocksScale = if (overrides.has("ps3ClocksScale")) overrides.getInt("ps3ClocksScale") else base.ps3.clocksScale,
resolutionScale = if (overrides.has("ps3ResolutionScale")) overrides.getInt("ps3ResolutionScale") else base.ps3.resolutionScale,
msaaMode = if (overrides.has("ps3MsaaMode")) overrides.getInt("ps3MsaaMode") else base.ps3.msaaMode,
@@ -1563,6 +1563,8 @@ val EN: Map<String, String> = mapOf(
"perf.llvmPrecompile.description" to "Compiles modules ahead of the game running instead of on demand. Longer wait at boot, fewer stutters in play.",
"perf.spuLoopDetection.label" to "SPU Loop Detection",
"perf.spuLoopDetection.description" to "Detects SPU wait loops and yields the thread instead of spinning. Can free CPU time on a handheld; a few games misbehave with it on.",
"perf.savestateCompatible.label" to "Allow save states (Compatible Savestate Mode)",
"perf.savestateCompatible.description" to "Required for save states to work at all: saving has to stop every SPU somewhere it can be written out, and without this that fails on any game with SPU work running. Costs SPU performance while it is on, and each save state is roughly 500 MB to 3 GB, so a few of them will fill your storage.",
"perf.accurateSpuDma.label" to "Accurate SPU DMA",
"perf.accurateSpuDma.description" to "Emulates SPU DMA transfers precisely. Slower, and only needed by a handful of games that corrupt without it.",
"common.auto" to "Auto",
@@ -319,6 +319,15 @@ fun PerformanceTab(state: MutableState<Settings>) {
description = str("perf.accurateSpuDma.description"),
onChange = { apply(s.copy(ps3 = s.ps3.copy(accurateSpuDma = it))) },
)
SettingsDivider()
// Sits with the SPU rows because that is what it costs, not under a savestate
// heading where it would read as free.
ToggleRow(
label = str("perf.savestateCompatible.label"),
value = s.ps3.savestateCompatibleMode,
description = str("perf.savestateCompatible.description"),
onChange = { apply(s.copy(ps3 = s.ps3.copy(savestateCompatibleMode = it))) },
)
}
SettingsDivider()
// The PS2 speedhacks here (INTC/wait-loop detection, fast CDVD, instant
@@ -53,6 +53,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
SettingsSearchEntry("perf.llvmPrecompile.label", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.spuLoopDetection.label", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.accurateSpuDma.label", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.savestateCompatible.label", true, SettingsCategory.Performance),
SettingsSearchEntry("perf.spuTuning.title", true, SettingsCategory.Performance),
SettingsSearchEntry("adv.accurateSpuRsv.label", true, SettingsCategory.Performance),
SettingsSearchEntry("adv.accurateCacheLine.label", true, SettingsCategory.Performance),