5 Commits
Author SHA1 Message Date
jpolo1224 39ca5cdab6 0.7.2: settings fixes, Oboe by default, and a working per-section Reset
Per-section Reset did nothing on most tabs. The field lists describe the tabs
as they were before the PS3 rewrite, so Reset was clearing settings the tabs no
longer show while missing most of what they do: Performance listed 22 of 47,
Graphics 45 of 57, Audio 10 of 16 -- audioRenderer, audioFormat, audioChannels
and audioCubebBackend were absent, so changing the audio backend and pressing
Reset was a no-op. Regenerated from what each tab actually writes, mapping
ps3.foo to its ps3Foo key and validating every entry against the serialiser.
Five keys also moved off Graphics because another tab owns them, which was a
cross-tab clobber waiting to happen.

Full Diagonal Range, per stick, on by default. A full diagonal was capped to
the unit circle at ~0.707 per axis, which is what a circular-gated DualShock
really sends -- but games that deadzone each axis separately then ignore
diagonals, and Oblivion's camera crawled diagonally while the cardinals were
fine. Off restores the hardware curve.

Oboe is the default audio backend on Android, with a migration for anyone still
on the old Cubeb default; a deliberate choice of another backend is kept.

Enter Button Assignment (circle/cross) is exposed. The core has always had it
and Android never showed it.

Reset all settings, in General. Per-game overrides and controller binds are
deliberately left alone -- they are invisible from that page.
2026-08-13 22:29:33 -04:00
jpolo1224 b82432c793 VK: allow native fp16 on Adreno drivers that accept it
Oblivion's water did not draw on Vulkan and did draw on OpenGL. The only
Vulkan-only shader workaround in play is the blanket disable of native float16
on every mobile GPU, which emulates it with fp32; its own comment claimed that
"renders correctly", and it does not.

The disable exists for a real failure -- Qualcomm's compiler rejected SPIR-V
containing float16_t and every pipeline came back VK_ERROR_UNKNOWN, which
presents as a black screen with working audio and a working compile overlay,
so it reads as a renderer bug rather than a shader one. That is not worth
reintroducing blind, so this is a version gate rather than a removal:

  Adreno on driver 512.676.53 or newer -> native fp16 (verified)
  older Adreno                         -> unchanged
  Mali, PowerVR, Xclipse, the rest     -> unchanged, untested either way

Found by switching the renderer to OpenGL, which isolated it to the Vulkan path
in one run after the settings-level suspects had all come back empty.
2026-08-13 22:29:17 -04:00
jpolo1224 7f54855b7d lv2/vm: fix a read-only unlink lockup, and three log floods
sys_fs_unlink handled notdir and noent but not readonly, so it fell through to
fmt::throw_exception and killed the PPU main thread inside the syscall. The
emulator then sat with nothing to run: the game froze with the CPU at 1% and
nothing in the log but a stalled RSX. On Android /app_home is the mounted ISO,
which is read-only, so any game deleting a file in its own directory hit it --
Oblivion removes warnings.txt at startup and never got past it. Returns
CELL_EROFS now, which is already what sys_fs_write and friends do. sys_fs_mkdir
and sys_fs_rmdir carried the identical block and are fixed with it.

Three log floods, all of which stall the emulator outright because writing them
is not free on Android:

- sys_fs_utime logged two warning lines per call and rides a polling loop.
  Oblivion's FileCaching thread hit it 7274 times in ten seconds on one .BSA,
  ~22k lines, and the frame loop stopped for over twenty seconds. Now trace.
- vm::lock_sudo reported a failed mlock on every mapping. Android never grants
  RLIMIT_MEMLOCK to apps, so it fails forever while advising the user to raise
  a limit they cannot raise -- 6470 lines in ten seconds here, and ~1200 in
  every other game log looked at. Reported once per session now.
- sys_mmapper's map/unmap pair, 12431 lines over the same window. Now trace.

None of them lose information: raise the channel to Trace to get them back.
2026-08-13 22:29:06 -04:00
jpolo1224 0819f1ef15 RSX: return the renderer to the 0.6 path, keeping the FIFO idle fix and ADPF
Testers consistently report the best performance on the build with the 0.6
renderer, so 0.7's graphics work goes back out. The Arkham City measurement
behind it (62.8 -> 51.2 ms) was one game on one device and did not survive
contact with a wider set of hardware.

Two files are kept from 0.7 because neither is render pass work and both are
measured wins on their own: RSXFIFO's idle spin plus WFE park, which took ~11%
of total CPU off sched_yield, and RSXThread's ADPF feed, without which the
performance-hint setting reports nothing and does nothing.

Everything else under Emu/RSX is byte-identical to 0.6. The removed work is not
lost -- it is in c4b45eee2 and can come back a piece at a time with testing
behind each one, which is how it should have gone in the first place.
2026-08-13 22:28:52 -04:00
jpolo1224 8ee20d91d5 0.7.1: remove vertex cache retention, keep the rest of the renderer
The previous commit reverted all of Emu/RSX to 0.6, which was more than the
bug required. Bisecting had already shown the render pass work was not
responsible -- reverting it alone changed nothing, while removing retention
with the render pass work in place fixed both reported games.

So only retention goes. It reused vertex cache entries across frames, and on
0.6 the attribute ring was too small for it to engage; raising the ring to
192M switched an existing path on in every game at once and handed draws
stale geometry. Back to purging every frame, as 0.6 did.

This restores what the wider revert had taken out for no reason: the render
pass reduction, the RSX FIFO idle fix, ADPF frame timing, the ZCULL and
occlusion query fixes, the swapchain and surface lifetime ports, and VRAM
budgeting.

