Files
DroidDeck/tools/linuxfs/overlay/usr/local/bin/bannerlator-session
T
The412Banner 921f156128 Desktop programs: controller seen by every emulator, Player 1 profiles, big-core pinning
- The pad keeps Steam Input's virtual-gamepad identity only in Steam sessions.
  SDL ignores a Steam virtual gamepad unless run under Steam, so on the desktop
  and from the rail every SDL emulator saw no controller at all; there it is a
  plain Xbox 360 pad now, which SDL and evdev map by themselves.
- bannerlator-pad-defaults writes Player 1 on the controller for emulators that
  have no profile yet (RPCS3 first: without one it takes the keyboard). Run at
  desktop start and before a program from the rail; never overwrites.
- The desktop and everything started from it, and a program from the rail, run
  on the game cores when chosen, else every core but the slowest cluster
  (program_cores); BL_PIN_CORES=0 turns it off. Game cores reach every mode.
2026-09-23 22:06:47 -04:00

510 lines
28 KiB
Bash
Executable File

#!/bin/bash
# The Linux runtime's session. Bannerlator execs this under proot with the output size and mode;
# the first pass starts gamescope, which execs this script again inside it for the program:
# bannerlator-session desktop the file manager
# bannerlator-session run <path> a Linux program (AppImage, script or ELF)
# bannerlator-session steam [steam-url] the native Steam client
# bannerlator-session lxqt the LXQt/labwc desktop (DroidDeck's desktop package)
# BL_WIDTH / BL_HEIGHT size the output, BL_FPS limits it, BL_LOG collects everything said here.
set -u
export HOME=${HOME:-/root}
mode=${1:-desktop}
shift || true
# One line per milestone, greppable as "== STEP", so a first run reads as a narrative rather than
# something to be reconstructed from Steam's own chatter. The app shows these while it waits.
step() { echo "== STEP $(date -u +%H:%M:%S) $*"; }
if [ -n "${BL_LOG:-}" ] && [ -z "${BL_INSIDE:-}" ]; then
mkdir -p "$(dirname "$BL_LOG")" 2>/dev/null
# The client alone writes megabytes an hour; one earlier log is kept, not every session's.
if [ "$(stat -c %s "$BL_LOG" 2>/dev/null || echo 0)" -gt 8388608 ]; then
mv -f "$BL_LOG" "$BL_LOG.1"
fi
exec >>"$BL_LOG" 2>&1
echo "== bannerlator-session $(date) mode=$mode $*"
echo "== uid=$(id -u) size=${BL_WIDTH:-?}x${BL_HEIGHT:-?} fps=${BL_FPS:-0} icd=${VK_ICD_FILENAMES:-none}"
echo "== gamescope: $(gamescope --version 2>&1 | head -1) at $(command -v gamescope)"
echo "== rootfs: $(cat /.version 2>/dev/null || echo unversioned)"
fi
# Until the runtime is stable, Steam's own logs are copied beside the session log at exit so a
# failed launch can be handed over as one folder.
collect_steam_logs() {
[ -n "${BL_DEBUG_DIR:-}" ] || return 0
local steam_root=${XDG_DATA_HOME:-$HOME/.local/share}/Steam
mkdir -p "$BL_DEBUG_DIR/steam" 2>/dev/null || return 0
# Into steam/ beside this session's own logs. The app scrubs these line by line after the
# session ends - they carry the account's session token and id - so nothing here is the last
# word on what the user ends up sharing; this is just the copy that survives the runtime.
[ -d "$steam_root/logs" ] && cp -r "$steam_root/logs/." "$BL_DEBUG_DIR/steam/" 2>/dev/null
[ -f "$HOME/.steam/steam/logs/steamwebhelper.log" ] && cp "$HOME/.steam/steam/logs/steamwebhelper.log" "$BL_DEBUG_DIR/steam/" 2>/dev/null
# NOT copied, ever: loginusers.vdf, config.vdf and the ssfn* files, which hold the credentials
# themselves rather than a mention of them.
echo "== steam logs copied to $BL_DEBUG_DIR/steam"
}
# A session that is killed never reaches the copy below, and those are the sessions worth reading.
trap 'collect_steam_logs 2>/dev/null || true' TERM INT HUP
# The driver everything in this session draws with: gamescope's compositing, the client's UI
# (OpenGL through Zink), every game Proton launches, and in desktop mode every program on it.
# BL_VK_DRIVER names an imported driver's ICD manifest on the Android side, which is bound into the
# session at its own path; unset means the runtime keeps the Turnip it was built with.
# VK_DRIVER_FILES REPLACES the loader's search, so a manifest whose library is unreadable would
# leave the session with no Vulkan at all - check both before setting it, and say so in the log
# rather than fail silently half a minute later.
if [ -n "${BL_VK_DRIVER:-}" ]; then
bl_icd_lib=$(sed -n 's/.*"library_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$BL_VK_DRIVER" 2>/dev/null | head -1)
# JSON may escape the separator: org.json writes "\/data\/user\/0\/...". The Vulkan loader
# parses that correctly; this check has to as well, or an absolute path looks relative and gets
# glued onto the manifest's directory - which is exactly what happened the first time.
bl_icd_lib=$(printf '%s' "$bl_icd_lib" | sed 's|\\/|/|g')
case $bl_icd_lib in
/*) ;;
?*) bl_icd_lib="$(dirname "$BL_VK_DRIVER")/$bl_icd_lib" ;;
esac
if [ -r "$BL_VK_DRIVER" ] && [ -n "$bl_icd_lib" ] && [ -r "$bl_icd_lib" ]; then
export VK_DRIVER_FILES="$BL_VK_DRIVER"
# Loaders older than 1.3.207 only know the old name; both are harmless together.
export VK_ICD_FILENAMES="$BL_VK_DRIVER"
echo "== vulkan driver: imported $bl_icd_lib (via $BL_VK_DRIVER)"
else
echo "== vulkan driver: IGNORING $BL_VK_DRIVER (manifest or ${bl_icd_lib:-library} unreadable); using the runtime's own"
fi
unset bl_icd_lib
fi
# The app's gamescope build (tools/gamescope) takes realtime-priority Vulkan queues on this
# 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.
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-1}
export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache}
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"
# The cores a program runs on outside Steam (the desktop and everything started from it, a
# program from the rail): the user's game cores when set (BL_GAME_CPUS), else every core but the
# slowest cluster - Android is free to park an emulator's threads on the little cores, and those
# are what compile shaders and recompile PPU/SPU code. BL_PIN_CORES=0 leaves the scheduler alone.
program_cores() {
[ "${BL_PIN_CORES:-1}" = 0 ] && return 0
command -v taskset >/dev/null 2>&1 || return 0
if [ -n "${BL_GAME_CPUS:-}" ]; then echo "$BL_GAME_CPUS"; return 0; fi
local f n max min= list=
for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do
max=$(cat "$f" 2>/dev/null) || continue
[ -z "$min" ] || [ "$max" -lt "$min" ] && min=$max
done
[ -n "$min" ] || return 0
for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do
max=$(cat "$f" 2>/dev/null) || continue
n=${f#/sys/devices/system/cpu/cpu}; n=${n%%/*}
[ "$max" -gt "$min" ] && list="$list${list:+,}$n"
done
echo "$list"
}
if [ -z "${BL_INSIDE:-}" ]; then
# The compositor may still be starting, and a socket left by an earlier session is not
# a compositor: probe until a connection succeeds.
for _ in $(seq 1 100); do
wayland-info >/dev/null 2>&1 && break
sleep 0.1
done
# The desktop (DroidDeck's hosted package) is its own nested compositor, labwc, drawing straight
# into the app's: no gamescope in between.
if [ "$mode" = lxqt ]; then
step "starting the desktop"
[ -x /usr/local/bin/droiddeck-desktop ] || { echo "== the desktop package is not installed"; exit 66; }
cores=$(program_cores)
if [ -n "$cores" ]; then
echo "== desktop cores: $cores (the desktop and every program started from it)"
exec taskset -c "$cores" /usr/local/bin/droiddeck-desktop
fi
exec /usr/local/bin/droiddeck-desktop
fi
width=${BL_WIDTH:-1280}
height=${BL_HEIGHT:-720}
args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$width" -h "$height")
# -r is what gamescope advertises as the refresh rate, and what a game reads as the display's.
# Left unset it defaults to 60, so a 120 Hz panel offers only 60 Hz in game settings. The FPS
# limiter wins when it is on (it is a deliberate cap); otherwise use the panel's own rate.
if [ "${BL_FPS:-0}" -gt 0 ]; then
args+=(-r "$BL_FPS")
elif [ "${BL_REFRESH:-0}" -gt 0 ]; then
args+=(-r "$BL_REFRESH")
fi
[ "$mode" = steam ] && args+=(-e)
# A game that resizes its own window when it loses focus (the Steam menu opening over it) comes
# back smaller, and gamescope draws a window that is not flagged fullscreen at its own size in
# the top-left corner. Forcing every window to the output's size keeps it filling the screen.
# BL_GAMESCOPE_FORCE_FULLSCREEN=0 turns it off.
[ "$mode" = steam ] && [ "${BL_GAMESCOPE_FORCE_FULLSCREEN:-1}" = 1 ] && args+=(--force-windows-fullscreen)
# HDR10: the app opened its compositor's colour-management gate for this session (BL_HDR=1);
# gamescope then offers HDR to its clients through the same wp_color_manager_v1 it binds on ours.
[ "${BL_HDR:-0}" = 1 ] && args+=(--hdr-enabled)
# Deck mode's performance overlay (Quick Access Menu → Performance → Overlay Level) is drawn by
# mangoapp, which gamescope starts beside the client; the client sets the level by rewriting
# MANGOHUD_CONFIGFILE. The apk stages Valve's aarch64 build (tools/mangoapp) - without it the menu
# still offers the levels and nothing appears. Starts hidden until the client writes a level.
# BL_MANGOAPP=0 turns it off (one community build found Valve's mangoapp crashing on Turnip).
if [ "$mode" = steam ] && [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_MANGOAPP:-1}" = 1 ] \
&& [ "${BL_STEAM_UI:-}" != desktop ] && command -v mangoapp >/dev/null 2>&1; then
MANGOHUD_CONFIGFILE=$(mktemp /tmp/mangohud.XXXXXXXX)
echo no_display > "$MANGOHUD_CONFIGFILE"
export MANGOHUD_CONFIGFILE STEAM_USE_MANGOAPP=1 STEAM_MANGOAPP_PRESETS_SUPPORTED=1 STEAM_MANGOAPP_HORIZONTAL_SUPPORTED=1
args+=(--mangoapp)
echo "== gamescope: mangoapp on ($(command -v mangoapp), config $MANGOHUD_CONFIGFILE)"
fi
export BL_INSIDE=1
exec gamescope "${args[@]}" -- "$0" "$mode" "$@"
fi
# gamescope manages X windows: toolkits go through Xwayland, not gamescope's own Wayland socket.
export GDK_BACKEND=x11 QT_QPA_PLATFORM=xcb SDL_VIDEODRIVER=x11
# gamescope's reaper waits for every process left under it before the compositor comes down. A
# process a game left behind - a Wine service blocked on a server that is already gone - is
# reparented there when the client exits, and the screen stays black on it for good. Nothing of
# the session outlives the program it was started for.
reap_orphans() {
local round p stat pid left
for round in 1 2 3 4 5; do
left=
for p in /proc/[0-9]*; do
pid=${p#/proc/}
[ "$pid" = "$$" ] && continue
stat=$(cat "$p/stat" 2>/dev/null) || continue
stat=${stat##*) }
set -- $stat
[ "${2:-}" = "$PPID" ] && left="$left $pid"
done
[ -n "$left" ] || return 0
echo "== leaving behind:$left"
kill -9 $left 2>/dev/null
sleep 0.2
done
}
case $mode in
desktop)
exec pcmanfm
;;
run)
target=${1:?program path}
shift
cores=$(program_cores)
if [ -n "$cores" ]; then
echo "== program cores: $cores"
taskset -cp "$cores" $$ >/dev/null 2>&1 || true
fi
# Player 1 on the controller in an emulator that has no profile of its own yet.
python3 /usr/local/bin/bannerlator-pad-defaults 2>&1 || true
case ${target,,} in
*.appimage)
export APPIMAGE_EXTRACT_AND_RUN=1
chmod +x "$target" 2>/dev/null
exec "$target" "$@"
;;
*.sh)
exec bash "$target" "$@"
;;
*)
chmod +x "$target" 2>/dev/null
cd "$(dirname "$target")" && exec "$target" "$@"
;;
esac
;;
steam)
step "checking the Steam client (first run downloads it, ~1-2 min with nothing on screen)"
bannerlator-steam-install || exit $?
step "Steam client ready"
steam_root=$XDG_DATA_HOME/Steam
# Register the card library (bound at /mnt/bannerlator-sd) with the client - internal storage is
# its main library already - and the ARM64 Proton that lets it launch the Windows ones.
# A request left in ~/.bl-proton-extra installs a third-party Proton before the client starts,
# so it is in the Compatibility dropdown the first time the user looks. One per line, each the
# arguments to bannerlator-proton-extra: "ge", "cachyos", "ge --tag GE-Proton11-7", or a URL.
# Hundreds of megabytes each, so it is only ever done on request, and the line is dropped once
# it has succeeded - a failed download stays and is retried next session.
if [ -s "$HOME/.bl-proton-extra" ]; then
_bl_left=$HOME/.bl-proton-extra.left
: > "$_bl_left"
while read -r _bl_extra || [ -n "$_bl_extra" ]; do
case ${_bl_extra# } in ""|"#"*) continue ;; esac
step "installing Proton '$_bl_extra' (large download, several minutes)"
if bannerlator-proton-extra "$steam_root" $_bl_extra; then
step "installed Proton '$_bl_extra'"
else
step "Proton '$_bl_extra' failed; will retry next session"
echo "$_bl_extra" >> "$_bl_left"
fi
done < "$HOME/.bl-proton-extra"
mv -f "$_bl_left" "$HOME/.bl-proton-extra"
fi
# steam:// URLs left in ~/.bl-steam-urls are handed to the client on its command line. The app
# writes one when the user asks for a Proton that is one of Valve's own depots rather than a
# tarball: those are not files anywhere, the client fetches them like any other app. One per
# line, and the file is cleared once they have been passed on - the client either acts on a URL
# or it does not, and repeating it every session would re-ask forever.
if [ -s "$HOME/.bl-steam-urls" ]; then
while read -r _bl_url || [ -n "$_bl_url" ]; do
case ${_bl_url# } in steam://*) ;; *) continue ;; esac
step "asking the client for $_bl_url"
set -- "$@" "$_bl_url"
done < "$HOME/.bl-steam-urls"
rm -f "$HOME/.bl-steam-urls"
fi
# Cores for the client itself. Steam pins its own interface renderer to a subset - on the
# device it took 5 of 8, leaving out both little cores and the fastest one - which is a
# reasonable thing to do while a game is running and the wrong thing when the client is all
# there is. BL_CLIENT_CPUS overrides it. Re-applied on a beat because steamwebhelper spawns
# children continuously and each one inherits Steam's choice, not ours.
if [ -n "${BL_CLIENT_CPUS:-}" ]; then
step "client cores: $BL_CLIENT_CPUS"
(
for _ in $(seq 1 720); do
for _p in $(pgrep -f 'steamwebhelper|steamrtarm64/steam|gamescope' 2>/dev/null); do
taskset -cp "$BL_CLIENT_CPUS" "$_p" >/dev/null 2>&1 || true
done
sleep 5
done
) &
fi
step "registering the app's game library with Steam"
bannerlator-steam-library "$steam_root" || true
step "setting up the ARM64 Proton compatibility tool"
bannerlator-steam-compat "$steam_root" || true
# The user's own games, as non-Steam shortcuts under that tool; the list is written by the app
# each session (empty = the app's earlier entries are removed, the client's own are kept).
if [ -n "${BL_ADDED_GAMES:-}" ]; then
step "adding the app's own games to the client's library"
bannerlator-steam-shortcuts "$steam_root" "$BL_ADDED_GAMES" || true
fi
# Steam runs a game's shared redistributables inside its prefix on first launch; marking
# them as already run keeps those x86 installers from starting under emulation at all.
bannerlator-seed-redists "$steam_root" || true
# The compatibility tool runs Valve's arm64 Proton, which the client fetches like any depot.
# Asked for on the command line the first time, so a Windows title has it before it is needed.
if [ ! -f "$steam_root/steamapps/appmanifest_4427310.acf" ]; then
step "asking the client to fetch the ARM64 Proton"
set -- "$@" steam://install/4427310
# Ported from Bannerlator (bannerlator-session, device-proven 2026-09-22). The client decides
# a title's compatibility tool when the title is installed and keeps that decision for the
# rest of its run, and the tool is set up before the client comes up. So once the depot has
# fully landed, restart the client once - its own -shutdown, then the loop below, which knows
# the marker - and every title installed from then on runs through the tool, with the
# refreshed tool list and defaults, and no second launch by the user. A first run only: with
# the depot present at session start nothing here is armed.
(
_bl_acf="$steam_root/steamapps/appmanifest_4427310.acf"
for _ in $(seq 1 360); do
sleep 10
# StateFlags 4 = fully installed; a moment more for the client to finish its bookkeeping.
if grep -q '"StateFlags"[[:space:]]*"4"' "$_bl_acf" 2>/dev/null; then
sleep 10
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
step "compatibility layer installed: restarting the Steam client once"
: > "$HOME/.bl-restart-once"
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
exit 0
fi
done
) &
# A client that starts signed out drops that request - on a fresh install nobody has signed
# in yet - so the install dialog only appeared after the user restarted the client by hand.
# With no remembered sign-in, ask again once the sign-in has happened: the client binary
# started a second time with a URL hands it to the running instance and exits.
if ! grep -q '"RememberPassword"[[:space:]]*"1"' "$steam_root/config/loginusers.vdf" 2>/dev/null; then
(
_bl_conn="$steam_root/logs/connection_log.txt"
_bl_from=$(stat -c %s "$_bl_conn" 2>/dev/null || echo 0)
for _ in $(seq 1 180); do
sleep 10
[ -f "$steam_root/steamapps/appmanifest_4427310.acf" ] && exit 0
if tail -c +$((_bl_from + 1)) "$_bl_conn" 2>/dev/null | grep -q "RecvMsgClientLogOnResponse() : processing complete"; then
sleep 15
[ -f "$steam_root/steamapps/appmanifest_4427310.acf" ] && exit 0
step "signed in: asking the client again for the ARM64 Proton"
"$steam_root/steamrtarm64/steam" steam://install/4427310 >/dev/null 2>&1 || true
exit 0
fi
done
) &
fi
fi
# The tool is registered before the client starts and finds the depot at launch time, so
# nothing here waits for a download. What can still change under a running client is which
# titles are installed: a title installed this session gets the client's own per-app Proton
# choice, which it cannot start. Refresh the per-app entries every so often; the registrar is
# idempotent, so a repeat costs nothing.
# The seeder has the same problem and a worse consequence. It marks the shared redistributables
# as already run in each prefix that exists, and a game's prefix is not created until the
# client launches it - so a title played for the first time this session was never covered, and
# its install scripts run for real. That is exactly the hang the seeder exists to prevent: Need
# for Speed Payback sat on "Running install script" with vcredist_x86.exe blocked in pipe_read,
# holding both ends of a pipe nobody would write to, while the client waited for it to exit.
# Seeding again on the same beat covers a prefix the moment it appears.
# The seeder runs on a shorter beat than the registrar because it is racing something. The
# client creates a prefix when it launches a title and runs that title's install scripts
# moments later, so the seeder has only that gap to mark the redistributables as already run.
# It stamps each prefix it has covered and skips it thereafter, so the extra passes cost
# almost nothing.
(
beat=0
for _ in $(seq 1 720); do
sleep 5
bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true
beat=$((beat + 1))
if [ "$((beat % 3))" -eq 0 ]; then
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
fi
done
) &
# Debug aid while the runtime is being brought up: an app id dropped in ~/.bl-autolaunch is
# handed to the client as a rungameid URL, so a launch can be reproduced without touching the
# screen. Removed once the runtime is stable.
if [ -s "$HOME/.bl-autolaunch" ]; then
_bl_app=$(cat "$HOME/.bl-autolaunch")
# A bare app id runs the game; a full steam:// URL is passed through, which is how a
# validate or an install is driven without touching the screen.
case $_bl_app in
steam://*) _bl_url=$_bl_app ;;
*) _bl_url=steam://rungameid/$_bl_app ;;
esac
echo "== autolaunch $_bl_url"
set -- "$@" "$_bl_url"
fi
# The client builds Settings > Internet and its connection indicator on NetworkManager, which
# it looks for on the SYSTEM bus as it starts. This runtime has neither, so the page failed
# ("StartScanningForNetworks is not a function") under a header that showed no connection even
# while downloads ran. A system bus and a stand-in that reports Android's connection - and
# refuses what it cannot do, since the app has no say over Wi-Fi - answer what the client asks.
# The bus config is written here rather than shipped, so a runtime installed earlier gets this
# from the app's copy of the session scripts without a re-host. (WinNative, maxjivi05, e7a224ae.)
if command -v dbus-daemon >/dev/null 2>&1 && command -v bannerlator-netmanager >/dev/null 2>&1; then
mkdir -p /run/dbus /usr/local/share/bannerlator
cat > /usr/local/share/bannerlator/system-bus.conf <<'BUSCONF'
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<type>system</type>
<listen>unix:path=/run/dbus/system_bus_socket</listen>
<auth>EXTERNAL</auth>
<policy context="default">
<allow user="*"/>
<allow own="*"/>
<allow send_destination="*"/>
<allow receive_sender="*"/>
</policy>
</busconfig>
BUSCONF
rm -f /run/dbus/system_bus_socket
dbus-daemon --config-file=/usr/local/share/bannerlator/system-bus.conf --nofork --nopidfile &
system_bus=$!
for _ in $(seq 1 50); do
[ -S /run/dbus/system_bus_socket ] && break
sleep 0.1
done
if [ -S /run/dbus/system_bus_socket ]; then
bannerlator-netmanager &
netmanager=$!
echo "== network: system bus up, NetworkManager stand-in running"
else
echo "== network: the system bus did not come up; the client will show no connection"
fi
else
echo "== network: no dbus-daemon or netmanager in this runtime; the client will show no connection"
fi
export STEAM_RUNTIME=1
export LD_LIBRARY_PATH=$steam_root/steamrtarm64:$steam_root/steamrtarm64/panorama${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
export SDL_VIDEO_X11_DGAMOUSE=0
# Native-Linux x86 games go through Valve's FEX compat tool, which runs FEX with FEX_PORTABLE=1
# - that makes FEX look for its config next to the binary, while the tool writes one into the
# app's compatdata instead. FEX therefore starts with no rootfs, every x86_64 library lookup
# falls through to the host, and the guest shell dies on libreadline before the game is reached.
# FEX_ROOTFS names it outright and is read whatever the config situation.
[ -d /usr/share/guestos/fex-mesa ] && export FEX_ROOTFS=/usr/share/guestos/fex-mesa
# 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.
_bl_ui=(-gamepadui)
[ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=()
# Deck mode (BL_STEAMDECK=1, the Performance page): -steamdeck is the flag that puts the Quick
# Access Menu, the performance overlay and the Deck settings pages on screen. Only -steamdeck,
# never -steamos3: that one makes the client manage itself as SteamOS does - it calls
# steamos-select-branch and opts the install into the steamdeck_stable client branch, writes
# that to package/beta, and from then on the branch on disk loses to the -clientbeta below on
# every launch; the updater finds "installed version 0", marks an update pending and exits 42
# to apply it, which never happens, so the client restarts for ever - under a running game,
# taking Steam Input with it each time. (Bannerlator 38996ea1, on device.) The SteamOS helpers
# the client calls in this mode are staged as no-op stubs under /usr/bin.
if [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_STEAM_UI:-}" != desktop ]; then
_bl_ui+=(-steamdeck)
# The QAM's Scaling Filter lists NVIDIA Image Scaling only when told gamescope has it (it does).
export STEAM_GAMESCOPE_NIS_SUPPORTED=1
echo "== steam: Deck mode on (-steamdeck)"
fi
# What a killed session leaves behind. Android ends sessions without warning (backgrounding, the
# low-memory killer), and the client then finds .crash - opening its update UI on the spinner -
# and the web helper's Singleton* locks in htmlcache, owned by a process that is gone, which can
# leave the Gamepad UI blank. No client or helper is running at this point in a session.
if ! pgrep -f 'steamwebhelper|steamrtarm64/steam' >/dev/null 2>&1; then
_stale=""
for _f in "$steam_root/.crash" "$steam_root/steam.pid" \
"$steam_root/config/htmlcache/SingletonLock" "$steam_root/config/htmlcache/SingletonCookie" \
"$steam_root/config/htmlcache/SingletonSocket"; do
if [ -e "$_f" ] || [ -L "$_f" ]; then rm -f "$_f" && _stale="$_stale ${_f##*/}"; fi
done
[ -n "$_stale" ] && echo "== steam: cleared what an earlier session left:$_stale"
unset _stale _f
fi
# The client branch. publicbeta is what every session has run on; steamdeck_publicbeta is the
# channel Armada bootstraps its ARM64 client from. Whatever wrote package/beta, the branch on
# disk has to agree with the one forced here - a disagreement is the restart loop above, and
# it is silent apart from rc=42 in this log.
# Checked before every start, not once: the client rewrites package/beta during its own run,
# and a restart below (rc=42, or the one after the ARM64 Proton lands) would otherwise start it
# again on the branch it just wrote.
steam_channel=${BL_STEAM_CHANNEL:-publicbeta}
steam_beta=$steam_root/package/beta
echo "== steam args: ${_bl_ui[*]} -clientbeta $steam_channel -no-cef-sandbox -cef-force-gpu -cef-ozone-platform=x11 -cef-use-gl=angle -cef-use-angle=vulkan $*"
# 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
while :; do
rm -f "$HOME/.bl-restart-once"
if [ -f "$steam_beta" ] && [ "$(cat "$steam_beta" 2>/dev/null)" != "$steam_channel" ]; then
echo "== steam: client branch on disk was '$(cat "$steam_beta" 2>/dev/null)', set to '$steam_channel' to match -clientbeta"
echo "$steam_channel" > "$steam_beta"
fi
step "starting the Steam client${BL_STEAM_UI:+ ($BL_STEAM_UI UI)}"
"$steam_root/steamrtarm64/steam" "${_bl_ui[@]}" -clientbeta "$steam_channel" \
-no-cef-sandbox -cef-force-gpu -cef-ozone-platform=x11 \
-cef-use-gl=angle -cef-use-angle=vulkan "$@"
rc=$?
echo "== steam exited rc=$rc"
collect_steam_logs
# The session asked for this exit (the ARM64 Proton landed): start again, without the URL
# arguments, which were acted on. Not counted against the update cap.
if [ -f "$HOME/.bl-restart-once" ]; then
echo "== steam: restarting once so the new compatibility layer is in the client's lists"
set --
continue
fi
[ "$rc" -eq 42 ] || break
steam_updates=$((steam_updates + 1))
if [ "$steam_updates" -ge 3 ]; then
echo "== steam: asked to restart for an update $steam_updates times and it is not installing; the session stops here rather than loop"
break
fi
# The URL arguments were acted on the first time; a restart must not repeat them.
set --
done
reap_orphans
exit "$rc"
;;
*)
echo "bannerlator-session: unknown mode '$mode'" >&2
exit 64
;;
esac