Tried on device: with a session bus the client still sat on "Switching to Desktop..." and
never reached its legacy path (no "No SteamOSManager support" line, steamos-session-select not
called), so the bus was not what it waited on. Sign-in was unaffected.
This reverts commit 40fc5463f3.
Big Picture's Switch to Desktop asks SteamOS Manager over the session bus before falling back to
steamos-session-select (steamui.so). The Steam session had no session bus, the ask never
finished, and the client sat on "Switching to Desktop..."; the new stub was never called. A
bus with nothing on it answers at once, and the client takes its legacy path.
- The front end's "Steam Desktop UI" started a Steam session without -gamepadui, and the client
put itself into Big Picture under gamescope anyway. It now opens the desktop with Steam's
desktop client started in it (BL_DESKTOP_STEAM, the desktop's autostart, the same
bannerlator-steam-launch the menu's Steam entry uses).
- Big Picture's power menu "Switch to Desktop" runs steamos-session-select, which did not exist,
so it sat on "Switching to Desktop..." for ever. The new stub leaves the app a request
(session=desktop in the steam-launch file); the app ends the Steam session and starts the
desktop with Steam's desktop client open.
AutoPlace on its own left the resized window in the top-left corner; with policy="center" it
opens in the middle of the desktop. Tried in a running desktop (labwc --reconfigure).
melonDS remembers a 1x window for the front end's full-screen toggle, too small on a desktop.
A labwc window rule (only the desktop reads rc.xml) resizes it to 800x600 on open and places
it again by the desktop's centre policy.
The desktop set QT_QPA_PLATFORM=wayland for every program. The melonDS AppImage bundles only
Qt's xcb plugin, so from the desktop it exited at once: "Could not find the Qt platform plugin
wayland ... Available platform plugins are: xcb" (exit 127). "wayland;xcb" keeps LXQt on
Wayland and lets such an AppImage use Xwayland. Proven in a running desktop: labwc mapped
"melonDS 1.1".
The guide button's toggle returns to melonDS's saved window size. A 512x288 window (1x wide,
16:9) is stretched by gamescope to fill the panel, menu bar and cursor with it; at 2x and up
the menus stay at their real size, tiny on a handheld. Written once, with the layout.
Hybrid repeats the big screen, small, above the bottom one. Horizontal with the top screen
emphasised fills the panel's height with the top screen and centres the bottom screen in the
rest. Installs set up with Hybrid (or still on melonDS's defaults) move once.
- bannerlator-pad-defaults: new installs start on the Hybrid layout with sharp pixels; an
existing install is moved once, only where still on melonDS's defaults (marker keeps later
choices). Guide = HK_FullscreenToggle. The user's DS/DSi BIOS, firmware and NAND are found in
ROMs/nds/{bios,firmware} and filled in only where melonDS has no path.
- Front end starts melonDS with -f, at the panel's own resolution unless the user chose one.
- From the rail: -batch -bigpicture -fullscreen (the same flags as PCSX2's,
src/duckstation-qt/qthost.cpp) - straight into the game, gone when it is quit.
- bannerlator-pad-defaults: a new player starts on DuckStation's first-time setup
with the BIOS folder in the ROMs folder (psx/bios, created, or an existing one),
psx as the game list, Vulkan, the controller on Player 1 and the guide button
on the pause menu (not the analog toggle). An existing install gets the guide
and game-list changes, and once, its own empty BIOS folder moved to the ROMs
folder and Automatic made Vulkan; later choices are kept. Users bring their own
BIOS and games.
Quake III Arena, Team Arena, Return to Castle Wolfenstein and Jedi Academy set their own
resolution and switch the display to it. Under gamescope with the ARM64 Proton that switch is
refused ("calling CDS: failed, bad mode") and the game never gets OpenGL - device-seen on a
Retroid Pocket 5 on Proton Experimental build 25406135 - and a window left at the game's own size
draws small in a corner. Both wrappers (Valve's depot and an adopted GE or CachyOS) now add
+set r_fullscreen 0 +set r_mode -1 +set r_customwidth/height at BL_WIDTH x BL_HEIGHT for those
app ids, unless the user's launch options already set the video mode; the choice is logged.
The same choice as the Steam cog's, in the drawer's Now group, changed while the client runs:
the app writes it to ~/.droiddeck-fill (and again at every session start, so a file an earlier
session left never disagrees), and a watcher in the Steam session hands each change to gamescope
as GAMESCOPE_FORCE_WINDOWS_FULLSCREEN on the root window, which gamescope reads whenever it
changes. The session log says so when it does.
- From the rail ARMSX2 starts the game in its full-screen, controller-driven UI
and exits with it (-batch -bigpicture -fullscreen).
- bannerlator-pad-defaults: a PCSX2 player's settings and memory cards are
copied into ARMSX2 once; a new player starts on ARMSX2's first-time setup with
the BIOS folder in the ROMs folder (ps2/bios, created, or ps2/firmware), the
ps2 folder as the game list and the controller on Player 1; the guide button
opens the pause menu (how a rail game is left) instead of toggling analog.
Users bring their own BIOS and games.
- The catalog can name a file per memory page size ("pages"); the device's own
size picks it (ARMSX2 has 4K and 16K builds).
- On-screen controls: Auto keeps them off the desktop (the screen is a touchpad
there) and shows them for rail games and Steam when no controller is
connected; the drawer offers the setting in every session.
Upstream PCSX2 interprets the PS2's CPUs on ARM64 (pcsx2/arm64/RecStubs.cpp):
NFS Underground 2 ran at 17 fps with one core at 100%. ARMSX2 is the PCSX2 fork
with ARM64 recompilers. It is listed beside PCSX2 for the ps2 folder, installs
from the desktop catalog (package armsx2), and bannerlator-pad-defaults gives it
the PCSX2 profile under its own folder (ARMSX2/inis/PCSX2.ini).
vulkan is the default desktop renderer (settings list it first). When labwc
cannot start on a GPU renderer the desktop falls back to pixman as before, and
now records the renderer with the patched wlroots and Linux driver it failed
on, so later desktops go straight to pixman instead of waiting 20 s each time;
an app update, a different driver, or choosing a renderer again in the settings
(which deletes the record) retries.
- A program from the rail draws with the desktop's Linux driver instead of the
Steam session's. The driver's shader cache is keyed on its build, so with two
drivers an emulator compiled everything once per way of starting it; seen on
device: desktop on the imported Turnip 26.3.0, rail on the runtime's own.
- RPCS3's default shader mode builds 6650 interpreter pipelines every boot.
bannerlator-pad-defaults starts a new install on Async Recompiler
(multi-threaded) and moves an existing one still on the default once; a
marker leaves any later choice in RPCS3's settings alone. Enum strings from
rpcs3/Emu/system_config_types.cpp.
bannerlator-pad-defaults now covers DuckStation, PCSX2, Dolphin (GameCube and
Wii), Cemu, melonDS, PPSSPP and RetroArch as well as RPCS3, from each project's
source: files an emulator would otherwise fill with keyboard-only defaults are
seeded when missing (Dolphin, Cemu, PPSSPP, RPCS3), or given SDL bindings in the
config the emulator wrote itself when nothing binds a controller yet
(DuckStation, PCSX2 with its SettingsVersion, melonDS). RetroArch gets an Xbox
360 profile for its sdl2 driver and is moved off udev, which finds no pads in the
runtime. The desktop re-runs it every 30 s, so an emulator's first config is
covered for its next start.
The PC keyboard takes 47% of the screen height, header included.
- 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.
The vulkan desktop sat on the loading screen: labwc connected and never opened
its window, and it did not exit, so the exit-based fallback never fired. The
launcher now asks labwc's socket for an answer (wayland-info, 3 s) for up to
20 s, then stops it and starts the desktop on pixman. labwc runs with -d on
vulkan/gles2 so desktop.log shows where it stopped.
droiddeck-gpu runs a program in a gamescope of its own on the app's compositor
(wayland-0), full screen over the desktop - the path the rail's play chips use,
where Vulkan works. droiddeck-desktop-gpu gives every Game/Emulator menu entry a
per-user copy through it (auto: while the desktop is on pixman), keeping 'Run in
a desktop window' as an action.
droiddeck-desktop loads /usr/local/lib/droiddeck-wlroots for the vulkan/gles2
renderers and falls back to pixman when labwc stops at start. The build stages
the wlroots release named by tools/wlroots/release.env, once published.
On a fresh install the client starts signed out and drops the
steam://install/4427310 the session hands it, so the Proton Experimental
(ARM64) dialog only appeared after the user restarted the client by
hand. Ported from Bannerlator's session (device-proven there 2026-09-22):
- With no remembered sign-in, a watcher on logs/connection_log.txt waits
for the sign-in to complete and asks the running client again.
- A watcher on appmanifest_4427310.acf waits for StateFlags 4, registers
the compatibility tool again, leaves ~/.bl-restart-once and asks the
client to -shutdown; the start loop sees the marker and starts it once
more (not counted against the update cap), so the tool lists and
defaults are current before the user installs a game.
- The client-branch check runs before every start, not once, since the
client rewrites package/beta during its run.
The loading screen no longer says Steam keeps running in the background:
sessions pause when the app leaves the screen (Background behavior).
App name, app ID and code package: DroidDeck, com.droiddeck.launcher
(sources moved to com/droiddeck/launcher, the 68 JNI functions renamed
with them). The new app ID installs beside SteamDeck rather than over it.
Everywhere the name was the app's own: the sidebar title, the session
report, share zips, the shell title, wake-lock and log tags, User-Agents,
the Compose theme, the Download folder and switch files (droiddeck-env,
-osc, -driver, -tu-debug, -no-pad, -pad-log, -no-hud, -wlr-renderer; the
old names are not read), and inside the runtime (droiddeck-desktop,
Firefox's droiddeck.js, the shortcuts tag, the rumble socket, debug and
cache paths). Build scripts, CI artifact (droiddeck-apk), CMake project,
README and the progress log follow.
Kept on purpose: Valve's Steam Deck (the -steamdeck flag,
steamdeck_publicbeta/stable, 'Steam Deck mode', the steamos.cloud
package URLs) and names inside the hosted desktop package (steamdeck.png,
steamdeck-steam, steamdeck-bigpicture). 'Desktop installed' now tests for
labwc instead of the launcher the app stages itself. A committed
__pycache__ .pyc is removed.
Before the client starts, remove .crash, steam.pid and the web helper's
htmlcache Singleton* locks when no client is running - Android ends
sessions without warning and those files leave the next start on the
update spinner or a blank Gamepad UI (SteamOS-ARM-SM8550's RUNSTEAM.sh
does the same). Deck mode exports STEAM_GAMESCOPE_NIS_SUPPORTED=1 so the
QAM's scaling filter lists NIS, and passes gamescope --mangoapp when
mangoapp is present (BL_MANGOAPP=0 turns it off).
frontend/AddedGameArt: cover/poster/boxart/folder-name images, header, hero, logo and icon from
the game's folder (or its art subfolder); otherwise a one-time store search by folder name and
the capsule, header, hero and logo from Steam's CDN into the app's cache (a miss retried after a
week); an icon pulled from the .exe. The rail shows the portrait. The session's listing carries
the paths; bannerlator-steam-shortcuts copies them into userdata/<acct>/config/grid under the
client's names, never over a file it did not write itself, and sets the shortcut's icon. A toggle
in the cog turns the store lookup off.
A Games folder chosen in the Steam cog is scanned one level deep, one subfolder per game; the
program to launch is the .exe named after the folder, else the largest, never an installer,
redistributable or crash reporter, and the settings page lets the user pick another for any
game. Each session the app writes the list, and bannerlator-steam-shortcuts puts the games into
the client's shortcuts.vdf as non-Steam games (the client's own entries kept) with the appid the
client itself derives, mapped to the ARM64 Proton through the compat registrar. They appear in
the rail under Steam and launch by steam://rungameid/ like any title.
On device the client's console log showed the calls: /usr/bin/steamos-polkit-helpers/steamos-update,
steamos-select-branch, jupiter-biosupdate and jupiter-dock-updater - the polkit-helpers path, not
/usr/bin. The missing steamos-update there was the 'Update Error' dialog. Stubs at both paths now.
Also seen on device: -steamdeck alone opts the install into the steamdeck_stable branch too, so the
branch guard is what keeps a session from reinstalling; and on the publicbeta channel Deck mode
reports installed version 0 at every start and reinstalls the same client (rc=42, the launch URL
lost with it), while on steamdeck_publicbeta the second launch comes up with no restart at all.
Deck mode passes -steamdeck alone: -steamos3 opted the install into the steamdeck_stable
branch and started the restart loop Bannerlator hit on device (38996ea1). The SteamOS helpers
the client calls in that mode - the two Armada found it needs under steamos-polkit-helpers and
the three under /usr/bin - are staged from the apk as no-op stubs. The client branch on disk is
kept in step with the one forced on the command line, and an update that cannot install stops
the session after three restarts instead of looping for ever.
The Steam cog gets a Client branch row: public beta (every session so far) or Steam Deck
public beta, the channel Armada bootstraps from.
The client reads /sys/class/power_supply/BAT<n>/; Android's supply is called battery and laid
out differently, so the Quick Access Menu had no battery. BatteryComponent writes BAT0 and BAT1
from Android's battery API every five seconds - status, percentage, charge, energy, power and
time to empty - into a directory bound over /sys/class/power_supply.
- libpci: an empty file bound over /proc/bus/pci/devices, created at session start, so
Chromium's GPU process stops dying on its way up and CEF stops falling back to SwiftShader
(WinNative deff1ac6 via Bannerlator 8fb668d1: 44 -> 85 fps in the Big Picture library on a
OnePlus 15; not measured here yet).
- Client interface, as toggles on the Performance page and lines in the device report:
mesa_glthread, ZINK_DESCRIPTORS=lazy (now on by default) and MESA_NO_ERROR on; Steam Deck
mode (-steamdeck -steamos3, the script builds the line) off.
- A non-Adreno device is told before the runtime download that a session would be sound over a
black screen, and may still go ahead.
Not carried: Bannerlator's proot fixes live in its proot source, which this repo does not
build (the runtime tarball ships its own); its apk-proot -i guard is moot here (the apk carries
no proot); its session-log head keep is moot (Steam's logs are copied whole).
The client already writes one menu entry per installed game (steam steam://rungameid/<id>, with
the game's icon); they were dead for want of a steam command, and r45 added a second entry per
game beside them. Now: steam on the desktop is a shim that hands off to bannerlator-steam-launch
(and is the client itself anywhere else), and bannerlator-desktop-games writes an entry only for
a game the client wrote none for.
bannerlator-desktop-games writes one menu entry per installed game (both libraries, the
client's own artwork) and links Steam Games / SD Games in the home folder. The entries, and the
menu's Steam / Big Picture ones, run bannerlator-steam-launch: the client cannot draw on the
desktop (labwc, no dma-buf), so it leaves a request in the session directory; the service
watches it (FileObserver), ends the desktop session and hands the activity a Steam session to
start - desktop UI (BL_STEAM_UI=desktop drops -gamepadui) with the game's rungameid URL.
The runtime already knew how to register /mnt/bannerlator-sd with the client; the app never
bound anything there, so the client only ever saw internal storage. The Steam cog's Game storage
section offers every removable volume (through the app's own folder on it, the one place a card
lets a targetSdk-28 app write) and any folder from the File Manager, proven writable first. The
session binds the choice and names it (BL_LIBRARY_LABEL); bannerlator-steam-library registers it,
or forgets it when nothing is bound. The client does the rest: asks where to install, shows both
on Storage, moves games between them. device.txt records the choice.
The file picker is now Bannerlator's File Manager itself (files/FileManagerScreen.kt, carried over
whole with its rail, favourites, grid/list, copy/cut/paste, rename, delete, properties) minus what
has no meaning without a Wine container: run, add to shortcuts, C:/Z: drives, the archive
unpacker. StorageRoots, CollapsibleRail, FavoritesStore, PeIconExtractor, OutlinedAlertDialog and
MenuStyle come with it unchanged; FileOps carries the progress copy/move. A Files tile opens it;
its pick mode chooses the ROMs folder and imports drivers.
Play and Desktop each get a cog: resolution cap (1080p default, 900p, 720p, panel), shape, HDR10,
the mode's Linux runtime driver, the display driver, touch, and per mode the on-screen controls +
audio (Steam) or the renderer (desktop). The Graphics drivers and Audio tiles are gone into them.
HDR10: HdrSupport reads the panel's HDR types; the switch is greyed with the reason unless HDR10 is
listed. On, the activity feeds the compositor's gate (nativeSetHdrDisplay + nativeSetHdrRequest),
turns zero-copy on, sets BANNER_WAYLAND_HDR=1, and the session gets BL_HDR=1 (gamescope
--hdr-enabled) and DXVK_HDR=1.
OrphanReaper kills every process of our uid but the app at session start and on an app crash: a
tree left by a killed app held the rootfs, the GPU and Steam's lock, and the next session never
came up (Thor Pro report). The drawer gains Steam menu, which sends the Guide button.
The Steam client builds Settings > Internet and its connection indicator on
NetworkManager and looks for one on the system bus as it starts. This runtime
had neither, so the page stopped with "StartScanningForNetworks is not a
function" under a header that showed no connection while downloads ran. A user
on an Odin 2 reported exactly that, and I told him it was cosmetic and unfixed -
wrongly: Max wrote the answer for WinNative (e7a224ae) and Bannerlator has
carried his port for days. This app was the only one without it.
The session now starts a small system bus and his stand-in, which reports one
connection that is up while Android has a route out and refuses what it cannot
do, since nothing here can scan for or join a network. Both the script and the
bus config come from the apk at every launch, so an installed runtime gets this
without a re-host; dbus-daemon and python-gi are already in r9.
Named bannerlator-netmanager rather than steamdeck-: CI copies
overlay/usr/local/bin/bannerlator-* by glob, so anything else would have been
built, committed and never packaged - the same shape as the proot artifact that
was produced and discarded on every run for a week.
Version 0.1.3, and the README gains a section for it.
A session used to leave a loose session-<stamp>.log in Download/SteamDeck and
a compositor log in a different folder entirely, with Steam's own logs copied
beside the first one only if the session exited cleanly - which the sessions
worth reading never do. Reports arrived needing three questions answered
before anything could be read: what device, which drivers, which settings.
Now each session claims Download/SteamDeck/session-<stamp>/ and everything
lands in it: device.txt, session.log, wayland.log, steam.log plus steam/ in
Steam mode, desktop.log in desktop mode. The folder is claimed by whoever
starts first, since the activity starts the compositor before the service
starts the session, and the compositor is told where to write through its
environment - it opens that file once, so the service also copies it in at
teardown, which covers the second and later sessions of one process.
device.txt is written before the session starts, so a session that dies in its
first second still says what it ran on: model, SoC, Android, kernel, cores and
ceilings, RAM, the GPU as KGSL names it, the panel, the driver each half of the
session chose, the runtime version, every switch in effect, and whether Android
is set to kill the session's child processes.
Nothing identifying is collected - no serial, no device or advertising id, no
account name, no network names - and Steam's logs are scrubbed line by line on
the way in, with Bannerlator's patterns: session tokens (including the bare
numeric "Using JWT" form), machine-auth GUIDs, WebAPI keys, Guard codes,
emails, and SteamIDs masked to their last four digits so lines still correlate.
loginusers.vdf, config.vdf and the ssfn files are never copied.
Also from Max: a switch to run proot without its seccomp acceleration
(PROOT_NO_SECCOMP=1), for kernels that handle that filter badly - ENOSYS from
calls that plainly exist is the signature, which is what the Fold's storm looks
like; and the phantom-process warning above, which is his "your OS killed it".
A Steam session runs two things that want different cores at the same time,
and one affinity cannot serve both (Bannerlator cfca3912).
The client is the case that prompted it: Steam pins its own interface renderer
to a subset of cores it chooses - five of eight on one device, leaving out both
little cores and the fastest one - reasonable while a game is running and wrong
when the client is all there is, where it shows as Big Picture feeling sluggish.
BL_CLIENT_CPUS overrides it, re-applied on a beat by the session script because
steamwebhelper spawns children continuously and each inherits Steam's choice.
That block is Bannerlator's, verbatim. One deliberate difference: the client's
mask is sent whenever the override is on, even when it names every core,
because undoing Steam's pin is the whole point and the scheduler's default is
exactly what that pin takes away.
A game gets BL_GAME_CPUS instead, consumed by the Proton wrappers that r27
already carries, and as in Bannerlator it is sent only when it is a real
restriction: a list naming every core is what the scheduler does unaided.
Both live behind a Performance dialog with a checkbox per core, labelled with
its ceiling, and apply at the next session start.
Two opt-ins on a new Audio screen, both ported from Bannerlator's gamescope
runtime where they are proven with voice in Steam's own tester.
DirectAudio replaces the audio driver inside the games the client launches -
Wine's mmdevapi driver, glibc build - with one that talks to a helper on the
Android side over a unix socket. The client keeps PulseAudio either way: it is
a native Linux program with no other way to make a sound. The helper runs here
because nothing inside the session can open Android's audio: a game is a glibc
process in a rootfs that binds no /system, and the uid the helper runs under is
what the recording permission is checked against.
The microphone is its own opt-in. The helper opens the input stream only when
asked and writes it to a pipe; PulseAudio's module-pipe-source turns that into
a source the client sees as DirectAudioMic. The daemon makes the pipe and the
helper waits for it - the two used to race, and the module fails on EEXIST.
The Proton wrappers gain Bannerlator's bl_directaudio, which adds the driver's
directory to WINEDLLPATH and names the driver in the prefix, and refuses on any
Proton that is not Wine 11 rather than pair a driver with an interface it was
not built against, which would be silence rather than an error. bl_game_cpus
comes along in the same splice and is inert until BL_GAME_CPUS is set.
The driver is staged into the runtime at every launch like the session scripts;
the helper ships as libdirectaudiorelay.so beside the PulseAudio daemon. Both
are the binaries Bannerlator carries, built by The412Banner/directaudio's relay
workflow (linux-wine11 build against ValveSoftware/wine proton_11.0).