Android: expose GS Multi-threading (GV7 back-thread) in settings + in-game menu

Off/On toggle (On = Pipelined, enum 3) for GSBackThreadMode, default Off:
- Renderer settings tab, under the graphics-API/driver picker
- In-game quick menu (Graphics), grouped with renderer + Apply & Restart
  (MenuSwitchRow gains an optional inline description)
- Settings model with per-game override, i18n strings, search index entry
- native-lib snapshots the field across a live settings apply so the
  restart-required option can't trigger a mid-game device recreate
This commit is contained in:
jpolo1224
2026-07-19 21:04:34 -04:00
parent 22f900e6cb
commit e8234e0ddf
6 changed files with 59 additions and 7 deletions
@@ -1334,6 +1334,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_applyGSSettingsLive(JNIEnv *env, jclass cla
const auto saved_no_vs_expand = EmuConfig.GS.DisableVertexShaderExpand;
const auto saved_tex_barriers = EmuConfig.GS.OverrideTextureBarriers;
const auto saved_depth_feedback = EmuConfig.GS.DepthFeedbackMode;
const auto saved_back_thread = EmuConfig.GS.BackThreadMode;
const auto saved_hwaa1 = EmuConfig.GS.HWAA1;
const auto saved_exclusive_fs = EmuConfig.GS.ExclusiveFullscreenControl;
const auto saved_sw_threads = EmuConfig.GS.SWExtraThreads;
@@ -1361,6 +1362,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_applyGSSettingsLive(JNIEnv *env, jclass cla
EmuConfig.GS.DisableVertexShaderExpand = saved_no_vs_expand;
EmuConfig.GS.OverrideTextureBarriers = saved_tex_barriers;
EmuConfig.GS.DepthFeedbackMode = saved_depth_feedback;
EmuConfig.GS.BackThreadMode = saved_back_thread;
EmuConfig.GS.HWAA1 = saved_hwaa1;
EmuConfig.GS.ExclusiveFullscreenControl = saved_exclusive_fs;
EmuConfig.GS.SWExtraThreads = saved_sw_threads;
@@ -263,6 +263,10 @@ data class Settings(
val useAngleOpenGL: Boolean = false,
/** EmuCore/GS/OverrideTextureBarriers — -1 Auto / 0 Off / 1 On. */
val overrideTextureBarriers: Int = -1,
/** EmuCore/GS/GSBackThreadMode GV7 GS front/back thread split.
* 0 Off (single-threaded), 1 Inline, 2 Lockstep, 3 Pipelined (fastest).
* Defaults to Off (opt-in); a per-game override can raise it. Restart-required. */
val gsBackThreadMode: Int = 0,
/** EmuCore/GS/DisableVertexShaderExpand — force CPU vertex expansion. Renderer-init; restart to apply. */
val disableVertexShaderExpand: Boolean = false,
/** EmuCore/GS/UseBlitSwapChain — blit present model instead of flip. Renderer-init; restart to apply. */
@@ -1066,6 +1070,7 @@ data class Settings(
forceMaliFbFetch = boolAt("EmuCore/GS/ForceMaliFramebufferFetch") ?: this.forceMaliFbFetch,
useAngleOpenGL = boolAt("EmuCore/GS/AndroidUseAngleOpenGL") ?: this.useAngleOpenGL,
overrideTextureBarriers = intAt("EmuCore/GS/OverrideTextureBarriers") ?: this.overrideTextureBarriers,
gsBackThreadMode = intAt("EmuCore/GS/GSBackThreadMode") ?: this.gsBackThreadMode,
disableVertexShaderExpand = boolAt("EmuCore/GS/DisableVertexShaderExpand") ?: this.disableVertexShaderExpand,
useBlitSwapChain = boolAt("EmuCore/GS/UseBlitSwapChain") ?: this.useBlitSwapChain,
disableShaderCache = boolAt("EmuCore/GS/DisableShaderCache") ?: this.disableShaderCache,
@@ -1256,6 +1261,7 @@ data class Settings(
// reflects the toggle.
put("EmuCore/GS", "AndroidUseAngleOpenGL", "bool", useAngleOpenGL.toString())
put("EmuCore/GS", "OverrideTextureBarriers", "int", overrideTextureBarriers.coerceIn(-1, 1).toString())
put("EmuCore/GS", "GSBackThreadMode", "int", gsBackThreadMode.coerceIn(0, 3).toString())
put("EmuCore/GS", "DisableVertexShaderExpand", "bool", disableVertexShaderExpand.toString())
put("EmuCore/GS", "UseBlitSwapChain", "bool", useBlitSwapChain.toString())
put("EmuCore/GS", "DisableShaderCache", "bool", disableShaderCache.toString())
@@ -1487,6 +1493,7 @@ data class Settings(
put("forceMaliFbFetch", forceMaliFbFetch)
put("useAngleOpenGL", useAngleOpenGL)
put("overrideTextureBarriers", overrideTextureBarriers)
put("gsBackThreadMode", gsBackThreadMode)
put("disableVertexShaderExpand", disableVertexShaderExpand)
put("useBlitSwapChain", useBlitSwapChain)
put("disableShaderCache", disableShaderCache)
@@ -1732,6 +1739,7 @@ data class Settings(
forceMaliFbFetch = json.optBoolean("forceMaliFbFetch", def.forceMaliFbFetch),
useAngleOpenGL = json.optBoolean("useAngleOpenGL", def.useAngleOpenGL),
overrideTextureBarriers = json.optInt("overrideTextureBarriers", def.overrideTextureBarriers),
gsBackThreadMode = json.optInt("gsBackThreadMode", def.gsBackThreadMode),
disableVertexShaderExpand = json.optBoolean("disableVertexShaderExpand", def.disableVertexShaderExpand),
useBlitSwapChain = json.optBoolean("useBlitSwapChain", def.useBlitSwapChain),
disableShaderCache = json.optBoolean("disableShaderCache", def.disableShaderCache),
@@ -1961,6 +1969,7 @@ data class Settings(
if (current.forceMaliFbFetch != base.forceMaliFbFetch) j.put("forceMaliFbFetch", current.forceMaliFbFetch)
if (current.useAngleOpenGL != base.useAngleOpenGL) j.put("useAngleOpenGL", current.useAngleOpenGL)
if (current.overrideTextureBarriers != base.overrideTextureBarriers) j.put("overrideTextureBarriers", current.overrideTextureBarriers)
if (current.gsBackThreadMode != base.gsBackThreadMode) j.put("gsBackThreadMode", current.gsBackThreadMode)
if (current.disableVertexShaderExpand != base.disableVertexShaderExpand) j.put("disableVertexShaderExpand", current.disableVertexShaderExpand)
if (current.useBlitSwapChain != base.useBlitSwapChain) j.put("useBlitSwapChain", current.useBlitSwapChain)
if (current.disableShaderCache != base.disableShaderCache) j.put("disableShaderCache", current.disableShaderCache)
@@ -2173,6 +2182,7 @@ data class Settings(
forceMaliFbFetch = if (overrides.has("forceMaliFbFetch")) overrides.getBoolean("forceMaliFbFetch") else base.forceMaliFbFetch,
useAngleOpenGL = if (overrides.has("useAngleOpenGL")) overrides.getBoolean("useAngleOpenGL") else base.useAngleOpenGL,
overrideTextureBarriers = if (overrides.has("overrideTextureBarriers")) overrides.getInt("overrideTextureBarriers") else base.overrideTextureBarriers,
gsBackThreadMode = if (overrides.has("gsBackThreadMode")) overrides.getInt("gsBackThreadMode") else base.gsBackThreadMode,
disableVertexShaderExpand = if (overrides.has("disableVertexShaderExpand")) overrides.getBoolean("disableVertexShaderExpand") else base.disableVertexShaderExpand,
useBlitSwapChain = if (overrides.has("useBlitSwapChain")) overrides.getBoolean("useBlitSwapChain") else base.useBlitSwapChain,
disableShaderCache = if (overrides.has("disableShaderCache")) overrides.getBoolean("disableShaderCache") else base.disableShaderCache,
@@ -992,6 +992,8 @@ val EN: Map<String, String> = mapOf(
"renderer.gamma.label" to "Gamma",
"renderer.gpuProfile.description" to "Overrides Android GPU workaround selection. Auto is recommended.",
"renderer.gpuProfile.label" to "GPU Profile",
"renderer.gsBackThread.label" to "GS Multi-threading",
"renderer.gsBackThread.description" to "Off by default. Runs GS rendering on a second thread so you can push 2x/3x internal resolution without the frame dips. If a game shows graphics glitches, turn it back off (can be set per-game). Restart the game to apply.",
"renderer.gsDump.description" to "Saves one frame of GPU commands (.gs) to the snaps folder — replayable in desktop PCSX2 to diagnose rendering bugs.",
"renderer.gsDump.label" to "Capture GS Dump (bug report)",
"renderer.gsDump.queued" to "GS dump queued — close this menu so it captures the frame. Saved to the snaps folder.",
@@ -577,6 +577,18 @@ private fun GraphicsPane(state: EmulationMenuUiState, viewModel: EmulationMenuVi
viewModel.updateSettings { it.copy(useAngleOpenGL = on) }
}
}
// GS Multi-threading (GV7 front/back split). Restart-required like the renderer /
// driver above, so it lives in the same group — hit Apply & Restart below to apply.
// Off = single-threaded; On = GS on a dedicated back thread (Pipelined, enum 3).
// The Inline/Lockstep dev rungs are not exposed. Description shown inline so users
// who never open full settings still understand what it does.
MenuSwitchRow(
str("renderer.gsBackThread.label"),
settings.gsBackThreadMode >= 3,
description = str("renderer.gsBackThread.description"),
) { on ->
viewModel.updateSettings { it.copy(gsBackThreadMode = if (on) 3 else 0) }
}
CompactAction(str("backend.applyRestart"), "", Modifier.fillMaxWidth(), MainActivityRuntime::restart)
HorizontalOptions(
title = str("renderer.upscale.label"),
@@ -1179,6 +1191,7 @@ private fun MenuSwitchRow(
title: String,
checked: Boolean,
enabled: Boolean = true,
description: String? = null,
onCheckedChange: (Boolean) -> Unit,
) {
Surface(
@@ -1200,13 +1213,22 @@ private fun MenuSwitchRow(
Modifier.padding(horizontal = 13.dp, vertical = 9.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
title,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.titleSmall,
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
)
Column(Modifier.weight(1f)) {
Text(
title,
style = MaterialTheme.typography.titleSmall,
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
)
if (description != null) {
Text(
description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 3,
)
}
}
Spacer(Modifier.width(10.dp))
Switch(checked = checked, onCheckedChange = if (enabled) onCheckedChange else null)
}
@@ -107,6 +107,21 @@ fun RendererTab(state: MutableState<Settings>) {
// from the removed first-run setup renderer page into settings.
RendererBackendSection(state)
SettingsDivider()
// GS Multi-threading (GV7 front/back split), placed right under the
// renderer/driver picker. Off = today's single-threaded path (the
// default — opt-in); On = the GS runs on a dedicated back thread
// (Pipelined, enum value 3). The enum's Inline/Lockstep rungs (1/2)
// are dev-only and deliberately not exposed. Restart-required (in
// RestartOptionsAreEqual); native-lib snapshots the field across a
// live apply, so it only takes effect on the next game boot.
ToggleRow(
str("renderer.gsBackThread.label"),
s.gsBackThreadMode >= 3,
description = str("renderer.gsBackThread.description"),
) {
apply(s.copy(gsBackThreadMode = if (it) 3 else 0))
}
SettingsDivider()
val upscaleIndex = UPSCALE_OPTIONS
.indexOfFirst { abs(it.value - s.upscaleFloat) < 0.01f }
.takeIf { it >= 0 } ?: 0
@@ -67,6 +67,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
SettingsSearchEntry("perf.advancedSpeedhacks.title", true, SettingsCategory.Performance),
SettingsSearchEntry("VSync", false, SettingsCategory.Graphics),
SettingsSearchEntry("renderer.shadeboost.label", true, SettingsCategory.Graphics),
SettingsSearchEntry("renderer.gsBackThread.label", true, SettingsCategory.Graphics),
SettingsSearchEntry("renderer.fxaa.label", true, SettingsCategory.Graphics),
SettingsSearchEntry("renderer.shaderChain.label", true, SettingsCategory.Graphics),
SettingsSearchEntry("renderer.shaderChain.preset.label", true, SettingsCategory.Graphics),