mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
fix: keep the Steam desktop client in KDE (#610)
This commit is contained in:
1 parent
4d1b8421fb
commit
6b2c8ace09
6 files changed
+176
-18
No files matched your search
@@ -0,0 +1,13 @@
|
||||
# Steam desktop client in KDE
|
||||
|
||||
The [Discord report](https://discord.com/channels/1552707793247539220/1552707794430459916/1558974992753098874) describes Steam replacing KDE with a gamescope session. The launcher unconditionally wrote `steam-launch`, which tells the Android app to end KDE and start Steam under gamescope. Other desktop applications have a separate GPU wrapper; this change addresses Steam's desktop client.
|
||||
|
||||
Desktop Steam now runs the existing session supervisor with `BL_INSIDE=1` on KDE's inherited Xwayland display and authentication. CEF uses software rendering because KDE's QPainter compositor accepts shared-memory buffers. Big Picture continues to request the explicit gaming session. A lock keeps repeated menu clicks from starting another supervisor while Steam is starting or updating; subsequent clicks wait for Steam's live IPC pipe and then forward the Steam URL to the existing client. This also prevents a second click during cold startup from launching a client without software CEF. KDE retains its own agent and clipboard helpers when Steam starts and exits.
|
||||
|
||||
Validation on AYN Thor uses the full wireless ADB serial `adb-d234a848-fv2FDl (2)._adb-tls-connect._tcp`. Live script testing displayed the authenticated library, restored the main window from maximized to windowed, opened Settings as a separate window, and forwarded a second launch to Downloads. KDE's panel remained visible, no gamescope process started, and no session handoff file was written. Quitting Steam returned to the running Plasma desktop with its agent still available. An explicit Big Picture launch then switched to a Steam session and reached READY with Steam focused.
|
||||
|
||||
Rapid repeated desktop-icon taps were also tested: one client started with software CEF and the library remained on KDE.
|
||||
|
||||
Host regression tests cover desktop display preservation, game URLs, duplicate launches before and after IPC is ready, explicit Big Picture handoff, non-desktop callers, invalid targets, and software CEF limited to the KDE desktop client. Gameplay through Steam's library on KDE has not been validated; the desktop's existing GPU presentation limitations remain.
|
||||
|
||||
The final release-signed APK was built with `tools/build_local.sh`, verified to contain the exact tested Steam scripts, and installed with `adb install -r`. APK SHA-256: `d104b1e6e20fb10e85fcdd95eaf2a8ca69061547164640b8590e41e4495663e4`. On that installation, two rapid desktop-icon taps started one software-rendered Steam client; the library displayed on KDE and restored to a desktop window. Final session: `2026-10-12-09-desktop`, READY, mode `lxqt`, with no gamescope process or handoff request. The Thor was left running this desktop Steam client.
|
||||
@@ -34,9 +34,9 @@ export MOZ_SHM_NO_SEALS=1
|
||||
# and the KGSL stand-in is not one. Its windows get shm buffers only, so GL programs on it draw with
|
||||
# Mesa's software rasterizer, Plasma's own Qt Quick draws with Qt's software renderer (plasmashell
|
||||
# crashes on llvmpipe here), and a Vulkan program cannot draw at all: games and emulators from the
|
||||
# menu run through droiddeck-gpu (their own gamescope on the app's compositor), and Steam goes back
|
||||
# to the app as a Steam session - SteamOS's "Return to Gaming Mode". The session's own GL choice
|
||||
# (zink on Turnip, for the Steam client) is put back by droiddeck-gpu.
|
||||
# menu run through droiddeck-gpu (their own gamescope on the app's compositor), and Steam's desktop
|
||||
# client uses software CEF on KDE. The session's own GL choice (zink on Turnip)
|
||||
# is put back by droiddeck-gpu.
|
||||
export KWIN_COMPOSE=Q QT_QUICK_BACKEND=software LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe
|
||||
# mpv refuses that software GL and then fails its X11 fallback (an assertion): allowed it, it plays.
|
||||
[ -e /etc/mpv/mpv.conf ] || { mkdir -p /etc/mpv && echo gpu-sw=yes >/etc/mpv/mpv.conf; } 2>/dev/null
|
||||
@@ -61,8 +61,7 @@ chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null
|
||||
mkdir -p /tmp/.X11-unix /tmp/.ICE-unix && chmod 1777 /tmp/.X11-unix /tmp/.ICE-unix 2>/dev/null
|
||||
# Fontconfig defaults and Android language fallbacks are set up by droiddeck-session before this
|
||||
# script or any desktop program starts.
|
||||
# The menu's "Steam" / "Steam Big Picture" entries and one entry per installed game: all of them
|
||||
# hand off to the app (droiddeck-steam-launch), since the client cannot draw on this desktop.
|
||||
# Steam opens on KDE; Steam Big Picture explicitly hands off to the gaming session.
|
||||
export BL_DESKTOP=1
|
||||
ln -sf /usr/local/bin/droiddeck-steam-launch /usr/local/bin/steamdeck-steam 2>/dev/null
|
||||
ln -sf /usr/local/bin/droiddeck-steam-shim /usr/local/bin/steam 2>/dev/null
|
||||
|
||||
@@ -355,11 +355,12 @@ reap_orphans() {
|
||||
|
||||
# Gamescope's Wayland backend does not forward the outer clipboard into Xwayland.
|
||||
# This helper owns only Android text selections and exits with this guest session.
|
||||
if [ -n "${BL_ANDROID_CLIPBOARD:-}" ] && [ -x /usr/local/bin/droiddeck-clipboard ]; then
|
||||
if [ "${BL_DESKTOP:-}" != 1 ] && [ -n "${BL_ANDROID_CLIPBOARD:-}" ] && [ -x /usr/local/bin/droiddeck-clipboard ]; then
|
||||
/usr/local/bin/droiddeck-clipboard "$BL_ANDROID_CLIPBOARD" &
|
||||
fi
|
||||
|
||||
start_agent
|
||||
# KDE already has a session agent; starting another would race its request files.
|
||||
[ "${BL_DESKTOP:-}" = 1 ] || start_agent
|
||||
|
||||
case $mode in
|
||||
desktop)
|
||||
@@ -857,9 +858,16 @@ BUSCONF
|
||||
droiddeck-steam-compat "$steam_root" 2>&1 &
|
||||
# The client exits 42 when it has updated itself and wants a fresh start (steam.sh does this).
|
||||
# BL_STEAM_UI=desktop = the client's desktop UI (a window, the library as a list), which the
|
||||
# desktop's launchers ask for; gamescope shows one window at a time and the game takes over.
|
||||
# desktop's launchers ask for. KDE owns its windows when BL_DESKTOP=1.
|
||||
_bl_ui=(-gamepadui)
|
||||
[ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=()
|
||||
_bl_cef=(-cef-force-gpu -cef-use-gl=angle -cef-use-angle=vulkan)
|
||||
if [ "${BL_DESKTOP:-}" = 1 ] && [ "${BL_STEAM_UI:-}" = desktop ]; then
|
||||
# KDE uses QPainter/shm on Android. CEF's Vulkan buffers cannot be
|
||||
# presented there; its software path keeps the library and dialogs on KDE.
|
||||
_bl_cef=(-cef-disable-gpu)
|
||||
_bl_ui=(-nobigpicture)
|
||||
fi
|
||||
# Deck mode, always outside the desktop UI: -steamdeck is the flag that puts
|
||||
# the Quick Access Menu and the Deck settings pages on screen; the QAM's performance overlay
|
||||
# controls stay greyed out without -steamos3, as SteamOS and Armada launch it. -steamos3 makes
|
||||
@@ -929,7 +937,7 @@ BUSCONF
|
||||
done
|
||||
) &
|
||||
fi
|
||||
echo "== steam args: ${_bl_ui[*]} ${_bl_cdp[*]} ${_bl_lang[*]} -clientbeta $steam_channel -no-cef-sandbox -cef-force-gpu -cef-ozone-platform=x11 -cef-use-gl=angle -cef-use-angle=vulkan $*"
|
||||
echo "== steam args: ${_bl_ui[*]} ${_bl_cdp[*]} ${_bl_lang[*]} -clientbeta $steam_channel -no-cef-sandbox -cef-ozone-platform=x11 ${_bl_cef[*]} $*"
|
||||
# An update that cannot install asks for a restart (rc=42) every time; the count is capped so a
|
||||
# session can never again loop in silence, which reads as a menu opening and closing by itself.
|
||||
steam_updates=0
|
||||
@@ -1020,8 +1028,7 @@ BUSCONF
|
||||
desktop_ui_pid=$!
|
||||
fi
|
||||
"$steam_root/steamrtarm64/steam" "${_bl_ui[@]}" "${_bl_cdp[@]}" "${_bl_lang[@]}" -clientbeta "$steam_channel" \
|
||||
-no-cef-sandbox -cef-force-gpu -cef-ozone-platform=x11 \
|
||||
-cef-use-gl=angle -cef-use-angle=vulkan "$@"
|
||||
-no-cef-sandbox -cef-ozone-platform=x11 "${_bl_cef[@]}" "$@"
|
||||
rc=$?
|
||||
if [ -n "$desktop_ui_pid" ]; then
|
||||
kill "$desktop_ui_pid" 2>/dev/null || true
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
# droiddeck-steam-launch desktop [appid|steam-url] Steam's desktop UI, optionally a game
|
||||
# droiddeck-steam-launch bigpicture [appid|steam-url] Big Picture
|
||||
#
|
||||
# The desktop (KDE Plasma) composites in software and has no dma-buf, so the client's own Vulkan
|
||||
# window - and any game it starts - would die at its first frame there. The request goes to the app
|
||||
# instead, which ends the desktop and starts a Steam session under gamescope in its place, as
|
||||
# SteamOS's desktop hands Steam back to Gaming Mode.
|
||||
# The desktop client stays on KDE's Xwayland. Big Picture is the explicit handoff
|
||||
# to the app's gaming session.
|
||||
set -u
|
||||
ui=${1:-desktop}
|
||||
target=${2:-}
|
||||
@@ -20,6 +18,31 @@ case $target in
|
||||
*) url=steam://rungameid/$target ;;
|
||||
esac
|
||||
|
||||
if [ "$ui" = desktop ] && [ "${BL_DESKTOP:-}" = 1 ]; then
|
||||
steam_root=${XDG_DATA_HOME:-${HOME:-/root}/.local/share}/Steam
|
||||
export LD_LIBRARY_PATH=$steam_root/steamrtarm64:$steam_root/steamrtarm64/panorama${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
|
||||
# A second menu click forwards its URL to Steam's existing instance. Keep only
|
||||
# one session supervisor, including while the client restarts for an update.
|
||||
mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}/droiddeck"
|
||||
exec 9>"${XDG_CACHE_HOME:-$HOME/.cache}/droiddeck/steam-desktop.lock"
|
||||
if ! flock -n 9; then
|
||||
# The supervisor may still be preparing Proton or updating Steam. Invoking
|
||||
# the binary before its IPC pipe exists would start a competing client.
|
||||
for ((attempt = 0; attempt < 180; attempt++)); do
|
||||
pid=$(cat "$HOME/.steam/steam.pid" 2>/dev/null || true)
|
||||
if [ -p "$HOME/.steam/steam.pipe" ] && [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then
|
||||
exec "$steam_root/steamrtarm64/steam" -nobigpicture -no-cef-sandbox \
|
||||
-cef-ozone-platform=x11 -cef-disable-gpu "${url:-steam://open/library}"
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
echo "droiddeck-steam-launch: the desktop client did not become ready" >&2
|
||||
exit 69
|
||||
fi
|
||||
export BL_INSIDE=1 BL_STEAM_UI=desktop
|
||||
exec /usr/local/bin/droiddeck-session steam "${url:-steam://open/library}"
|
||||
fi
|
||||
|
||||
# Ask the app for a Steam session.
|
||||
dir=${BL_LAUNCH_DIR:-}
|
||||
if [ -z "$dir" ] || [ ! -d "$dir" ]; then
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/bin/bash
|
||||
# `steam` on the desktop. The client writes a menu entry per installed game that runs
|
||||
# `steam steam://rungameid/<id>`, and its own launchers say `steam`. On the desktop that goes
|
||||
# through droiddeck-steam-launch - which runs the client on the desktop itself when it draws with
|
||||
# the GPU, hands the URL to a client already running there, and asks the app for a Steam session
|
||||
# on a software desktop. Anywhere else it is the client itself.
|
||||
# through droiddeck-steam-launch, which keeps the desktop client on KDE and
|
||||
# hands URLs to a client already running there. Anywhere else it is the client itself.
|
||||
set -u
|
||||
if [ "${BL_DESKTOP:-}" = 1 ]; then
|
||||
url=
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Desktop Steam stays on KDE; explicit Big Picture still switches sessions."""
|
||||
import fcntl
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
BIN = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin'
|
||||
|
||||
|
||||
class DesktopSteamLaunchTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.session = self.root / 'session'
|
||||
self.session.mkdir()
|
||||
self.env = dict(os.environ, HOME=str(self.root), XDG_DATA_HOME=str(self.root / 'data'),
|
||||
XDG_CACHE_HOME=str(self.root / 'cache'), BL_DESKTOP='1',
|
||||
BL_LAUNCH_DIR=str(self.session))
|
||||
supervisor = self.root / 'supervisor'
|
||||
supervisor.write_text('#!/bin/bash\nprintf "%s\\n" "$BL_INSIDE" "$BL_STEAM_UI" "$DISPLAY" "$@"\n')
|
||||
supervisor.chmod(0o755)
|
||||
self.launcher = self.root / 'launch'
|
||||
self.launcher.write_text((BIN / 'droiddeck-steam-launch').read_text().replace(
|
||||
'/usr/local/bin/droiddeck-session', str(supervisor)))
|
||||
|
||||
def launch(self, *args):
|
||||
return subprocess.run(['bash', str(self.launcher), *args], env=self.env,
|
||||
text=True, capture_output=True)
|
||||
|
||||
def test_desktop_keeps_display_and_does_not_request_session_switch(self):
|
||||
self.env['DISPLAY'] = ':7'
|
||||
result = self.launch('desktop')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(), ['1', 'desktop', ':7', 'steam', 'steam://open/library'])
|
||||
self.assertFalse((self.session / 'steam-launch').exists())
|
||||
|
||||
def test_desktop_game_url_goes_to_client(self):
|
||||
result = self.launch('desktop', '501590')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines()[-1], 'steam://rungameid/501590')
|
||||
self.assertFalse((self.session / 'steam-launch').exists())
|
||||
|
||||
def test_second_click_forwards_without_another_supervisor(self):
|
||||
lock = self.root / 'cache/droiddeck/steam-desktop.lock'
|
||||
lock.parent.mkdir(parents=True)
|
||||
steam = self.root / 'data/Steam/steamrtarm64/steam'
|
||||
steam.parent.mkdir(parents=True)
|
||||
steam.write_text('#!/bin/bash\nprintf "%s\\n" "$@"\n')
|
||||
steam.chmod(0o755)
|
||||
(self.root / '.steam').mkdir()
|
||||
os.mkfifo(self.root / '.steam/steam.pipe')
|
||||
(self.root / '.steam/steam.pid').write_text(str(os.getpid()))
|
||||
with lock.open('w') as handle:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
result = self.launch('desktop', 'steam://open/settings')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(), ['-nobigpicture', '-no-cef-sandbox', '-cef-ozone-platform=x11',
|
||||
'-cef-disable-gpu', 'steam://open/settings'])
|
||||
self.assertFalse((self.session / 'steam-launch').exists())
|
||||
|
||||
def test_second_click_waits_for_steam_ipc_during_startup(self):
|
||||
lock = self.root / 'cache/droiddeck/steam-desktop.lock'
|
||||
lock.parent.mkdir(parents=True)
|
||||
steam = self.root / 'data/Steam/steamrtarm64/steam'
|
||||
steam.parent.mkdir(parents=True)
|
||||
marker = self.root / 'forwarded'
|
||||
steam.write_text(f'#!/bin/bash\ntouch "{marker}"\n')
|
||||
steam.chmod(0o755)
|
||||
with lock.open('w') as handle:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
process = subprocess.Popen(['bash', str(self.launcher), 'desktop'], env=self.env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
try:
|
||||
time.sleep(0.7)
|
||||
self.assertIsNone(process.poll())
|
||||
self.assertFalse(marker.exists())
|
||||
(self.root / '.steam').mkdir()
|
||||
(self.root / '.steam/steam.pid').write_text(str(os.getpid()))
|
||||
os.mkfifo(self.root / '.steam/steam.pipe')
|
||||
_, stderr = process.communicate(timeout=3)
|
||||
self.assertEqual(process.returncode, 0, stderr)
|
||||
self.assertTrue(marker.exists())
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.communicate()
|
||||
|
||||
def test_big_picture_requests_explicit_handoff(self):
|
||||
result = self.launch('bigpicture')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual((self.session / 'steam-launch').read_text(), 'ui=bigpicture\nurl=\n')
|
||||
|
||||
def test_outside_desktop_preserves_session_request(self):
|
||||
self.env.pop('BL_DESKTOP')
|
||||
result = self.launch('desktop', 'steam://open/settings')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual((self.session / 'steam-launch').read_text(),
|
||||
'ui=desktop\nurl=steam://open/settings\n')
|
||||
|
||||
def test_invalid_target_cannot_trigger_a_handoff(self):
|
||||
self.assertEqual(self.launch('desktop', 'bad').returncode, 64)
|
||||
self.assertFalse((self.session / 'steam-launch').exists())
|
||||
|
||||
def test_cef_software_is_scoped_to_the_kde_client(self):
|
||||
source = (BIN / 'droiddeck-session').read_text()
|
||||
setup = source[source.index(' _bl_ui=(-gamepadui)'):source.index(' # Deck mode, always')]
|
||||
for desktop, ui, software in [('1', 'desktop', True), ('1', 'bigpicture', False), ('', 'desktop', False)]:
|
||||
with self.subTest(desktop=desktop, ui=ui):
|
||||
env = dict(self.env, BL_DESKTOP=desktop, BL_STEAM_UI=ui)
|
||||
result = subprocess.check_output(['bash', '-c', setup + '\nprintf "%s\\n" "${_bl_cef[@]}"'],
|
||||
env=env, text=True)
|
||||
self.assertEqual('-cef-disable-gpu' in result, software)
|
||||
self.assertEqual('-cef-use-angle=vulkan' in result, not software)
|
||||
Reference in new issue
Block a user