Sonic Unleashed still does not render FMV cutscenes. That reproduces with
the 0.6 renderer too, so it is unrelated and still open.
2026-08-13 19:51:48 -04:00
16 changed files with 393 additions and 53 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ android {
applicationId = "com.armsx3"
minSdk = 26
targetSdk = 37
versionCode = 12
versionName = "0.7.1"
versionCode = 13
versionName = "0.7.2"
// ARMSX2's UI reads these. STORAGE_ALL_FILES gates the all-files storage path in
// onboarding; IN_APP_UPDATER gates the in-app GitHub-release updater.
@@ -63,6 +63,8 @@ object ConfigStore {
private const val KEY_SPU_DECODER_RESTORE = "config.migrated.spuDecoderRestoreLlvm"
private const val KEY_XFLOAT_BACK_TO_APPROX = "config.migrated.xfloatBackToApprox"
private const val KEY_PRECISE_SPU_OFF = "config.migrated.preciseSpuVerifyOff"
// Oboe became the Android default in 0.7.2; move anyone still on the old Cubeb default.
private const val KEY_AUDIO_OBOE = "config.migrated.audioOboeDefault"
private const val KEY_ATOMIC_DMA_OFF = "config.migrated.atomicDmaStoresOff"
// Bumped: the first pass recorded only Vblank Rate, which did not hold on its own.
private const val KEY_VBLANK_60 = "config.migrated.frameCap60"
@@ -316,6 +318,16 @@ object ConfigStore {
}
// Oboe is the Android default now. Only move people sitting on the previous default
// (Cubeb, index 2) -- anyone who deliberately picked Null or another backend keeps it.
if (!MainActivityRuntime.prefs.getBoolean(KEY_AUDIO_OBOE, false)) {
if (raw != null && parsed.ps3.audioRenderer == 2) {
parsed = parsed.copy(ps3 = parsed.ps3.copy(audioRenderer = 4))
dirty = true
}
MainActivityRuntime.prefs.edit { putBoolean(KEY_AUDIO_OBOE, true) }
}
// The ARM64 block checksum is fixed, so the full-compare workaround can go.
if (!MainActivityRuntime.prefs.getBoolean(KEY_PRECISE_SPU_OFF, false)) {
if (raw != null && parsed.ps3.preciseSpuVerification) {
@@ -198,6 +198,12 @@ data class Ps3Settings(
* preciseSpuVerification). Accurate is a rarely-exercised path and costs
* speed, so there is no reason to sit on it.
*/
/**
* Which face button confirms in PS3 system dialogs. 0 = circle, 1 = cross, matching
* enter_button_assign. Japanese-region games and hardware confirm with circle; the rest of
* the world uses cross, which is why RPCS3 exposes it rather than deriving it from region.
*/
val enterButtonAssign: Int = 1,
val spuXFloat: Int = 1,
val accurateSpuRsv: Boolean = true,
/**
@@ -249,7 +255,8 @@ data class Ps3Settings(
val debugConsoleMode: Boolean = false,
val resolution: Int = 2,
val anisoFilter: Int = 0,
val audioRenderer: Int = 2,
/** Index into Rpcs3Settings.AUDIO_RENDERERS. 4 = Oboe, the Android default (see node_audio). */
val audioRenderer: Int = 4,
/**
* Output aspect override in permille (1778 = 16:9, 1333 = 4:3), 0 = follow the game.
*
@@ -1064,6 +1071,7 @@ data class Settings(
put("PS3/Net", "Internet enabled", "enum", ps3.netEnabled.toString())
put("PS3/Net", "PSN status", "enum", ps3.psnStatus.toString())
put("PS3/Net", "UPNP Enabled", "bool", ps3.upnpEnabled.toString())
put("PS3/System", "Enter button assignment", "enum", ps3.enterButtonAssign.toString())
put("PS3/Core", "SPU XFloat Accuracy", "enum", ps3.spuXFloat.toString())
put("PS3/Core", "Accurate SPU Reservations", "bool", ps3.accurateSpuRsv.toString())
put("PS3/Core", "Accurate Cache Line Stores", "bool", ps3.accurateCacheLine.toString())
@@ -2032,6 +2040,7 @@ data class Settings(
put("ps3NetEnabled", ps3.netEnabled)
put("ps3PsnStatus", ps3.psnStatus)
put("ps3UpnpEnabled", ps3.upnpEnabled)
put("ps3EnterButtonAssign", ps3.enterButtonAssign)
put("ps3SpuXFloat", ps3.spuXFloat)
put("ps3AccurateSpuRsv", ps3.accurateSpuRsv)
put("ps3AccurateCacheLine", ps3.accurateCacheLine)
@@ -2370,6 +2379,7 @@ data class Settings(
netEnabled = json.optBoolean("ps3NetEnabled", def.ps3.netEnabled),
psnStatus = json.optBoolean("ps3PsnStatus", def.ps3.psnStatus),
upnpEnabled = json.optBoolean("ps3UpnpEnabled", def.ps3.upnpEnabled),
enterButtonAssign = json.optInt("ps3EnterButtonAssign", def.ps3.enterButtonAssign),
spuXFloat = json.optInt("ps3SpuXFloat", def.ps3.spuXFloat),
accurateSpuRsv = json.optBoolean("ps3AccurateSpuRsv", def.ps3.accurateSpuRsv),
accurateCacheLine = json.optBoolean("ps3AccurateCacheLine", def.ps3.accurateCacheLine),
@@ -2688,6 +2698,7 @@ data class Settings(
if (current.ps3.netEnabled != base.ps3.netEnabled) j.put("ps3NetEnabled", current.ps3.netEnabled)
if (current.ps3.psnStatus != base.ps3.psnStatus) j.put("ps3PsnStatus", current.ps3.psnStatus)
if (current.ps3.upnpEnabled != base.ps3.upnpEnabled) j.put("ps3UpnpEnabled", current.ps3.upnpEnabled)
if (current.ps3.enterButtonAssign != base.ps3.enterButtonAssign) j.put("ps3EnterButtonAssign", current.ps3.enterButtonAssign)
if (current.ps3.spuXFloat != base.ps3.spuXFloat) j.put("ps3SpuXFloat", current.ps3.spuXFloat)
if (current.ps3.accurateSpuRsv != base.ps3.accurateSpuRsv) j.put("ps3AccurateSpuRsv", current.ps3.accurateSpuRsv)
if (current.ps3.accurateCacheLine != base.ps3.accurateCacheLine) j.put("ps3AccurateCacheLine", current.ps3.accurateCacheLine)
@@ -2987,6 +2998,7 @@ data class Settings(
netEnabled = if (overrides.has("ps3NetEnabled")) overrides.getBoolean("ps3NetEnabled") else base.ps3.netEnabled,
psnStatus = if (overrides.has("ps3PsnStatus")) overrides.getBoolean("ps3PsnStatus") else base.ps3.psnStatus,
upnpEnabled = if (overrides.has("ps3UpnpEnabled")) overrides.getBoolean("ps3UpnpEnabled") else base.ps3.upnpEnabled,
enterButtonAssign = if (overrides.has("ps3EnterButtonAssign")) overrides.getInt("ps3EnterButtonAssign") else base.ps3.enterButtonAssign,
spuXFloat = if (overrides.has("ps3SpuXFloat")) overrides.getInt("ps3SpuXFloat") else base.ps3.spuXFloat,
accurateSpuRsv = if (overrides.has("ps3AccurateSpuRsv")) overrides.getBoolean("ps3AccurateSpuRsv") else base.ps3.accurateSpuRsv,
accurateCacheLine = if (overrides.has("ps3AccurateCacheLine")) overrides.getBoolean("ps3AccurateCacheLine") else base.ps3.accurateCacheLine,
@@ -525,6 +525,14 @@ val EN: Map<String, String> = mapOf(
"adv.accurateRsxRsv.description" to "Synchronises GPU access to reserved memory strictly. Fixes rare graphical corruption at a performance cost.",
"adv.ppuRsvPriority.label" to "PPU Reservation Priority",
"adv.ppuRsvPriority.description" to "Gives the main CPU priority over the SPUs when competing for the same memory. Can help games that stall waiting on the PPU.",
"pad.section.enterButton" to "Enter Button Assignment",
"pad.enterButton.label" to "Confirm button",
"pad.enterButton.circle" to "Enter with circle",
"pad.enterButton.cross" to "Enter with cross",
"pad.enterButton.description" to "Which button confirms in PS3 system dialogs. Japanese games usually expect circle; most others use cross.",
"app.resetAll" to "Reset all settings",
"app.resetAll.desc" to "Put every setting back to its default. Per-game settings and controller binds are kept.",
"app.resetAll.confirm" to "Every global setting goes back to its default. Per-game overrides and controller binds are not touched.",
"adv.spuVerification.label" to "SPU Verification",
"adv.spuVerification.description" to "Verifies compiled SPU code against the original. Catches miscompiles; turning it off is faster but makes bad codegen silent.",
"adv.preciseSpuVerification.label" to "Precise SPU Verification",
@@ -821,6 +829,8 @@ val EN: Map<String, String> = mapOf(
"pad.stickFeel.acceleration.description" to "Non-linear response curve: small tilts stay precise for aiming, full tilt ramps up to full speed. 0 = linear (off); higher = more curve.",
"pad.stickFeel.acceleration.label" to "Acceleration",
"pad.stickFeel.antiDeadzone.description" to "Smallest output sent to the game, to cancel a game's OWN built-in stick deadzone (e.g. Cold Fear / Area 51 ignore the stick until ~45%, then aim jumps). Set near the game's deadzone so any stick movement responds immediately and the full travel maps smoothly above it. 0 = off.",
"pad.stickFeel.squareGate.label" to "Full Diagonal Range",
"pad.stickFeel.squareGate.description" to "Sends the full range on diagonals instead of the reduced value a real DualShock gives (~70%), so diagonal movement is as fast as straight up/down/left/right. On by default. Turn off to match original hardware exactly.",
"pad.stickFeel.antiDeadzone.label" to "Anti-Deadzone",
"pad.stickFeel.deadzone.description" to "Fraction of physical analog travel ignored near center (applied to the stick's radial distance, so diagonals behave like cardinals). Output re-normalizes past it, so movement still ramps smoothly from 0 — which also means the on-screen effect can be masked by a game's OWN built-in deadzone (Area 51 ignores input below ~45% no matter what you set here; use Anti-Deadzone for that). 0 = off — raw hardware values pass through, including any stick drift.",
"pad.stickFeel.deadzone.label" to "Deadzone",
@@ -364,6 +364,22 @@ object ControllerMappings {
private const val KEY_STICK_ANTIDZ = "pad.stick.antiDeadzone"
const val STICK_ANTIDZ_MAX = 0.60f
private val prefStickAntiDz = PerStickPref(KEY_STICK_ANTIDZ, 0.0f, 0f, STICK_ANTIDZ_MAX)
// Square gate: send the full per-axis range on diagonals instead of capping them to the
// unit circle. A DualShock 3 is circular-gated, so a full diagonal is ~0.707 per axis, and
// emitting that is technically faithful -- but it lands inside the internal deadzone of games
// that test each axis separately, and their camera then crawls diagonally while the cardinals
// are fine. Oblivion is the case that found this.
//
// DEFAULT ON. Faithfulness to a circular gate is not worth a control scheme that feels broken,
// and a modern pad's own gate is closer to square anyway. Off restores the hardware curve for
// anyone who wants it. Per stick.
private const val KEY_STICK_SQUARE = "pad.stick.square"
fun stickSquareGate(left: Boolean): Boolean =
MainActivityRuntime.prefs.getBoolean(KEY_STICK_SQUARE + if (left) ".l" else ".r", true)
fun setStickSquareGate(left: Boolean, v: Boolean) =
MainActivityRuntime.prefs.edit { putBoolean(KEY_STICK_SQUARE + if (left) ".l" else ".r", v) }
fun stickAntiDeadzone(left: Boolean): Float = prefStickAntiDz.get(left)
fun setStickAntiDeadzone(left: Boolean, v: Float) = prefStickAntiDz.set(left, v)
@@ -4361,7 +4361,12 @@ open class MainActivityRuntime : ComponentActivity() {
val mag = kotlin.math.hypot(gx, gy)
if (mag <= 0f) return
val shaped = shapeStickMag(mag.coerceAtMost(1f), left)
val scale = shaped / mag // preserves direction; caps square-gate diagonals at unit circle
// Dividing by the magnitude caps a full diagonal at the unit circle: 0.707 per axis, which
// is what a circular-gated DualShock 3 really sends. Games that deadzone each axis on its
// own then ignore diagonals almost entirely. Dividing by the LARGER axis instead expands
// to the square, so a full diagonal reaches 1.0 on both. Same result on the cardinals.
val denom = if (ControllerMappings.stickSquareGate(left)) kotlin.math.max(abs(gx), abs(gy)) else mag
val scale = if (denom > 0f) shaped / denom else 0f
val ox = gx * scale
val oy = gy * scale
if (ox > 0f) accumAnalog(aXPos, ox) else if (ox < 0f) accumAnalog(aXNeg, -ox)
@@ -792,6 +792,7 @@ fun AppTab() {
)
ClearCacheRow()
ResetAllSettingsRow()
}
}
@@ -836,6 +837,77 @@ private fun ClearCacheRow() {
}
}
/** Put every global setting back to its default in one go.
*
* The per-tab Reset in the top bar only covers the page you are looking at, which is right for
* undoing one experiment but tedious when a config has drifted across half a dozen tabs. This is
* the "start clean" button. Per-game overrides are deliberately left alone: they belong to
* individual games, are invisible from here, and wiping them from a global page would be a
* surprise. Controller binds live in ControllerMappings and keep their own reset. */
@Composable
private fun ResetAllSettingsRow() {
var confirming by remember { mutableStateOf(false) }
Surface(
onClick = { confirming = true },
modifier = Modifier.fillMaxWidth()
.controllerFocusable("app.resetAll", RoundedCornerShape(20.dp), onConfirm = { confirming = true }),
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.72f),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.46f)),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Surface(
modifier = Modifier.size(46.dp),
shape = RoundedCornerShape(14.dp),
color = MaterialTheme.colorScheme.errorContainer,
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
Text("↺", fontSize = 21.sp)
}
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
Text(str("app.resetAll"), style = MaterialTheme.typography.titleMedium)
Text(
str("app.resetAll.desc"),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
if (confirming) {
com.armsx2.ui.common.ConfirmOverlay(
title = str("app.resetAll"),
message = str("app.resetAll.confirm"),
confirmLabel = str("action.reset"),
destructive = true,
idPrefix = "settings-reset-all",
onConfirm = {
val defaults = com.armsx2.config.Settings()
com.armsx2.ui.InGameOverlay.settingsState.value = defaults
com.armsx2.config.ConfigStore.saveGlobal(defaults)
// Push straight to the core when a game is live, the same way the per-tab reset
// does. Without this the UI shows defaults while the running VM keeps the old
// values until the next boot.
if (MainActivityRuntime.nativeReady.value &&
MainActivityRuntime.eState.value != com.armsx2.EmuState.STOPPED) {
runCatching { defaults.applyTo() }
}
confirming = false
},
onDismiss = { confirming = false },
)
}
}
/** Export / import everything a reinstall would destroy: save states, memory cards, artwork,
* per-game settings, controller profiles, patches and every preference. ROMs and BIOS are left
* out — those live outside the app and survive on their own. See [com.armsx2.BackupManager]. */
@@ -56,7 +56,7 @@ import kotlinx.coroutines.withContext
import com.armsx3.NativeApp
@Composable
fun PadTab(@Suppress("UNUSED_PARAMETER") state: MutableState<Settings>) {
fun PadTab(state: MutableState<Settings>) {
val scroll = settingsScrollState()
ControllerAutoScroll(scroll)
val capture = remember { mutableStateOf<ControllerMappings.Action?>(null) }
@@ -448,6 +448,24 @@ fun PadTab(@Suppress("UNUSED_PARAMETER") state: MutableState<Settings>) {
// (com.armsx2.ui.settings.GyroSection). Here it follows the Pad tab's Global/Game
// scope (editSerial) and shares the tab's refreshToken so it re-reads live.
GyroSection(editSerial = editSerial, externalRefresh = refreshToken)
// Which face button the PS3 itself treats as "confirm" in system dialogs. This is a
// console setting (cellSysutil ID_ENTER_BUTTON_ASSIGN), not a pad remap: it changes what
// the GAME asks for, so it has to live in the config rather than in the bind table.
// Japanese titles generally expect circle and can read as inverted without it.
CollapsibleSection(str("pad.section.enterButton"), initiallyExpanded = false) {
SegmentedRow(
label = str("pad.enterButton.label"),
options = listOf(str("pad.enterButton.circle"), str("pad.enterButton.cross")),
selectedIndex = state.value.ps3.enterButtonAssign.coerceIn(0, 1),
description = str("pad.enterButton.description"),
onChange = { idx ->
com.armsx2.ui.InGameOverlay.saveSettings(
state.value.copy(ps3 = state.value.ps3.copy(enterButtonAssign = idx)),
)
},
)
}
CollapsibleSection(str("pad.section.buttonMapping"), initiallyExpanded = false) {
ControllerMappings.actions.forEach { action ->
val physical = ControllerMappings.physicalForScope(action, editPlayer.intValue, editSerial)
@@ -899,6 +917,12 @@ private fun StickFeelSliders(left: Boolean, title: String, refreshToken: Mutable
valueFormatter = { "${it * 5}%" },
onChange = { ControllerMappings.setStickSensitivity(left, it / 20f); refreshToken.value++ },
)
ToggleRow(
str("pad.stickFeel.squareGate.label"),
ControllerMappings.stickSquareGate(left),
description = str("pad.stickFeel.squareGate.description"),
) { ControllerMappings.setStickSquareGate(left, it); refreshToken.value++ }
SettingsDivider()
IntSliderRow(
label = str("pad.stickFeel.acceleration.label"),
@@ -25,73 +25,87 @@ import org.json.JSONObject
internal val SETTINGS_CATEGORY_FIELDS: Map<SettingsCategory, List<String>> = mapOf(
// PerformanceTab.kt
SettingsCategory.Performance to listOf(
"eeClampMode", "eeCycleRate", "eeCycleSkip", "eeFpuRoundMode",
"fastCDVD", "fpsLimit", "frameSkip", "framerateNtsc", "frameratePal",
"intcStat", "mtvu", "nominalSpeedPercent", "skipDuplicateFrames", "vu0RoundMode",
"vu1Instant", "vu1RoundMode", "vuClampMode", "vuDeferredWrites", "vuFlagHack",
"vuNeonFusions", "vuSkipStallSim", "waitLoop",
"accurateBlendingUnit", "affinityMode", "eeClampMode", "eeCycleRate", "eeCycleSkip",
"eeFpuRoundMode", "fastCDVD", "fpsLimit", "frameSkip", "framerateNtsc", "frameratePal",
"hwMipmap", "hwRov", "hwScaler", "intcStat", "mtvu", "nominalSpeedPercent",
"ps3AccurateCacheLine", "ps3AccurateSpuDma", "ps3AccurateSpuRsv", "ps3ClocksScale",
"ps3GpuTurbo", "ps3LlvmPrecompile", "ps3LlvmThreads", "ps3MaxSpursThreads",
"ps3PpuDecoder", "ps3PreferredSpuThreads", "ps3SavestateCompatibleMode",
"ps3SilenceAllLogs", "ps3SpuBlockSize", "ps3SpuCache", "ps3SpuDecoder",
"ps3SpuLoopDetection", "ps3SpuXFloat", "screenResOverride", "skipDuplicateFrames",
"texturePreloading", "upscaleFloat", "vu0RoundMode", "vu1Instant", "vu1RoundMode",
"vuClampMode", "vuDeferredWrites", "vuFlagHack", "vuNeonFusions", "vuSkipStallSim",
"waitLoop",
),
// RendererTab.kt
SettingsCategory.Graphics to listOf(
"accurateBlendingUnit", "adrenoFbFetch", "aspectRatio", "casMode", "casSharpness",
"customAspectRatio", "deinterlaceMode", "displayBilinear", "dumpReplaceableTextures", "fmvAspectRatio",
"forceMaliFbFetch", "fxaa", "gpuProfile", "gsBackThreadMode", "hardwareDownloadMode",
"hwAa1", "hwAccurateAlphaTest", "hwMipmap", "hwRov", "loadTextureReplacements",
"adrenoFbFetch", "aspectRatio", "autoProgressiveScan", "casMode", "casSharpness",
"customAspectRatio", "customDriverId", "deinterlaceMode", "displayBilinear",
"displayFitMode", "dumpReplaceableTextures", "fmvAspectRatio", "forceMaliFbFetch",
"fxaa", "gpuProfile", "gsBackThreadMode", "hardwareDownloadMode", "hwAa1",
"hwAccurateAlphaTest", "landscapeRenderTop", "loadTextureReplacements",
"loadTextureReplacementsAsync", "maxAnisotropy", "orientation",
"osdShowTextureReplacements", "portraitRenderTop", "landscapeRenderTop", "autoProgressiveScan",
"affinityMode", "precacheTextureReplacements",
"osdShowTextureReplacements", "portraitRenderTop", "precacheTextureReplacements",
"ps3AnisoFilter", "ps3AsyncTexStream", "ps3DisableZcull", "ps3DisplayAspect",
"ps3MsaaMode", "ps3MultithreadedRsx", "ps3ReadColorBuffers", "ps3ReadDepthBuffer",
"ps3RelaxedZcull", "ps3Resolution", "ps3ShaderMode", "ps3StrictRendering",
"ps3VramLimitMb", "ps3WriteColorBuffers", "ps3WriteDepthBuffer", "renderer",
"shadeBoost", "shadeBoostBrightness", "shadeBoostContrast", "shadeBoostGamma",
"shadeBoostSaturation", "shaderChainEnabled", "shaderChainParams", "shaderChainPreset",
"textureFiltering", "texturePreloading", "triFilter", "tvShader", "upscaleFloat",
"vsyncEnable", "displayFitMode", "ps3DisplayAspect",
"textureFiltering", "triFilter", "tvShader", "upscaleFloat", "useAngleOpenGL",
"vsyncEnable",
),
// AudioTab.kt
SettingsCategory.Audio to listOf(
"audioBufferMs", "audioFastForwardVolume", "audioMuted", "audioOpenSLES",
"audioOutputLatencyMs", "audioSwapChannels", "audioTimeStretch", "audioVolume",
"spu2LightweightMix", "spu2NeonReverb",
"ps3AudioBufferMs", "ps3AudioChannels", "ps3AudioCubebBackend", "ps3AudioFormat",
"ps3AudioRenderer", "ps3AudioTimeStretch", "spu2LightweightMix", "spu2NeonReverb",
),
// NetworkTab.kt
SettingsCategory.Network to listOf(
"dev9AutoGateway", "dev9AutoMask", "dev9Dns1", "dev9Dns2", "dev9EthApi", "dev9EthDevice",
"dev9EthEnable", "dev9EthHosts", "dev9EthLogDhcp", "dev9EthLogDns", "dev9Gateway",
"dev9HddEnable", "dev9HddFile", "dev9InterceptDhcp", "dev9Mask", "dev9ModeDns1",
"dev9ModeDns2", "dev9Ps2Ip", "ip", "url", "usbKeyboard",
"dev9AutoGateway", "dev9AutoMask", "dev9Dns1", "dev9Dns2", "dev9EthApi",
"dev9EthDevice", "dev9EthEnable", "dev9EthHosts", "dev9EthLogDhcp", "dev9EthLogDns",
"dev9Gateway", "dev9HddEnable", "dev9HddFile", "dev9InterceptDhcp", "dev9Mask",
"dev9ModeDns1", "dev9ModeDns2", "dev9Ps2Ip", "ip", "ps3NetEnabled", "ps3PsnStatus",
"ps3UpnpEnabled", "url", "usbKeyboard",
),
// OverlayTab.kt
SettingsCategory.OnScreen to listOf(
"osdColor", "osdScale", "osdShowCpu", "osdShowFps", "osdShowFrameTimes", "osdShowGpu",
"osdShowGpuStats", "osdShowGsStats", "osdShowHardwareInfo", "osdShowInputs",
"osdShowMessages", "osdShowResolution", "osdShowSettings", "osdShowSpeed",
"osdShowVersion", "osdShowVps",
// RPCS3's own performance overlay (the lower half of the tab).
"ps3OverlayEnabled", "ps3OverlayDetail", "ps3OverlayPosition", "ps3OverlayFontSize",
"ps3OverlayOpacity", "ps3OverlayFramerateGraph", "ps3OverlayFrametimeGraph",
"ps3OverlayBodyColor", "ps3OverlayBodyBg", "ps3OverlayTitleColor", "ps3OverlayTitleBg",
"osdShowVersion", "osdShowVps", "ps3OverlayBodyBg", "ps3OverlayBodyColor",
"ps3OverlayDetail", "ps3OverlayEnabled", "ps3OverlayFontSize",
"ps3OverlayFramerateGraph", "ps3OverlayFrametimeGraph", "ps3OverlayOpacity",
"ps3OverlayPosition", "ps3OverlayTitleBg", "ps3OverlayTitleColor",
),
// FixesTab.kt — also owns the GameDB fixes and the recompiler toggles, which moved here
// from Performance and from the retired Recompiler tab.
SettingsCategory.Advanced to listOf(
"enableFastBoot", "enableGameFixes",
"gamefixBlitInternalFps", "gamefixDmaBusy", "gamefixEETiming", "gamefixFpuMul",
"gamefixFullVu0Sync", "gamefixGifFifo", "gamefixGoemonTlb", "gamefixIbit",
"gamefixInstantDma", "gamefixOphFlag", "gamefixSkipMpeg",
"gamefixSoftwareRendererFmv", "gamefixVif1Stall", "gamefixVuAddSub",
"gamefixVuOverflow", "gamefixVuSync", "gamefixXgkick",
"enableFastmem", "recEE", "recIOP", "recVU0", "recVU1",
"alignSprite", "antiBlur", "autoFlush", "autoFlushSw", "bilinearUpscale", "cpuClutRender",
"cpuFramebufferConversion", "cpuSpriteRenderBw", "cpuSpriteRenderLevel", "cropBottom",
"cropLeft", "cropRight", "cropTop", "displayZoom", "disableDepthEmulation", "disableFramebufferFetch",
"disableInterlaceOffset", "disablePartialInvalidation", "disableRenderFixes",
"disableSafeFeatures", "disableShaderCache", "disableVertexShaderExpand", "dithering",
"drawBuffering", "estimateTextureRegion", "forceEvenSpritePosition",
"alignSprite", "antiBlur", "autoFlush", "autoFlushSw", "bilinearUpscale",
"cpuClutRender", "cpuFramebufferConversion", "cpuSpriteRenderBw",
"cpuSpriteRenderLevel", "cropBottom", "cropLeft", "cropRight", "cropTop",
"disableDepthEmulation", "disableFramebufferFetch", "disableInterlaceOffset",
"disablePartialInvalidation", "disableRenderFixes", "disableSafeFeatures",
"disableShaderCache", "disableVertexShaderExpand", "displayZoom", "dithering",
"drawBuffering", "enableFastBoot", "enableFastmem", "enableGameFixes",
"estimateTextureRegion", "forceEvenSpritePosition", "gamefixBlitInternalFps",
"gamefixDmaBusy", "gamefixEETiming", "gamefixFpuMul", "gamefixFullVu0Sync",
"gamefixGifFifo", "gamefixGoemonTlb", "gamefixIbit", "gamefixInstantDma",
"gamefixOphFlag", "gamefixSkipMpeg", "gamefixSoftwareRendererFmv", "gamefixVif1Stall",
"gamefixVuAddSub", "gamefixVuOverflow", "gamefixVuSync", "gamefixXgkick",
"gpuPaletteConversion", "gpuTargetClut", "halfPixelOffset", "hwAccurateAlphaTest",
"integerScaling", "limit24BitDepth", "manualUserHacks", "mergeSprite", "mipmapSw",
"nativeScaling", "overrideTextureBarriers", "preloadFrameData", "readTargetsWhenClosing",
"roundSprite", "screenOffsets", "showOverscan", "skipDrawEnd", "skipDrawStart",
"spinCpuReadbacks", "spinGpuReadbacks", "swThreads", "swThreadsHeight",
"syncToHostRefresh", "textureInsideRt", "textureOffsetX", "textureOffsetY",
"unscaledPaletteDraw", "useBlitSwapChain", "vsyncQueueSize",
"nativeScaling", "overrideTextureBarriers", "preloadFrameData", "ps3AccurateCacheLine",
"ps3AccurateDfma", "ps3AccurateRsxRsv", "ps3AccurateSpuRsv", "ps3DebugConsoleMode",
"ps3HleLwmutex", "ps3PpuNanHandling", "ps3PpuRsvPriority", "ps3PreciseSpuVerification",
"ps3SetDazFtz", "ps3SleepTimers", "ps3SpuVerification", "ps3SpuXFloat",
"readTargetsWhenClosing", "recEE", "recIOP", "recVU0", "recVU1", "roundSprite",
"screenOffsets", "showOverscan", "skipDrawEnd", "skipDrawStart", "spinCpuReadbacks",
"spinGpuReadbacks", "swThreads", "swThreadsHeight", "syncToHostRefresh",
"textureInsideRt", "textureOffsetX", "textureOffsetY", "unscaledPaletteDraw",
"useBlitSwapChain", "vsyncQueueSize",
),
// Controls / Hotkeys / Skins / General / Info / Patches / About own no Settings fields —
// Controls keeps its binds and tunables in ControllerMappings and has its own reset row.
+49 -3
View File
@@ -2174,6 +2174,19 @@ error_code sys_fs_unlink(ppu_thread& ppu, vm::cptr<char> path)
{
return {mp == &g_mp_sys_dev_hdd1 ? sys_fs.warning : sys_fs.error, CELL_ENOENT, path};
}
case fs::error::readonly:
{
// A virtual device that cannot be modified. On Android that is the mounted ISO behind
// /app_home, whose device_base mutators report readonly by design -- so any game that
// writes, renames or deletes inside its own USRDIR lands here. Oblivion deletes
// /app_home/warnings.txt at startup, which a real console would simply allow.
//
// This used to fall through to the throw below, which killed the PPU thread inside the
// syscall: the game froze with the CPU idle and nothing in the log but a stalled RSX.
// Report it and let the guest decide what to do, which is what hardware would do for a
// write to read-only media.
return { CELL_EROFS, path };
}
default:
{
if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath)))
@@ -2425,7 +2438,9 @@ error_code sys_fs_fcntl(ppu_thread& ppu, u32 fd, u32 op, vm::ptr<void> _arg, u32
// Load mountpoint (doesn't support multiple // at the start)
std::string_view vpath{arg->name.get_ptr(), arg->name_size};
sys_fs.notice("sys_fs_fcntl(0xc0000006): %s", vpath);
// Trace for the same reason as sys_fs_utime: this rides along with the utime polling
// loop and made up the last third of that flood.
sys_fs.trace("sys_fs_fcntl(0xc0000006): %s", vpath);
// Check only mountpoint
vpath = vpath.substr(0, vpath.find_first_of('\0'));
@@ -3087,6 +3102,19 @@ error_code sys_fs_truncate(ppu_thread& ppu, vm::cptr<char> path, u64 size)
{
return {mp == &g_mp_sys_dev_hdd1 ? sys_fs.warning : sys_fs.error, CELL_ENOENT, path};
}
case fs::error::readonly:
{
// A virtual device that cannot be modified. On Android that is the mounted ISO behind
// /app_home, whose device_base mutators report readonly by design -- so any game that
// writes, renames or deletes inside its own USRDIR lands here. Oblivion deletes
// /app_home/warnings.txt at startup, which a real console would simply allow.
//
// This used to fall through to the throw below, which killed the PPU thread inside the
// syscall: the game froze with the CPU idle and nothing in the log but a stalled RSX.
// Report it and let the guest decide what to do, which is what hardware would do for a
// write to read-only media.
return { CELL_EROFS, path };
}
default:
{
if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath)))
@@ -3311,8 +3339,13 @@ error_code sys_fs_utime(ppu_thread& ppu, vm::cptr<char> path, vm::cptr<CellFsUti
{
lv2_obj::sleep(ppu);
sys_fs.warning("sys_fs_utime(path=%s, timep=*0x%x)", path, timep);
sys_fs.warning("** actime=%u, modtime=%u", timep->actime, timep->modtime);
// Trace, not warning. Setting a file's timestamps is routine and uninteresting, but games
// that poll it do so in tight loops: Oblivion's FileCaching thread hit this 7274 times in ten
// seconds on one .BSA, and at two warning lines a call that alone stalled the emulator for
// over twenty seconds. Logging is not free on Android. Raise the sys_fs channel to Trace to
// get these back.
sys_fs.trace("sys_fs_utime(path=%s, timep=*0x%x)", path, timep);
sys_fs.trace("** actime=%u, modtime=%u", timep->actime, timep->modtime);
const auto [path_error, vpath] = translate_to_str(path);
@@ -3354,6 +3387,19 @@ error_code sys_fs_utime(ppu_thread& ppu, vm::cptr<char> path, vm::cptr<CellFsUti
{
return {mp == &g_mp_sys_dev_hdd1 ? sys_fs.warning : sys_fs.error, CELL_ENOENT, path};
}
case fs::error::readonly:
{
// A virtual device that cannot be modified. On Android that is the mounted ISO behind
// /app_home, whose device_base mutators report readonly by design -- so any game that
// writes, renames or deletes inside its own USRDIR lands here. Oblivion deletes
// /app_home/warnings.txt at startup, which a real console would simply allow.
//
// This used to fall through to the throw below, which killed the PPU thread inside the
// syscall: the game froze with the CPU idle and nothing in the log but a stalled RSX.
// Report it and let the guest decide what to do, which is what hardware would do for a
// write to read-only media.
return { CELL_EROFS, path };
}
default:
{
if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath)))
+6 -2
View File
@@ -614,7 +614,10 @@ error_code sys_mmapper_map_shared_memory(ppu_thread& ppu, u32 addr, u32 mem_id,
{
ppu.state += cpu_flag::wait;
sys_mmapper.warning("sys_mmapper_map_shared_memory(addr=0x%x, mem_id=0x%x, flags=0x%x)", addr, mem_id, flags);
// Trace, not warning: a successful map is routine, and games that cycle shared memory do it
// thousands of times a second. Oblivion logged 6476 of these in ten seconds, which on Android
// costs more than the mapping itself. Raise the sys_mmapper channel to Trace to get them back.
sys_mmapper.trace("sys_mmapper_map_shared_memory(addr=0x%x, mem_id=0x%x, flags=0x%x)", addr, mem_id, flags);
const auto area = vm::get(vm::any, addr);
@@ -765,7 +768,8 @@ error_code sys_mmapper_unmap_shared_memory(ppu_thread& ppu, u32 addr, vm::ptr<u3
{
ppu.state += cpu_flag::wait;
sys_mmapper.warning("sys_mmapper_unmap_shared_memory(addr=0x%x, mem_id=*0x%x)", addr, mem_id);
// Pairs with the map above; same reasoning.
sys_mmapper.trace("sys_mmapper_unmap_shared_memory(addr=0x%x, mem_id=*0x%x)", addr, mem_id);
const auto area = vm::get(vm::any, addr);
+16 -1
View File
@@ -1194,7 +1194,22 @@ namespace vm
if (!utils::memory_lock(g_sudo_addr + addr, size))
{
vm_log.error("Failed to lock sudo memory (addr=0x%x, size=0x%x). Consider increasing your system limits.", addr, size);
// Report this once per session, not once per call.
//
// Android does not grant RLIMIT_MEMLOCK to ordinary apps, so this fails for every
// mapping and cannot be "fixed" by the user the message tells to raise their limits.
// Games that map and unmap shared memory in a loop then drown the log in it --
// Oblivion's BSTaskManagerThread produced 6470 of these in ten seconds, and writing
// them is expensive enough on Android to stall the emulator outright.
//
// The first one still says what happened; the rest are the same fact repeated.
static atomic_t<bool> s_reported{false};
if (!s_reported.exchange(true))
{
vm_log.error("Failed to lock sudo memory (addr=0x%x, size=0x%x). Consider increasing your system limits."
" Further failures will not be reported.", addr, size);
}
}
}
+33
View File
@@ -744,10 +744,15 @@ namespace rsx
// why it is game- and timing-dependent rather than reliable.
fifo_ctrl->sync_get_force();
// Spin budget for the idle wait below. Reset whenever a fresh idle period starts, so
// each drain gets its own short hot window before parking.
static thread_local u32 s_fifo_idle_spins = 0;
if (performance_counters.state == FIFO::state::running)
{
performance_counters.FIFO_idle_timestamp = get_system_time();
performance_counters.state = FIFO::state::empty;
s_fifo_idle_spins = 0;
}
else
{
@@ -776,8 +781,36 @@ namespace rsx
// It has now caused two wrong conclusions in one session: once reading a
// starving RSX as CPU-bound decode, and once reading a thread stuck in an
// occlusion query wait as the same thing.
// UPDATE: the yield is now itself the problem, and it is measured. A native
// profile of Arkham City gameplay put ~11% of TOTAL process CPU in sched_yield
// reached from here -- 93% of the RSX thread's kernel time, and its single largest
// cost. sched_yield is close to the worst available wait on this device: it is a
// syscall, it forces a scheduler pass, and with ~14 hot threads over 8 cores it is
// usually rescheduled immediately -- so it burns a core one of the five SPU threads
// actually wants, while doing nothing to notice the guest sooner.
//
// A short hot spin first, so a PUT that lands within microseconds is still caught
// without paying any wake latency; only sustained idle parks. WFE costs no syscall
// and the architected event stream bounds the park to tens of microseconds, so the
// RSX still sits on the frame's dependency chain rather than sleeping through work.
//
// The pre-spin is not optional: ouroboros420/rpcsx parked bare here (e31ef44ef) and
// had to walk it back (832c23078) when the wake latency cost frametime smoothness.
RSX_PROF_SCOPE(idle);
#if defined(ARCH_ARM64)
if (s_fifo_idle_spins < 8)
{
s_fifo_idle_spins++;
utils::pause();
}
else
{
utils::wait_for_event();
}
#else
std::this_thread::yield();
#endif
}
return;
+54
View File
@@ -15,7 +15,12 @@
#include "gcm_printing.h"
#include "RSXDisAsm.h"
#ifdef __ANDROID__
#include <unistd.h> // ::gettid() for the ADPF feed
#endif
#include "Emu/System.h"
#include "Emu/system_utils.hpp"
#include "Emu/Cell/PPUThread.h"
#include "Emu/Cell/SPUThread.h"
#include "Emu/Cell/timers.hpp"
@@ -2778,6 +2783,55 @@ namespace rsx
{
m_eng_interrupt_mask.clear(rsx::display_interrupt);
#ifdef __ANDROID__
// ADPF feed: publish this frame's real CPU cost and the presenting thread's OS tid so
// the app can drive PerformanceHintManager. Measured at a fixed point each iteration, so
// the previous iteration's frame-limiter sleep lands in the idle delta and is excluded.
// Advisory only: these go to atomics nothing in the core reads back.
// Ported from ouroboros420/rpcsx (3d4ba6060).
{
static thread_local u64 s_last_now = 0;
static thread_local u64 s_last_idle = 0;
static thread_local s32 s_tid = 0;
if (s_tid == 0)
{
s_tid = static_cast<s32>(::gettid());
}
// Republished every flip so a recreated RSX thread overwrites a stale tid, rather
// than leaving the app's hint session pointed at a dead thread after a restart.
rpcs3::utils::set_rsx_thread_tid(s_tid);
const u64 now_us = get_system_time();
const u64 idle_us = performance_counters.idle_time.load();
if (s_last_now != 0 && now_us > s_last_now)
{
const u64 wall = now_us - s_last_now;
// The flip-to-flip deadline. Without it the hint judges a 30fps game against a
// 60fps target and over-boosts, which is pure heat.
rpcs3::utils::report_frame_period_ns(wall * 1000);
// idle_time is reset periodically by get_load(), so a delta that went backwards is
// a reset, not a real frame. Idle can also exceed the wall window (it accrues from
// FIFO/semaphore paths). Reporting work == wall in either case would feed a bogus
// fully-busy sample and over-boost; skipping leaves the last good one in place.
if (idle_us >= s_last_idle)
{
if (const u64 idle_delta = idle_us - s_last_idle; idle_delta < wall)
{
rpcs3::utils::report_frame_work_ns((wall - idle_delta) * 1000);
}
}
}
s_last_now = now_us;
s_last_idle = idle_us;
}
#endif
if (async_flip_requested & flip_request::any)
{
// Deferred flip
+16 -1
View File
@@ -314,7 +314,22 @@ namespace vk
shader_types_support.allow_float16 = (driver_properties.driverID == VK_DRIVER_ID_AMD_PROPRIETARY_KHR);
}
if (is_MOBILE(get_driver_vendor()) && shader_types_support.allow_float16)
// Qualcomm's compiler stopped rejecting native float16 at some point, and the blanket
// disable below now costs more than it saves: emulating fp16 with fp32 does NOT "render
// correctly" as claimed -- Oblivion's water simply does not draw on Vulkan, while the GL
// backend (which has no such workaround) draws it. Verified fixed by allowing fp16 on
// driver 512.676.53.
//
// Gated on the version rather than removed. The original failure is a bad one to
// reintroduce -- every pipeline rejected, so the game is black while audio and the
// compile overlay keep working, which reads as a renderer bug rather than a shader one --
// and older Adreno drivers may still be affected. Only Adreno is opened up: no other
// mobile vendor has been tested either way, so they keep the safe path.
constexpr u32 s_adreno_fp16_min_driver = (512u << 22) | (676u << 12) | 53u; // 512.676.53
const bool adreno_fp16_ok = is_ADRENO(get_driver_vendor()) &&
props.driverVersion >= s_adreno_fp16_min_driver;
if (!adreno_fp16_ok && is_MOBILE(get_driver_vendor()) && shader_types_support.allow_float16)
{
// Adreno advertises shaderFloat16, but its shader compiler rejects the
// SPIR-V RPCS3 generates with native float16_t in it -- every game
+8
View File
@@ -264,7 +264,15 @@ struct cfg_root : cfg::node
{
node_audio(cfg::node* _this) : cfg::node(_this, "Audio") {}
#ifdef __ANDROID__
// Oboe by default here. Cubeb reaches AAudio too, but Oboe is what carries the per-device
// quirks database, the automatic AAudio -> OpenSL ES fallback on parts where AAudio
// misbehaves, and error-callback stream recovery when the route changes or the device
// disconnects -- all of which are the normal case on a handheld, not the exception.
cfg::_enum<audio_renderer> renderer{ this, "Renderer", audio_renderer::oboe, true };
#else
cfg::_enum<audio_renderer> renderer{ this, "Renderer", audio_renderer::cubeb, true };
#endif
cfg::_enum<audio_provider> provider{ this, "Audio Provider", audio_provider::cell_audio, false };
cfg::_enum<audio_avport> rsxaudio_port{ this, "RSXAudio Avport", audio_avport::hdmi_0, true };
cfg::_bool dump_to_file{ this, "Dump to file", false, true };