iOS: Add per-game boot compatibility settings

Add per-game EE Cycle Rate and Fast Boot overrides through the emulator
bridge. setGameSettings gains eeCycleRate and fastBoot parameters with
their override flags, the global game settings result reports the global
defaults, and per-game overrides are read, clamped, and written into the
EmuCore Speedhacks and Fast Boot INI keys. A negative EE underclock is
clamped back to zero while RetroAchievements hardcore mode is active.

The per-game settings panel surfaces EE Cycle Rate and Fast Boot pickers
with reset-to-global controls, and gates saving on the game having a
settings identity.
This commit is contained in:
Jeen
2026-06-19 21:45:45 +02:00
parent 0cd2bf8e19
commit ca1d482ba0
3 changed files with 145 additions and 2 deletions
+10 -2
View File
@@ -145,11 +145,15 @@ typedef void (^ARMSX2RetroAchievementsCompletion)(BOOL success, NSString * _Nonn
volumePercent:(int)volumePercent
eeCoreType:(int)eeCoreType
mtvu:(BOOL)mtvu
eeCycleRateOverride:(BOOL)eeCycleRateOverride
eeCycleRate:(int)eeCycleRate
fastBootOverride:(BOOL)fastBootOverride
fastBoot:(BOOL)fastBoot
enableCheats:(BOOL)enableCheats
enablePatches:(BOOL)enablePatches
enableGameFixes:(BOOL)enableGameFixes
enableGameDBHardwareFixes:(BOOL)enableGameDBHardwareFixes
NS_SWIFT_NAME(setGameSettings(forISO:enabled:upscaleMultiplier:aspectRatio:textureFiltering:hardwareMipmapping:blendingAccuracy:interlaceMode:trilinearFiltering:halfPixelOffset:roundSprite:alignSpriteOverride:alignSprite:mergeSpriteOverride:mergeSprite:wildArmsOffsetOverride:wildArmsOffset:textureOffsetXOverride:textureOffsetX:textureOffsetYOverride:textureOffsetY:skipDrawStartOverride:skipDrawStart:skipDrawEndOverride:skipDrawEnd:volumeOverride:volumePercent:eeCoreType:mtvu:enableCheats:enablePatches:enableGameFixes:enableGameDBHardwareFixes:));
NS_SWIFT_NAME(setGameSettings(forISO:enabled:upscaleMultiplier:aspectRatio:textureFiltering:hardwareMipmapping:blendingAccuracy:interlaceMode:trilinearFiltering:halfPixelOffset:roundSprite:alignSpriteOverride:alignSprite:mergeSpriteOverride:mergeSprite:wildArmsOffsetOverride:wildArmsOffset:textureOffsetXOverride:textureOffsetX:textureOffsetYOverride:textureOffsetY:skipDrawStartOverride:skipDrawStart:skipDrawEndOverride:skipDrawEnd:volumeOverride:volumePercent:eeCoreType:mtvu:eeCycleRateOverride:eeCycleRate:fastBootOverride:fastBoot:enableCheats:enablePatches:enableGameFixes:enableGameDBHardwareFixes:));
+ (void)setGameSettingsForCurrentGameWithEnabled:(BOOL)enabled
upscaleMultiplier:(float)upscaleMultiplier
aspectRatio:(nonnull NSString *)aspectRatio
@@ -178,11 +182,15 @@ typedef void (^ARMSX2RetroAchievementsCompletion)(BOOL success, NSString * _Nonn
volumePercent:(int)volumePercent
eeCoreType:(int)eeCoreType
mtvu:(BOOL)mtvu
eeCycleRateOverride:(BOOL)eeCycleRateOverride
eeCycleRate:(int)eeCycleRate
fastBootOverride:(BOOL)fastBootOverride
fastBoot:(BOOL)fastBoot
enableCheats:(BOOL)enableCheats
enablePatches:(BOOL)enablePatches
enableGameFixes:(BOOL)enableGameFixes
enableGameDBHardwareFixes:(BOOL)enableGameDBHardwareFixes
NS_SWIFT_NAME(setGameSettingsForCurrentGame(enabled:upscaleMultiplier:aspectRatio:textureFiltering:hardwareMipmapping:blendingAccuracy:interlaceMode:trilinearFiltering:halfPixelOffset:roundSprite:alignSpriteOverride:alignSprite:mergeSpriteOverride:mergeSprite:wildArmsOffsetOverride:wildArmsOffset:textureOffsetXOverride:textureOffsetX:textureOffsetYOverride:textureOffsetY:skipDrawStartOverride:skipDrawStart:skipDrawEndOverride:skipDrawEnd:volumeOverride:volumePercent:eeCoreType:mtvu:enableCheats:enablePatches:enableGameFixes:enableGameDBHardwareFixes:));
NS_SWIFT_NAME(setGameSettingsForCurrentGame(enabled:upscaleMultiplier:aspectRatio:textureFiltering:hardwareMipmapping:blendingAccuracy:interlaceMode:trilinearFiltering:halfPixelOffset:roundSprite:alignSpriteOverride:alignSprite:mergeSpriteOverride:mergeSprite:wildArmsOffsetOverride:wildArmsOffset:textureOffsetXOverride:textureOffsetX:textureOffsetYOverride:textureOffsetY:skipDrawStartOverride:skipDrawStart:skipDrawEndOverride:skipDrawEnd:volumeOverride:volumePercent:eeCoreType:mtvu:eeCycleRateOverride:eeCycleRate:fastBootOverride:fastBoot:enableCheats:enablePatches:enableGameFixes:enableGameDBHardwareFixes:));
+ (nonnull NSString *)clearCacheForISO:(nonnull NSString *)isoName NS_SWIFT_NAME(clearCache(forISO:));
+ (nonnull NSString *)deleteGameDataForISO:(nonnull NSString *)isoName NS_SWIFT_NAME(deleteGameData(forISO:));
+ (BOOL)deleteISO:(nonnull NSString *)isoName deleteGameData:(BOOL)deleteGameData NS_SWIFT_NAME(deleteISO(_:deleteGameData:));
+52
View File
@@ -1483,6 +1483,14 @@ static NSMutableDictionary<NSString*, id>* ARMSX2BuildGlobalGameSettingsResult()
const bool globalEnableGameDBHardwareFixes = g_p44_settings_interface ? !g_p44_settings_interface->GetBoolValue("EmuCore/GS", "UserHacks", false) : true;
const int globalEECoreType = g_p44_settings_interface ? g_p44_settings_interface->GetIntValue("EmuCore/CPU", "CoreType", 2) : 2;
const bool globalMTVU = g_p44_settings_interface ? g_p44_settings_interface->GetBoolValue("EmuCore/Speedhacks", "vuThread", true) : true;
const int globalEECycleRate = ARMSX2ClampInt(
g_p44_settings_interface ? g_p44_settings_interface->GetIntValue("EmuCore/Speedhacks", "EECycleRate", 0) : 0,
-3,
3);
const bool globalFastBoot = g_p44_settings_interface ?
g_p44_settings_interface->GetBoolValue(
"EmuCore", "EnableFastBoot",
g_p44_settings_interface->GetBoolValue("GameISO", "FastBoot", false)) : false;
const int globalVolumePercent = ARMSX2ClampInt(
g_p44_settings_interface ? g_p44_settings_interface->GetIntValue("SPU2/Output", "StandardVolume", ARMSX2DefaultAudioVolumePercent) : ARMSX2DefaultAudioVolumePercent,
0,
@@ -1524,6 +1532,12 @@ static NSMutableDictionary<NSString*, id>* ARMSX2BuildGlobalGameSettingsResult()
@"enableGameDBHardwareFixes": @(globalEnableGameDBHardwareFixes),
@"eeCoreType": @(globalEECoreType),
@"mtvu": @(globalMTVU),
@"globalEECycleRate": @(globalEECycleRate),
@"eeCycleRate": @(globalEECycleRate),
@"hasEECycleRateOverride": @NO,
@"globalFastBoot": @(globalFastBoot),
@"fastBoot": @(globalFastBoot),
@"hasFastBootOverride": @NO,
@"globalVolumePercent": @(globalVolumePercent),
@"volumePercent": @(globalVolumePercent),
@"hasVolumeOverride": @NO,
@@ -1583,6 +1597,8 @@ static void ARMSX2ApplyPerGameSettingsOverrides(NSMutableDictionary<NSString*, i
si.ContainsValue("EmuCore/CPU", "CoreType") ||
si.ContainsValue("EmuCore/CPU", "UseArm64Dynarec") ||
si.ContainsValue("EmuCore/Speedhacks", "vuThread") ||
si.ContainsValue("EmuCore/Speedhacks", "EECycleRate") ||
si.ContainsValue("EmuCore", "EnableFastBoot") ||
si.ContainsValue("SPU2/Output", "StandardVolume") ||
si.ContainsValue("SPU2/Output", "FastForwardVolume");
@@ -1629,6 +1645,10 @@ static void ARMSX2ApplyPerGameSettingsOverrides(NSMutableDictionary<NSString*, i
result[@"enableGameDBHardwareFixes"] = @(!si.GetBoolValue("EmuCore/GS", "UserHacks", ![result[@"enableGameDBHardwareFixes"] boolValue]));
result[@"eeCoreType"] = @(si.GetIntValue("EmuCore/CPU", "CoreType", [result[@"eeCoreType"] intValue]));
result[@"mtvu"] = @(si.GetBoolValue("EmuCore/Speedhacks", "vuThread", [result[@"mtvu"] boolValue]));
result[@"hasEECycleRateOverride"] = @(si.ContainsValue("EmuCore/Speedhacks", "EECycleRate"));
result[@"eeCycleRate"] = @(ARMSX2ClampInt(si.GetIntValue("EmuCore/Speedhacks", "EECycleRate", [result[@"eeCycleRate"] intValue]), -3, 3));
result[@"hasFastBootOverride"] = @(si.ContainsValue("EmuCore", "EnableFastBoot"));
result[@"fastBoot"] = @(si.GetBoolValue("EmuCore", "EnableFastBoot", [result[@"fastBoot"] boolValue]));
}
static void ARMSX2WriteGameSettingsForIdentity(const std::string& serial,
@@ -1661,6 +1681,10 @@ static void ARMSX2WriteGameSettingsForIdentity(const std::string& serial,
int volumePercent,
int eeCoreType,
BOOL mtvu,
BOOL eeCycleRateOverride,
int eeCycleRate,
BOOL fastBootOverride,
BOOL fastBoot,
BOOL enableCheats,
BOOL enablePatches,
BOOL enableGameFixes,
@@ -1760,6 +1784,22 @@ static void ARMSX2WriteGameSettingsForIdentity(const std::string& serial,
si.SetIntValue("ARMSX2iOS/PerGame", "ManualMTVUVersion", 3);
si.SetBoolValue("EmuCore/Speedhacks", "vuThread", mtvu);
}
if (eeCycleRateOverride) {
int clampedEECycleRate = ARMSX2ClampInt(eeCycleRate, -3, 3);
if (ARMSX2RetroAchievementsHardcoreActive() && clampedEECycleRate < 0) {
ARMSX2LogRetroAchievementsHardcoreBlock("per_game_ee_underclock");
clampedEECycleRate = 0;
}
si.SetIntValue("EmuCore/Speedhacks", "EECycleRate", clampedEECycleRate);
} else {
si.DeleteValue("EmuCore/Speedhacks", "EECycleRate");
}
if (fastBootOverride)
si.SetBoolValue("EmuCore", "EnableFastBoot", fastBoot);
else
si.DeleteValue("EmuCore", "EnableFastBoot");
} else {
si.DeleteValue("ARMSX2iOS/PerGame", "Enabled");
si.DeleteValue("EmuCore/GS", "upscale_multiplier");
@@ -1787,6 +1827,8 @@ static void ARMSX2WriteGameSettingsForIdentity(const std::string& serial,
si.DeleteValue("ARMSX2iOS/PerGame", "ManualMTVU");
si.DeleteValue("ARMSX2iOS/PerGame", "ManualMTVUVersion");
si.DeleteValue("EmuCore/Speedhacks", "vuThread");
si.DeleteValue("EmuCore/Speedhacks", "EECycleRate");
si.DeleteValue("EmuCore", "EnableFastBoot");
si.DeleteValue("SPU2/Output", "StandardVolume");
si.DeleteValue("SPU2/Output", "FastForwardVolume");
si.RemoveEmptySections();
@@ -2337,6 +2379,10 @@ static void ARMSX2WriteGameSettingsForIdentity(const std::string& serial,
volumePercent:(int)volumePercent
eeCoreType:(int)eeCoreType
mtvu:(BOOL)mtvu
eeCycleRateOverride:(BOOL)eeCycleRateOverride
eeCycleRate:(int)eeCycleRate
fastBootOverride:(BOOL)fastBootOverride
fastBoot:(BOOL)fastBoot
enableCheats:(BOOL)enableCheats
enablePatches:(BOOL)enablePatches
enableGameFixes:(BOOL)enableGameFixes
@@ -2356,6 +2402,7 @@ static void ARMSX2WriteGameSettingsForIdentity(const std::string& serial,
textureOffsetYOverride, textureOffsetY, skipDrawStartOverride,
skipDrawStart, skipDrawEndOverride, skipDrawEnd,
volumeOverride, volumePercent, eeCoreType, mtvu,
eeCycleRateOverride, eeCycleRate, fastBootOverride, fastBoot,
enableCheats, enablePatches, enableGameFixes, enableGameDBHardwareFixes);
}
@@ -2387,6 +2434,10 @@ static void ARMSX2WriteGameSettingsForIdentity(const std::string& serial,
volumePercent:(int)volumePercent
eeCoreType:(int)eeCoreType
mtvu:(BOOL)mtvu
eeCycleRateOverride:(BOOL)eeCycleRateOverride
eeCycleRate:(int)eeCycleRate
fastBootOverride:(BOOL)fastBootOverride
fastBoot:(BOOL)fastBoot
enableCheats:(BOOL)enableCheats
enablePatches:(BOOL)enablePatches
enableGameFixes:(BOOL)enableGameFixes
@@ -2412,6 +2463,7 @@ static void ARMSX2WriteGameSettingsForIdentity(const std::string& serial,
textureOffsetYOverride, textureOffsetY, skipDrawStartOverride,
skipDrawStart, skipDrawEndOverride, skipDrawEnd,
volumeOverride, volumePercent, eeCoreType, mtvu,
eeCycleRateOverride, eeCycleRate, fastBootOverride, fastBoot,
enableCheats, enablePatches, enableGameFixes, enableGameDBHardwareFixes);
if (VMManager::HasValidVM()) {
@@ -1561,6 +1561,10 @@ struct PerGameSettingsPanel: View {
private static let useGlobalSentinel = -1
private static let trilinearUseGlobalSentinel = Int(Int32.min)
private static let eeCycleRateUseGlobalSentinel = Int(Int32.min)
private static let fastBootUseGlobalSentinel = -1
private static let fastBootOff = 0
private static let fastBootOn = 1
private static let deinterlaceOptions = [
PickerOption(id: 0, title: "None"),
@@ -1626,8 +1630,15 @@ struct PerGameSettingsPanel: View {
@State private var globalVolumePercent: Int
@State private var volumeOverride: Bool
@State private var volumePercent: Int
@State private var padLayoutIdentity: PadLayoutGameIdentity?
@State private var showPadLayoutEditor = false
@State private var eeCoreType: Int
@State private var mtvu: Bool
@State private var globalEECycleRate: Int
@State private var eeCycleRate: Int
@State private var globalFastBoot: Bool
@State private var fastBoot: Int
@State private var hasGameSettingsIdentity: Bool
@State private var enableCheats: Bool
@State private var enablePatches: Bool
@State private var enableGameFixes: Bool
@@ -1652,6 +1663,11 @@ struct PerGameSettingsPanel: View {
_globalVolumePercent = State(initialValue: inheritedVolume)
_volumeOverride = State(initialValue: Self.boolValue(info["hasVolumeOverride"], defaultValue: false))
_volumePercent = State(initialValue: loadedVolume)
_padLayoutIdentity = State(initialValue: PadLayoutGameIdentity(
serial: (info["serial"] as? String) ?? game.metadata["serial"],
crc: (info["crc"] as? String) ?? game.metadata["crc"]
))
_hasGameSettingsIdentity = State(initialValue: !PadLayoutGameIdentity.normalizedCRC((info["crc"] as? String) ?? game.metadata["crc"]).isEmpty)
_upscaleMultiplier = State(initialValue: Self.floatValue(info["upscaleMultiplier"], defaultValue: 1.0))
_aspectRatio = State(initialValue: Self.normalizedAspect(info["aspectRatio"] as? String))
_textureFiltering = State(initialValue: Self.intValue(info["textureFiltering"], defaultValue: 2))
@@ -1686,6 +1702,12 @@ struct PerGameSettingsPanel: View {
_skipDrawEnd = State(initialValue: initialSkipDrawEnd)
_eeCoreType = State(initialValue: Self.intValue(info["eeCoreType"], defaultValue: 2))
_mtvu = State(initialValue: Self.boolValue(info["mtvu"], defaultValue: true))
let inheritedEECycleRate = Self.clampedEECycleRate(Self.intValue(info["globalEECycleRate"], defaultValue: 0))
_globalEECycleRate = State(initialValue: inheritedEECycleRate)
_eeCycleRate = State(initialValue: Self.boolValue(info["hasEECycleRateOverride"], defaultValue: false) ? Self.clampedEECycleRate(Self.intValue(info["eeCycleRate"], defaultValue: inheritedEECycleRate)) : Self.eeCycleRateUseGlobalSentinel)
let inheritedFastBoot = Self.boolValue(info["globalFastBoot"], defaultValue: false)
_globalFastBoot = State(initialValue: inheritedFastBoot)
_fastBoot = State(initialValue: Self.boolValue(info["hasFastBootOverride"], defaultValue: false) ? (Self.boolValue(info["fastBoot"], defaultValue: inheritedFastBoot) ? Self.fastBootOn : Self.fastBootOff) : Self.fastBootUseGlobalSentinel)
_enableCheats = State(initialValue: Self.boolValue(info["enableCheats"], defaultValue: false))
_enablePatches = State(initialValue: Self.boolValue(info["enablePatches"], defaultValue: true))
_enableGameFixes = State(initialValue: Self.boolValue(info["enableGameFixes"], defaultValue: true))
@@ -1766,6 +1788,11 @@ struct PerGameSettingsPanel: View {
Text(settings.localized("Overrides are saved for this game only and apply on the next boot/reset of this title."))
.font(.caption)
.foregroundStyle(.secondary)
if !hasGameSettingsIdentity {
Text("Start this game once before saving its settings.")
.font(.caption)
.foregroundStyle(.orange)
}
}
Section(settings.localized("Audio")) {
@@ -1836,6 +1863,41 @@ struct PerGameSettingsPanel: View {
.foregroundStyle(.secondary)
}
Section("Performance / Compatibility") {
Picker("EE Cycle Rate", selection: $eeCycleRate) {
Text("Global Default (\(Self.formatEECycleRate(globalEECycleRate)))").tag(Self.eeCycleRateUseGlobalSentinel)
ForEach(-3...3, id: \.self) { value in
Text(Self.formatEECycleRate(value)).tag(value)
}
}
.disabled(!enabled)
Button("Reset EE Cycle Rate to Global") {
eeCycleRate = Self.eeCycleRateUseGlobalSentinel
}
.disabled(!enabled || eeCycleRate == Self.eeCycleRateUseGlobalSentinel)
Text("Can improve performance in heavy games, but may cause timing or compatibility issues. Reset or relaunch the game after changing it.")
.font(.caption)
.foregroundStyle(.secondary)
Picker("Fast Boot", selection: $fastBoot) {
Text("Global Default (\(globalFastBoot ? "On" : "Off"))").tag(Self.fastBootUseGlobalSentinel)
Text("On").tag(Self.fastBootOn)
Text("Off").tag(Self.fastBootOff)
}
.disabled(!enabled)
Button("Reset Fast Boot to Global") {
fastBoot = Self.fastBootUseGlobalSentinel
}
.disabled(!enabled || fastBoot == Self.fastBootUseGlobalSentinel)
Text("Some games may need Fast Boot on or off to avoid looping at the disc screen. Reset or relaunch the game after changing it.")
.font(.caption)
.foregroundStyle(.secondary)
}
Section(settings.localized("Graphics")) {
Picker(settings.localized("Internal Resolution"), selection: $upscaleMultiplier) {
Text("0.25x (Fastest)").tag(Float(0.25))
@@ -2038,6 +2100,10 @@ struct PerGameSettingsPanel: View {
}
private func save() {
guard hasGameSettingsIdentity else {
statusMessage = "Start this game once before saving its settings."
return
}
let normalizedSkipDraw = normalizedSkipDrawValues()
if skipDrawStart != normalizedSkipDraw.start {
skipDrawStart = normalizedSkipDraw.start
@@ -2076,6 +2142,10 @@ struct PerGameSettingsPanel: View {
volumePercent: Int32(volumePercent),
eeCoreType: Int32(eeCoreType),
mtvu: mtvu,
eeCycleRateOverride: enabled && eeCycleRate != Self.eeCycleRateUseGlobalSentinel,
eeCycleRate: Int32(Self.clampedEECycleRate(eeCycleRate == Self.eeCycleRateUseGlobalSentinel ? globalEECycleRate : eeCycleRate)),
fastBootOverride: enabled && fastBoot != Self.fastBootUseGlobalSentinel,
fastBoot: fastBoot == Self.fastBootOn,
enableCheats: enableCheats,
enablePatches: enablePatches,
enableGameFixes: enableGameFixes,
@@ -2112,6 +2182,10 @@ struct PerGameSettingsPanel: View {
volumePercent: Int32(volumePercent),
eeCoreType: Int32(eeCoreType),
mtvu: mtvu,
eeCycleRateOverride: enabled && eeCycleRate != Self.eeCycleRateUseGlobalSentinel,
eeCycleRate: Int32(Self.clampedEECycleRate(eeCycleRate == Self.eeCycleRateUseGlobalSentinel ? globalEECycleRate : eeCycleRate)),
fastBootOverride: enabled && fastBoot != Self.fastBootUseGlobalSentinel,
fastBoot: fastBoot == Self.fastBootOn,
enableCheats: enableCheats,
enablePatches: enablePatches,
enableGameFixes: enableGameFixes,
@@ -2190,10 +2264,19 @@ struct PerGameSettingsPanel: View {
SettingsStore.clampedEmulatorVolumePercent(value)
}
private static func clampedEECycleRate(_ value: Int) -> Int {
min(max(value, -3), 3)
}
private static func formatPercent(_ value: Int) -> String {
"\(clampedVolume(value))%"
}
private static func formatEECycleRate(_ value: Int) -> String {
let clamped = clampedEECycleRate(value)
return clamped > 0 ? "+\(clamped)" : "\(clamped)"
}
private static func normalizedSkipDrawEnd(start: Int, end: Int, startOverride: Bool, endOverride: Bool) -> Int {
let clampedEnd = clampedSkipDraw(end)
guard startOverride && endOverride else {