diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index 0180539df..1a0639201 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -155,6 +155,7 @@ class FrontEndActivity : ComponentActivity() { private var gpuClockPin by mutableStateOf(false) private var prootNoSeccomp by mutableStateOf(false) private var prootFastPath by mutableStateOf(true) + private var cursorComposite by mutableStateOf(false) private var guestHostname by mutableStateOf(SessionPrefs.DEFAULT_GUEST_HOSTNAME) private var phantomWarning by mutableStateOf(null) private var phantomProcessStatus by mutableStateOf(PhantomProcessStatus.NOT_APPLICABLE) @@ -1771,7 +1772,8 @@ class FrontEndActivity : ComponentActivity() { fsyncFirst = fsyncFirst, gamescopeRealtime = gamescopeRealtime, gpuClockPin = gpuClockPin, - prootNoSeccomp = prootNoSeccomp, prootFastPath = prootFastPath, guestHostname = guestHostname, phantomWarning = phantomWarning, + prootNoSeccomp = prootNoSeccomp, prootFastPath = prootFastPath, cursorComposite = cursorComposite, + guestHostname = guestHostname, phantomWarning = phantomWarning, onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on }, onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on }, onZinkLazy = { on -> SessionPrefs.setZinkLazy(this, on); zinkLazy = on }, @@ -1785,6 +1787,7 @@ class FrontEndActivity : ComponentActivity() { onGpuClockPin = { on -> SessionPrefs.setGpuClockPin(this, on); gpuClockPin = on }, onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on }, onProotFastPath = { on -> SessionPrefs.setProotFastPath(this, on); prootFastPath = on }, + onCursorComposite = { on -> SessionPrefs.setCursorComposite(this, on); cursorComposite = on }, onGuestHostname = { name -> SessionPrefs.setGuestHostname(this, name) }, onClientCore = { core, on -> clientCores = if (on) clientCores + core else clientCores - core @@ -1967,6 +1970,7 @@ class FrontEndActivity : ComponentActivity() { gpuClockPin = SessionPrefs.gpuClockPin(this) prootNoSeccomp = SessionPrefs.prootNoSeccomp(this) prootFastPath = SessionPrefs.prootFastPath(this) + cursorComposite = SessionPrefs.cursorComposite(this) guestHostname = SessionPrefs.guestHostname(this) refreshPhantomStatus() } 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 0b8ef2824..5d5861d3a 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -472,6 +472,13 @@ object SessionPrefs { prefs(context).edit().putBoolean("prootFastPath", on).apply() } + /** gamescope draws the cursor itself. Off by default: on a phone that plane costs frames. */ + fun cursorComposite(context: Context): Boolean = prefs(context).getBoolean("cursorComposite", false) + + fun setCursorComposite(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("cursorComposite", on).apply() + } + const val DEFAULT_GUEST_HOSTNAME = "DroidDeck" @JvmStatic 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 f014207e9..44f6cc4c4 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -720,6 +720,7 @@ class SessionService : Service() { guest.add("BL_SYNC_FALLBACK=" + (if (SessionPrefs.syncFallback(this)) "1" else "0")) guest.add("BL_FSYNC_FIRST=" + (if (SessionPrefs.fsyncFirst(this)) "1" else "0")) guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0")) + if (SessionPrefs.cursorComposite(this)) guest.add("BL_CURSOR_COMPOSITE=1") if (com.droiddeck.launcher.core.ZramSupport.supported()) guest.add("BL_ZRAM=1") guest.add("BANNER_AUDIO_DIRECT_DECAY=0") val extra = extraEnv() + com.droiddeck.launcher.agent.AgentEnv.beginSession(this) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt b/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt index 0c430c1c5..27bc081b0 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt @@ -20,7 +20,7 @@ object SettingsReset { "gpuDriverMode", "androidDriver", "linuxDriver", "linuxDriver.steam", "linuxDriver.desktop", "tuSysmem", "glThread", "noGlError", "zinkLazy", "mangoapp", "gpuClockPin", "gamescopeRealtime", "directAudio", "mic", "wifiDiscovery", "clientCpusOverride", "clientCpus", "gameCpus", - "noXalia", "fastSync", "fsyncFirst", "syncFallback", "prootNoSeccomp", "prootFastPath", + "noXalia", "fastSync", "fsyncFirst", "syncFallback", "prootNoSeccomp", "prootFastPath", "cursorComposite", "guestHostname", "fexPreset", "forceSsbs", "logs", "storageDiagnostics", "upscaler", "upscaleSharpness", "screenEffects", "textureAnisotropy", "textureLodBias", "steamDownloadsInBackground", "steam_offline", "bootMovie", "bootMovieSound", "bootMovieVolume", "bootMovieSeconds", 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 b46133463..25f344a77 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt @@ -46,6 +46,7 @@ fun PerformancePage( gpuClockPin: Boolean, prootNoSeccomp: Boolean, prootFastPath: Boolean, + cursorComposite: Boolean, guestHostname: String, phantomWarning: String?, onClientOverride: (Boolean) -> Unit, @@ -61,6 +62,7 @@ fun PerformancePage( onGpuClockPin: (Boolean) -> Unit, onProotNoSeccomp: (Boolean) -> Unit, onProotFastPath: (Boolean) -> Unit, + onCursorComposite: (Boolean) -> Unit, onGuestHostname: (String) -> Unit, onClientCore: (Int, Boolean) -> Unit, onGameCore: (Int, Boolean) -> Unit, @@ -158,6 +160,11 @@ fun PerformancePage( else stringResource(R.string.perf_fastpath_hint), prootFastPath && !prootNoSeccomp, enabled = !prootNoSeccomp, onChange = onProotFastPath, ) + ToggleRow( + host, "cursorcomposite", stringResource(R.string.perf_cursor_composite), + stringResource(R.string.perf_cursor_composite_hint), + cursorComposite, onChange = onCursorComposite, + ) } SettingsGroup(stringResource(R.string.perf_identity)) { var draft by remember(guestHostname) { mutableStateOf(guestHostname) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 698634587..26fe5a93e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -568,6 +568,8 @@ proot fast path Needs proot\'s seccomp filter. Faster file lookups and less stutter. Turn off if a program can\'t find its files. + Draw the cursor inside gamescope + Gamescope paints the pointer itself instead of handing it to Android. Turn on for a controller-driven cursor. Off is faster on a phone. Applies next session. Session identity Host name The name Steam sees for this device. Blank restores %1$s. diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 3badd65ec..d8a44be52 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -227,7 +227,9 @@ if [ -z "${BL_INSIDE:-}" ]; then echo "== gamescope: games at ${nested}x${height} stretched to the ${width}x${height} output" fi fi - if [ "$mode" = steam ]; then + # Off unless Performance asks for it. Compositing the cursor here adds a second + # plane on the phone's compositor; a controller-driven cursor is the reason to turn it on. + if [ "$mode" = steam ] && [ "${BL_CURSOR_COMPOSITE:-0}" = 1 ]; then export gamescope_cursor_composite=2 fi args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$nested" -h "$height") diff --git a/tools/tests/test_session_cursor_routing.py b/tools/tests/test_session_cursor_routing.py index fdc88f75d..60d28edc4 100644 --- a/tools/tests/test_session_cursor_routing.py +++ b/tools/tests/test_session_cursor_routing.py @@ -1,4 +1,4 @@ -"""The XTest cursor path is enabled only for Steam sessions.""" +"""Gamescope composites the cursor only when a Steam session asks for it.""" import os from pathlib import Path import re @@ -8,29 +8,37 @@ import unittest SESSION = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin/droiddeck-session' SOURCE = SESSION.read_text() MATCH = re.search( - r'^ if \[ "\$mode" = steam \]; then\n.*?^ fi\n', SOURCE, re.M | re.S) + r'^ if \[ "\$mode" = steam \] && \[ "\$\{BL_CURSOR_COMPOSITE:-0\}" = 1 \]; then\n.*?^ fi\n', + SOURCE, re.M | re.S) assert MATCH is not None CURSOR_SETUP = MATCH.group(0) class SessionCursorRoutingTest(unittest.TestCase): - def run_setup(self, mode, initial=None): + def run_setup(self, mode, initial=None, composite=None): env = dict(os.environ, mode=mode) if initial is not None: env['gamescope_cursor_composite'] = initial else: env.pop('gamescope_cursor_composite', None) + if composite is not None: + env['BL_CURSOR_COMPOSITE'] = composite + else: + env.pop('BL_CURSOR_COMPOSITE', None) result = subprocess.run( ['bash', '-c', CURSOR_SETUP + 'printf "%s\n" "${gamescope_cursor_composite-}"'], env=env, text=True, capture_output=True, check=True) return result.stdout.strip() - def test_steam_enables_manual_composition_for_the_nested_cursor(self): - self.assertEqual('2', self.run_setup('steam')) + def test_steam_leaves_the_cursor_with_the_host_unless_asked(self): + self.assertEqual('', self.run_setup('steam')) + + def test_steam_composites_the_cursor_when_the_setting_is_on(self): + self.assertEqual('2', self.run_setup('steam', composite='1')) def test_other_sessions_keep_their_existing_cursor_composition(self): - self.assertEqual('', self.run_setup('desktop')) - self.assertEqual('1', self.run_setup('run', '1')) + self.assertEqual('', self.run_setup('desktop', composite='1')) + self.assertEqual('1', self.run_setup('run', '1', composite='1')) if __name__ == '__main__':