diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt b/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt index e255d314b..d024d8614 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt @@ -219,7 +219,20 @@ object Library { "armsx2" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath) // DuckStation shares PCSX2's flags (src/duckstation-qt/qthost.cpp): the same launch and exit. "duckstation" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath) - "dolphin" -> listOf("-e", guestPath) + // Dolphin: full screen, drawn inside its own main window, without its "stop the emulation?" + // question; no warning boxes either, which wait for a click a controller cannot give + // (they still go to Dolphin's log). The guide button is Dolphin's Toggle Fullscreen hotkey + // (bannerlator-pad-defaults), which shows Dolphin's window and its settings - so no -b, which + // hides that window; closing Dolphin ends the session. Under gamescope Dolphin does not + // always see its window as focused, and by default both its hotkeys and the game's + // controller then stop: HotkeysRequireFocus off, BackgroundInput on. -C sets a Dolphin.ini + // value for this run only, so Dolphin started from the desktop keeps its own settings. + "dolphin" -> listOf( + "-C", "Dolphin.Display.Fullscreen=True", "-C", "Dolphin.Display.RenderToMain=True", + "-C", "Dolphin.Interface.ConfirmStop=False", "-C", "Dolphin.Interface.UsePanicHandlers=False", + "-C", "Dolphin.General.HotkeysRequireFocus=False", "-C", "Dolphin.Input.BackgroundInput=True", + "-e", guestPath, + ) "cemu" -> listOf("-g", guestPath) else -> listOf(guestPath) } diff --git a/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt b/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt index 38f28c1f8..48e4642a6 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt @@ -16,6 +16,10 @@ import kotlin.math.abs * - two fingers up / down ............ scroll wheel * - hold still, then drag ............ left-button drag * - double-tap, hold, drag ........... left-button drag as well + * - one finger held, another slides .. left-button drag, as on a laptop's pad: the held finger is + * the button, the other moves the pointer (a window by its + * title bar or edge, a desktop icon). The sliding finger may + * lift and slide again; lifting the held one drops. * * Positions handed to the listener are the pointer's, in the view's pixels; the caller keeps them * inside the picture. @@ -45,6 +49,16 @@ class TouchpadGestures( private var twoFingers = false private var scrollAnchorY = 0f private var scrolled = false + // Hold-and-slide: the first finger stays as the button, the second moves the pointer. + private var anchorId = -1 + private var anchorStartX = 0f + private var anchorStartY = 0f + private var moverId = -1 + private var moverStartX = 0f + private var moverStartY = 0f + private var moverLastX = 0f + private var moverLastY = 0f + private var holdDrag = false private val longPress = Runnable { held = true dragging = true @@ -63,6 +77,7 @@ class TouchpadGestures( lastX = event.x; lastY = event.y downAt = event.eventTime travelled = 0f; held = false; twoFingers = false; scrolled = false; secondWasTap = false + anchorId = event.getPointerId(0); moverId = -1; holdDrag = false // A tap shortly before this touch: holding now drags (double-tap-and-hold). if (event.eventTime - lastTapAt < DOUBLE_TAP_MS) { dragging = true @@ -75,6 +90,17 @@ class TouchpadGestures( } MotionEvent.ACTION_POINTER_DOWN -> { handler.removeCallbacks(longPress) + val i = event.actionIndex + if (moverId == -1 && event.getPointerId(i) != anchorId) { + moverId = event.getPointerId(i) + moverStartX = event.getX(i); moverStartY = event.getY(i) + moverLastX = moverStartX; moverLastY = moverStartY + event.findPointerIndex(anchorId).takeIf { it >= 0 }?.let { + anchorStartX = event.getX(it); anchorStartY = event.getY(it) + } + } + // Mid-drag, a finger coming back only picks the sliding up again. + if (holdDrag) return true twoFingers = true secondDownAt = event.eventTime secondWasTap = true @@ -82,6 +108,38 @@ class TouchpadGestures( return true } MotionEvent.ACTION_MOVE -> { + val ai = event.findPointerIndex(anchorId) + val mi = if (moverId == -1) -1 else event.findPointerIndex(moverId) + if (ai >= 0 && mi >= 0) { + val mx = event.getX(mi); val my = event.getY(mi) + // One finger still, the other on the move: that is a drag, not a scroll (which + // moves both). Decided once, before anything has scrolled. + if (!holdDrag && !scrolled) { + val anchorMoved = abs(event.getX(ai) - anchorStartX) + abs(event.getY(ai) - anchorStartY) + val moverMoved = abs(mx - moverStartX) + abs(my - moverStartY) + if (moverMoved > slopPx && anchorMoved < slopPx) { + holdDrag = true + secondWasTap = false + if (!dragging) { + dragging = true + listener.onButton(PointerGestures.BTN_LEFT, true, x, y) + } + } + } + if (holdDrag) { + val dx = mx - moverLastX + val dy = my - moverLastY + moverLastX = mx; moverLastY = my + val speed = abs(dx) + abs(dy) + val gain = sensitivity * (1f + minOf(speed / 40f, 1.5f)) + place(x + dx * gain, y + dy * gain) + listener.onMove(x, y) + return true + } + } else if (holdDrag) { + // Only the held finger is down: the drag waits for the next slide. + return true + } if (twoFingers && event.pointerCount >= 2) { val mid = (event.getY(0) + event.getY(1)) / 2f val dy = mid - scrollAnchorY @@ -109,6 +167,30 @@ class TouchpadGestures( return true } MotionEvent.ACTION_POINTER_UP -> { + val gone = event.getPointerId(event.actionIndex) + if (holdDrag) { + if (gone == anchorId) { + // The held finger lifted: drop. The other finger carries on as the pointer. + listener.onButton(PointerGestures.BTN_LEFT, false, x, y) + dragging = false; holdDrag = false; twoFingers = false + anchorId = moverId; moverId = -1 + event.findPointerIndex(anchorId).takeIf { it >= 0 }?.let { + lastX = event.getX(it); lastY = event.getY(it) + } + travelled = slopPx + 1f + } else if (gone == moverId) { + moverId = -1 + } + return true + } + if (gone == moverId) moverId = -1 + if (gone == anchorId) { + // The first finger left a two-finger gesture: the other one is the pointer now. + anchorId = event.getPointerId(if (event.actionIndex == 0) 1 else 0) + event.findPointerIndex(anchorId).takeIf { it >= 0 }?.let { + lastX = event.getX(it); lastY = event.getY(it) + } + } // The second finger lifting quickly, with nothing scrolled: a right click. if (twoFingers && secondWasTap && event.eventTime - secondDownAt < TAP_MS && !scrolled) { listener.onButton(PointerGestures.BTN_RIGHT, true, x, y) @@ -136,6 +218,7 @@ class TouchpadGestures( } } dragging = false; held = false; twoFingers = false + holdDrag = false; anchorId = -1; moverId = -1 return true } } 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 5fa6f3c5d..3c86c4e00 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -170,7 +170,8 @@ class SessionService : Service() { SessionState.steamUrl = intent?.getStringExtra(EXTRA_STEAM_URL) // Another Steam client on the device signs ours out seconds after every login; the one that // does it here runs from boot without being opened. Only the Steam session signs in. - if (SessionState.mode == MODE_STEAM) RivalClients.stopBeforeSession(this) + // The desktop too: its Steam launchers run the client right there (bannerlator-steam-launch). + if (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) RivalClients.stopBeforeSession(this) SessionState.running = true SessionState.firstFrameSeen = false // This session's number, claimed here and not when its process starts: the session it @@ -303,7 +304,10 @@ class SessionService : Service() { if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true") if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1") if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0")) - if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this)) + // The desktop's Steam launchers start the client there (bannerlator-steam-launch), through the + // same set-up as a Steam session: it gets what the client and its games are started with. + val steamHere = SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP + if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this)) if (SessionState.mode == MODE_STEAM) { guest.add("BL_GAMESCOPE_FORCE_FULLSCREEN=" + (if (SessionPrefs.forceFullscreen(this)) "1" else "0")) SessionPrefs.writeForceFullscreenFlag(this) @@ -315,7 +319,7 @@ class SessionService : Service() { // The FEXCore preset for the x86 games the client launches: its FEX_* variables go in // here, before the script, so every game process inherits them from the client. The // default preset sets nothing, which is what every session ran on before. - if (SessionState.mode == MODE_STEAM) { + if (steamHere) { val preset = SessionPrefs.fexPreset(this) val vars = com.droiddeck.launcher.core.FexPreset.env(preset) vars.forEach { guest.add(it) } @@ -330,7 +334,7 @@ class SessionService : Service() { // on, even naming every core: it exists to undo the pin Steam applies to its own interface // renderer, and the scheduler's default is exactly what that pin takes away. A game's is // sent only when it is a real restriction - a game has no pin of its own to undo. - if (SessionState.mode == MODE_STEAM && SessionPrefs.clientCpusOverride(this)) { + if (steamHere && SessionPrefs.clientCpusOverride(this)) { guest.add("BL_CLIENT_CPUS=" + CpuCores.listOrAll(SessionPrefs.clientCpus(this))) } // The game cores also pin the desktop and a program from the rail; without a choice the @@ -432,7 +436,7 @@ class SessionService : Service() { } // The user's own games, for the runtime's shortcuts writer to put in the client's library // before the client starts (see frontend/AddedGames and bannerlator-steam-shortcuts). - if (SessionState.mode == MODE_STEAM) { + if (steamHere) { val added = com.droiddeck.launcher.frontend.AddedGames.scan(this) val listing = com.droiddeck.launcher.frontend.AddedGames.writeListing(this, added) guest.add("BL_ADDED_GAMES=" + listing.path) @@ -567,7 +571,7 @@ class SessionService : Service() { // Whether the client signs in to Valve or starts offline: read once, while it starts, and // rewritten by the client when it exits, so it is set again here at every session start. - if (SessionState.mode == MODE_STEAM) OfflineMode.apply(this, root) + if (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) OfflineMode.apply(this, root) val networkLink = LinuxNetworkLinkComponent(this, root) networkLink.attach(this) diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-pad-defaults b/tools/linuxfs/overlay/usr/local/bin/bannerlator-pad-defaults index 88e56d76d..954916d29 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-pad-defaults +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-pad-defaults @@ -383,7 +383,7 @@ Buttons/1 = `Button W` Buttons/2 = `Button N` Buttons/- = `Back` Buttons/+ = `Start` -Buttons/Home = `Guide` +Buttons/Home = `Shoulder R` D-Pad/Up = `Pad N` D-Pad/Down = `Pad S` D-Pad/Left = `Pad W` @@ -510,10 +510,188 @@ def armsx2_bios(ini): print(f"bannerlator-pad-defaults: ARMSX2: BIOS {pick}") +# Dolphin (PC) has no controller menu, so the guide button is its Toggle Fullscreen hotkey: out of full +# screen is Dolphin's own window with its Config / Graphics / Stop buttons. Hotkeys.ini is all-or- +# nothing - a file with one line drops every other default - so a new one carries Dolphin's defaults, +# generated from HotkeyManager.cpp's LoadDefaults (Linux branch, RetroAchievements on) plus the guide; +# an existing one only gets the guide added to its Toggle Fullscreen. The Wii remote's Home, which +# was on the guide button, is on the right shoulder button instead. +DOLPHIN_GUIDE = "`SDL/0/Xbox 360 Controller:Guide`" +DOLPHIN_HOTKEYS = """[Hotkeys] +Device = XInput2/0/Virtual core pointer +General/Open = @(Ctrl+O) +General/Toggle Pause = F10 +General/Stop = Escape +General/Toggle Fullscreen = @(Alt+Return) | `SDL/0/Xbox 360 Controller:Guide` +General/Take Screenshot = F9 +General/Open Achievements = @(Alt+A) +Emulation Speed/Disable Emulation Speed Limit = Tab +Stepping/Step Into = F11 +Stepping/Step Over = @(Shift+F10) +Stepping/Step Out = @(Shift+F11) +Breakpoint/Toggle Breakpoint = @(Shift+F9) +Wii/Connect Wii Remote 1 = @(Alt+F5) +Wii/Connect Wii Remote 2 = @(Alt+F6) +Wii/Connect Wii Remote 3 = @(Alt+F7) +Wii/Connect Wii Remote 4 = @(Alt+F8) +Wii/Connect Balance Board = @(Alt+F9) +Load State/Load State Slot 1 = F1 +Load State/Load State Slot 2 = F2 +Load State/Load State Slot 3 = F3 +Load State/Load State Slot 4 = F4 +Load State/Load State Slot 5 = F5 +Load State/Load State Slot 6 = F6 +Load State/Load State Slot 7 = F7 +Load State/Load State Slot 8 = F8 +Save State/Save State Slot 1 = @(Shift+F1) +Save State/Save State Slot 2 = @(Shift+F2) +Save State/Save State Slot 3 = @(Shift+F3) +Save State/Save State Slot 4 = @(Shift+F4) +Save State/Save State Slot 5 = @(Shift+F5) +Save State/Save State Slot 6 = @(Shift+F6) +Save State/Save State Slot 7 = @(Shift+F7) +Save State/Save State Slot 8 = @(Shift+F8) +Other State Hotkeys/Undo Load State = F12 +Other State Hotkeys/Undo Save State = @(Shift+F12) +GBA Core/Load ROM = @(`Ctrl`+`Shift`+`O`) +GBA Core/Unload ROM = @(`Ctrl`+`Shift`+`W`) +GBA Core/Reset = @(`Ctrl`+`Shift`+`R`) +GBA Volume/Volume Down = `KP_Subtract` +GBA Volume/Volume Up = `KP_Add` +GBA Volume/Volume Toggle Mute = `M` +GBA Window Size/1x = `KP_1` +GBA Window Size/2x = `KP_2` +GBA Window Size/3x = `KP_3` +GBA Window Size/4x = `KP_4` +USB Emulation Devices/Show Skylanders Portal = @(Ctrl+P) +USB Emulation Devices/Show Infinity Base = @(Ctrl+I) +""" + +DOLPHIN_APPIMAGE = "/opt/appimages/dolphin.AppImage" +DOLPHIN_DATA = "/usr/share/dolphin-emu" + + +def dolphin_data(): + """The Dolphin build in the catalog finds its own data - the GameCube fonts, the per-game fixes + (GameSettings: GUT.ini puts Ultimate Spider-Man on the MMU), its icons and shaders - only at the + fixed /usr/share/dolphin-emu it was built for; there is no setting or variable to move it. Here it + runs from a copy unpacked under /tmp (no FUSE), so it found none of it: a GameCube game stopped + on "Windows-1252 fonts are not loaded", another on an invalid write the missing game fix would + have prevented. So the folder is unpacked from the AppImage to that path, once per AppImage.""" + import shutil, subprocess, tempfile + try: + st = os.stat(DOLPHIN_APPIMAGE) + except OSError: + return + stamp = "%d:%d" % (st.st_size, int(st.st_mtime)) + marker = os.path.join(DOLPHIN_DATA, ".droiddeck-from") + try: + if open(marker).read() == stamp and os.path.isfile(os.path.join(DOLPHIN_DATA, "Sys/GC/font_western.bin")): + return + except OSError: + pass + work = tempfile.mkdtemp(prefix="dolphin-data-", dir="/tmp") + try: + subprocess.run([DOLPHIN_APPIMAGE, "--appimage-extract", "usr/share/dolphin-emu/*"], cwd=work, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600) + src = os.path.join(work, "squashfs-root/usr/share/dolphin-emu") + if not os.path.isfile(os.path.join(src, "Sys/GC/font_western.bin")): + print("bannerlator-pad-defaults: Dolphin: could not unpack its data from the AppImage") + return + new = DOLPHIN_DATA + ".new" + shutil.rmtree(new, ignore_errors=True) + shutil.move(src, new) + with open(os.path.join(new, ".droiddeck-from"), "w") as f: + f.write(stamp) + shutil.rmtree(DOLPHIN_DATA, ignore_errors=True) + os.rename(new, DOLPHIN_DATA) + print("bannerlator-pad-defaults: Dolphin: its data (fonts, per-game fixes) in " + DOLPHIN_DATA) + except (OSError, subprocess.SubprocessError) as e: + print("bannerlator-pad-defaults: Dolphin: unpacking its data failed: %s" % e) + finally: + shutil.rmtree(work, ignore_errors=True) + + def dolphin(): + if not os.path.isfile(DOLPHIN_APPIMAGE): + return + dolphin_data() d = os.path.join(CONFIG, "dolphin-emu") seed(os.path.join(d, "GCPadNew.ini"), DOLPHIN_GC, "Dolphin (GameCube)") seed(os.path.join(d, "WiimoteNew.ini"), DOLPHIN_WII, "Dolphin (Wii)") + wii_pad = os.path.join(d, "WiimoteNew.ini") + if ini_get(wii_pad, "Wiimote1", "Buttons/Home") == "`Guide`": + ini_set(wii_pad, "Wiimote1", "Buttons/Home", "`Shoulder R`") + print("bannerlator-pad-defaults: Dolphin (Wii): Home on the right shoulder button (guide = Dolphin's window)") + hotkeys = os.path.join(d, "Hotkeys.ini") + if not os.path.isfile(hotkeys): + seed(hotkeys, DOLPHIN_HOTKEYS, "Dolphin", "the guide button shows Dolphin's window (Toggle Fullscreen)") + else: + current = ini_get(hotkeys, "Hotkeys", "General/Toggle Fullscreen") + if current is not None and DOLPHIN_GUIDE not in current: + ini_set(hotkeys, "Hotkeys", "General/Toggle Fullscreen", + (current + " | " if current else "") + DOLPHIN_GUIDE) + print("bannerlator-pad-defaults: Dolphin: the guide button shows Dolphin's window (Toggle Fullscreen)") + # Games from the ROMs folder's gamecube and wii folders, subfolders included; Vulkan; no + # NKit warning (it will not let a game start until a box is ticked - games run either way, + # the warning is about loading times, netplay and save states); no statistics question. + # Names from Dolphin's MainSettings.cpp / UISettings.cpp. Only what is not set yet is added, + # except the game folders, which are added to whatever the user has. + ini = os.path.join(d, "Dolphin.ini") + gc = system_folder(("gamecube", "gc", "ngc"), "gamecube") + wii = system_folder(("wii",), "wii") + if not os.path.isfile(ini): + os.makedirs(d, exist_ok=True) + with open(ini, "w", encoding="utf-8") as f: + f.write("[Analytics]\nEnabled = False\nPermissionAsked = True\n" + "[Core]\nGFXBackend = Vulkan\n" + "[General]\nISOPaths = 2\nISOPath0 = %s\nISOPath1 = %s\nRecursiveISOPaths = True\n" + "[Interface]\nSkipNKitWarning = True\n" % (gc, wii)) + print("bannerlator-pad-defaults: Dolphin: first-time setup (Vulkan, games from %s and %s)" % (gc, wii)) + return + try: + count = int(ini_get(ini, "General", "ISOPaths") or "0") + except ValueError: + count = 0 + paths = [ini_get(ini, "General", "ISOPath%d" % i) for i in range(count)] + changed = [] + # The whole ROMs folder as a game folder (what Dolphin's "set a games directory" was pointed at + # before) would, searched with its subfolders, have Dolphin open every PS2 and PS3 image too; + # the system folders below cover it. + if any((p or "").rstrip("/") == ROMS for p in paths): + paths = [p for p in paths if p and p.rstrip("/") != ROMS] + for i, p in enumerate(paths): + ini_set(ini, "General", "ISOPath%d" % i, p) + for i in range(len(paths), count): + ini_set(ini, "General", "ISOPath%d" % i, "") + count = len(paths) + ini_set(ini, "General", "ISOPaths", str(count)) + changed.append("not the whole ROMs folder") + for folder in (gc, wii): + if folder not in paths: + ini_set(ini, "General", "ISOPath%d" % count, folder) + count += 1 + paths.append(folder) + changed.append(folder) + if changed: + ini_set(ini, "General", "ISOPaths", str(count)) + if ini_get(ini, "General", "RecursiveISOPaths") != "True": + ini_set(ini, "General", "RecursiveISOPaths", "True") + changed.append("subfolders") + if changed: + print("bannerlator-pad-defaults: Dolphin: games from " + ", ".join(changed)) + # Once, for a Dolphin set up before this: Vulkan unless it was chosen already, and the NKit box. + marker = os.path.join(d, ".droiddeck-setup") + if os.path.exists(marker): + return + if not ini_get(ini, "Core", "GFXBackend"): + ini_set(ini, "Core", "GFXBackend", "Vulkan") + ini_set(ini, "Interface", "SkipNKitWarning", "True") + if ini_get(ini, "Analytics", "PermissionAsked") != "True": + ini_set(ini, "Analytics", "Enabled", "False") + ini_set(ini, "Analytics", "PermissionAsked", "True") + open(marker, "w").close() + print("bannerlator-pad-defaults: Dolphin: Vulkan, no NKit warning") # Cemu: an SDLController profile for the GamePad. The uuid is "_" and SDL's GUID diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session index bdcc653c9..c54bcecd3 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session @@ -350,7 +350,8 @@ case $mode in # The drawer's "Stretch games to fill the screen" works live: the app writes its choice to # ~/.droiddeck-fill, and each change goes to gamescope as GAMESCOPE_FORCE_WINDOWS_FULLSCREEN on # the root window, which it watches. What gamescope was started with is where this begins. - ( + # Not on the desktop (BL_DESKTOP): no gamescope there, and the loop would outlive the client. + [ -z "${BL_DESKTOP:-}" ] && ( last=${BL_GAMESCOPE_FORCE_FULLSCREEN:-1} while :; do sleep 1 @@ -515,7 +516,17 @@ BUSCONF # The URL arguments were acted on the first time; a restart must not repeat them. set -- done - reap_orphans + if [ -n "${BL_DESKTOP:-}" ]; then + # Started from the desktop (bannerlator-steam-launch): the desktop's own programs share this + # script's parent, so reap_orphans - right under gamescope, where everything is the client's - + # is not for here. The helpers this run started (the cores and redistributables beats, the + # system bus and the NetworkManager stand-in) go with the client instead. + _bl_jobs=$(jobs -p) + [ -n "$_bl_jobs" ] && kill $_bl_jobs 2>/dev/null + echo "== steam: the client on the desktop has exited" + else + reap_orphans + fi exit "$rc" ;; *) diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-launch b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-launch index 1b2afba0a..92d1c9d22 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-launch +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-launch @@ -1,12 +1,18 @@ #!/bin/bash -# Asks the app to start the Steam client, from inside a session (the desktop's menu entries and -# the per-game launchers bannerlator-desktop-games writes): +# Starts the Steam client from inside a session (the desktop's menu entries and the per-game +# launchers bannerlator-desktop-games writes): # bannerlator-steam-launch desktop [appid|steam-url] Steam's desktop UI, optionally a game # bannerlator-steam-launch bigpicture [appid|steam-url] Big Picture -# The client cannot run on the desktop: labwc composites in software and offers no dma-buf, so a -# game (or the client's own Vulkan window) would die at its first frame. It runs under gamescope, -# which is a session of its own - so this writes a request into the directory the app watches, -# and the app ends the desktop session and starts a Steam session in its place. +# +# On a desktop that draws with the GPU (labwc on vulkan or gles2 - the default since the GPU +# desktop) the client runs right there: a window on the desktop, through the desktop's Xwayland, +# with the same set-up a Steam session gives it (bannerlator-session steam, minus gamescope), and +# its games open in windows of their own. A client already running there gets the request instead +# of a second one being started. +# +# On a software desktop (pixman: the GPU renderers failed on this device) there is no dma-buf, so a +# game - or the client's own Vulkan window - would die at its first frame. There the request goes +# to the app, which ends the desktop and starts a Steam session under gamescope in its place. set -u ui=${1:-desktop} target=${2:-} @@ -18,6 +24,30 @@ case $target in *[!0-9]*) echo "not an app id or steam:// URL: $target" >&2; exit 64 ;; *) url=steam://rungameid/$target ;; esac + +steam_root=${XDG_DATA_HOME:-${HOME:-/root}/.local/share}/Steam +client=$steam_root/steamrtarm64/steam + +if [ "${BL_DESKTOP:-}" = 1 ] && [ -n "${WLR_RENDERER:-}" ] && [ "$WLR_RENDERER" != pixman ]; then + log=${BL_DEBUG_DIR:-$HOME/.cache/droiddeck}/steam-desktop.log + mkdir -p "$(dirname "$log")" 2>/dev/null + if pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1 && [ -x "$client" ]; then + # One client at a time: the running one takes the request (a second start hands its command + # line over and exits). No game asked for = switch it to the UI that was clicked. + [ -n "$url" ] || { [ "$ui" = bigpicture ] && url=steam://open/bigpicture || url=steam://open/main; } + echo "bannerlator-steam-launch: the client is running on the desktop; handing it $url" | tee -a "$log" + exec "$client" "$url" + fi + echo "bannerlator-steam-launch: starting the Steam client on the desktop ($ui${url:+, $url}); log: $log" + # BL_INSIDE: the session script's Steam start-up without gamescope in front of it. The desktop UI + # unless Big Picture was asked for; never Deck mode (its Quick Access Menu needs gamescope). + export BL_INSIDE=1 BL_STEAMDECK=0 + if [ "$ui" = desktop ]; then export BL_STEAM_UI=desktop; else unset BL_STEAM_UI; fi + setsid /usr/local/bin/bannerlator-session steam ${url:+"$url"} >>"$log" 2>&1 < /dev/null & + exit 0 +fi + +# The software desktop, or outside the desktop: ask the app for a Steam session. dir=${BL_LAUNCH_DIR:-} if [ -z "$dir" ] || [ ! -d "$dir" ]; then echo "bannerlator-steam-launch: not inside a DroidDeck session (BL_LAUNCH_DIR unset)" >&2 diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-shim b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-shim index 66d1ee085..a30c2c8d8 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-shim +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-steam-shim @@ -1,12 +1,19 @@ #!/bin/bash # `steam` on the desktop. The client writes a menu entry per installed game that runs -# `steam steam://rungameid/`, and its own launchers say `steam`; neither can run the client -# here (no dma-buf on the desktop), so on the desktop this hands off to the app for a Steam session -# in the client's desktop UI under gamescope. Anywhere else it is the client itself. +# `steam steam://rungameid/`, and its own launchers say `steam`. On the desktop that goes +# through bannerlator-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. set -u if [ "${BL_DESKTOP:-}" = 1 ]; then url= - for arg in "$@"; do case $arg in steam://*) url=$arg ;; esac; done - exec /usr/local/bin/bannerlator-steam-launch desktop ${url:+"$url"} + ui=desktop + for arg in "$@"; do + case $arg in + steam://*) url=$arg ;; + -gamepadui|-bigpicture) ui=bigpicture ;; + esac + done + exec /usr/local/bin/bannerlator-steam-launch "$ui" ${url:+"$url"} fi exec "${HOME:-/root}/.local/share/Steam/steamrtarm64/steam" "$@"