diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index e40c575c9..cf788f5ac 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -126,7 +126,8 @@ class MainActivity : ComponentActivity() { private var gameCores by mutableStateOf>(emptySet()) private var tuSysmem by mutableStateOf(false) private var zinkLazy by mutableStateOf(false) - private var noXalia by mutableStateOf(false) + private var noXalia by mutableStateOf(true) + private var gamescopeRealtime by mutableStateOf(false) private var prootNoSeccomp by mutableStateOf(false) private var guestHostname by mutableStateOf(SessionPrefs.DEFAULT_GUEST_HOSTNAME) private var phantomWarning by mutableStateOf(null) @@ -503,8 +504,9 @@ class MainActivity : ComponentActivity() { if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } - // The microphone is on by default; ask once, with the storage prompt, so voice chat works - // without a trip to the settings. A refusal is not asked again - the toggle asks when used. + // The microphone is off by default. Someone who turned it on before the permission was + // granted is asked once here, with the storage prompt; a refusal is not asked again - the + // toggle asks when used. if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this) && checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { wanted.add(Manifest.permission.RECORD_AUDIO) @@ -893,6 +895,7 @@ class MainActivity : ComponentActivity() { cores = CpuCores.all.map { c -> CoreRow(c, "cpu$c" + (CpuCores.maxGhz(c)?.let { String.format(java.util.Locale.US, " ยท %.1f GHz", it) } ?: "")) }, clientOverride = clientOverride, clientCores = clientCores, gameCores = gameCores, tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, noXalia = noXalia, + gamescopeRealtime = gamescopeRealtime, prootNoSeccomp = prootNoSeccomp, guestHostname = guestHostname, phantomWarning = phantomWarning, onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on }, onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on }, @@ -900,6 +903,7 @@ class MainActivity : ComponentActivity() { onGlThread = { on -> SessionPrefs.setGlThread(this, on); glThread = on }, onNoGlError = { on -> SessionPrefs.setNoGlError(this, on); noGlError = on }, onNoXalia = { on -> SessionPrefs.setNoXalia(this, on); noXalia = on }, + onGamescopeRealtime = { on -> SessionPrefs.setGamescopeRealtime(this, on); gamescopeRealtime = on }, onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on }, onGuestHostname = { name -> SessionPrefs.setGuestHostname(this, name) }, onClientCore = { core, on -> @@ -993,6 +997,7 @@ class MainActivity : ComponentActivity() { tuSysmem = SessionPrefs.tuSysmem(this) zinkLazy = SessionPrefs.zinkLazy(this) noXalia = SessionPrefs.noXalia(this) + gamescopeRealtime = SessionPrefs.gamescopeRealtime(this) prootNoSeccomp = SessionPrefs.prootNoSeccomp(this) guestHostname = SessionPrefs.guestHostname(this) refreshPhantomStatus() diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt index 80550be44..944b752a6 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt @@ -146,6 +146,7 @@ object DeviceReport { k("Steam Deck mode", SessionPrefs.steamDeckMode(context)) k("FEX preset", SessionPrefs.fexPreset(context).ifEmpty { "FEX defaults" }) k("Skip xalia", SessionPrefs.noXalia(context)) + k("gamescope realtime", SessionPrefs.gamescopeRealtime(context)) k("proot without seccomp", SessionPrefs.prootNoSeccomp(context)) k("Guest host name", SessionPrefs.guestHostname(context)) k("DirectAudio for games", SessionPrefs.directAudio(context)) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 820c6e401..07b2e3fba 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -143,8 +143,8 @@ object SessionPrefs { prefs(context).edit().putBoolean("directAudio", on).apply() } - /** The microphone for voice chat, on unless turned off; used only once RECORD_AUDIO is granted. */ - fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", true) + /** The microphone for voice chat, off until the user turns it on (which asks for RECORD_AUDIO). */ + fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", false) /** Whether the app has already asked for the microphone once at start-up. */ fun micAsked(context: Context): Boolean = prefs(context).getBoolean("micAsked", false) @@ -188,14 +188,28 @@ object SessionPrefs { * xalia is an x86 Windows program Proton launches to give Windows programs gamepad navigation. * Under FEX it cannot load the session's aarch64 preload shim, so its socket() and memfd calls * reach the vendor's seccomp filter raw; where that answers ENOSYS - a Galaxy Fold, measured - - * it storms, and the session dies seconds after Big Picture appears. + * it storms, and the session dies seconds after Big Picture appears. On by default: where it + * does run, it sits beside every game under FEX for nothing a controller-first session needs + * (about 10% of a core beside Once Upon a KATAMARI on an SD 8 Gen 2). */ - fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", false) + fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", true) fun setNoXalia(context: Context, on: Boolean) { prefs(context).edit().putBoolean("noXalia", on).apply() } + /** + * Whether gamescope asks for realtime-priority Vulkan queues (GAMESCOPE_FORCE_VULKAN_REALTIME=1, + * which the app's gamescope build honours without CAP_SYS_NICE). Off by default, as in + * Bannerlator's session: the compositor's queue preempting the game's buys nothing on a device + * whose GPU is waiting on the CPU. + */ + fun gamescopeRealtime(context: Context): Boolean = prefs(context).getBoolean("gamescopeRealtime", false) + + fun setGamescopeRealtime(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("gamescopeRealtime", on).apply() + } + /** * Whether proot runs without its seccomp acceleration (PROOT_NO_SECCOMP=1). * diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index 913328d5b..13400e511 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -530,9 +530,12 @@ class SessionService : Service() { SessionPrefs.writeForceFullscreenFlag(this) } // Proton's own gate for its xalia helper (its `proton` script reads this, and sets - // XALIA_SUPPORTED_ONLY itself otherwise). Off by default: xalia is Valve's, and on a device - // whose seccomp answers its syscalls normally there is no reason to take it away. + // XALIA_SUPPORTED_ONLY itself otherwise). Skipped by default: under FEX it costs every game + // a slice of a core for gamepad navigation the session already has. if (SessionPrefs.noXalia(this)) guest.add("PROTON_USE_XALIA=0") + // gamescope's realtime Vulkan queues (the session script turns this into + // GAMESCOPE_FORCE_VULKAN_REALTIME); off unless the user turns it on. + guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0")) // Anything else, for a device that cannot be reached with a debugger: Downloads/droiddeck-env // holds KEY=VALUE lines that go into the session's environment as written, after ours, so a // line here wins. Zink and Turnip tunables (ZINK_DESCRIPTORS=lazy, MESA_*), gamescope's, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt index 16f7a0e50..1083babe2 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt @@ -37,6 +37,7 @@ fun PerformancePage( glThread: Boolean, noGlError: Boolean, noXalia: Boolean, + gamescopeRealtime: Boolean, prootNoSeccomp: Boolean, guestHostname: String, phantomWarning: String?, @@ -46,6 +47,7 @@ fun PerformancePage( onGlThread: (Boolean) -> Unit, onNoGlError: (Boolean) -> Unit, onNoXalia: (Boolean) -> Unit, + onGamescopeRealtime: (Boolean) -> Unit, onProotNoSeccomp: (Boolean) -> Unit, onGuestHostname: (String) -> Unit, onClientCore: (Int, Boolean) -> Unit, @@ -94,6 +96,11 @@ fun PerformancePage( "Disables per-call GL validation.", noGlError, onChange = onNoGlError, ) + ToggleRow( + host, "gsrealtime", "gamescope: realtime GPU queue", + "Gives the compositor's GPU work priority over the game's. Can smooth frame pacing, but can cost games GPU time.", + gamescopeRealtime, onChange = onGamescopeRealtime, + ) } SettingsGroup("Session fixes") { ToggleRow( @@ -103,7 +110,7 @@ fun PerformancePage( ) ToggleRow( host, "xalia", "Skip Steam's xalia helper", - "Disables Proton's gamepad navigation helper. Try if sessions crash at startup.", + "Disables Proton's gamepad navigation helper, which costs every game CPU time. Turn off only if a game needs it.", noXalia, onChange = onNoXalia, ) ToggleRow( diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session index de4c6918b..f46fbca1d 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session @@ -77,8 +77,9 @@ fi # switch instead of on CAP_SYS_NICE, which proot never has; it falls back to normal priority # where the driver refuses, and the runtime's own build ignores it. On KGSL Turnip (Mesa 26.2) it # changes nothing yet: the driver reports one submit-queue priority and creates every KGSL context -# with the same flags, so no priority reaches the GPU. Kept for a driver that maps it. -export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-1} +# with the same flags, so no priority reaches the GPU. Kept for a driver that maps it, and off +# unless the app's Performance page turns it on (BL_GAMESCOPE_REALTIME=1). +export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0} # A sleep that starts no process. Under proot every exec is a traced execve plus the loader's # opens and maps, and the watchers below poll for the whole session: `sleep 1` in a loop was 60