- 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.
wlroots 0.20.2 (Arch's wlroots0.20, what the desktop's labwc links) gains an
allocator that exports Vulkan images as DMA-BUFs for a GPU node that is not a
DRM device - the Adreno KGSL stand-in - where gbm and the DRM lease path fail
('unable to create allocator'). linux-dmabuf skips its GEM import check on such
a node so client buffers are not all refused. Built by build-wlroots.yml.
Compositor: without a Wine desktop, a window that opens on top takes keyboard
focus (a gamescope started from the Linux desktop).
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.
The QAM offers Performance Overlay levels but nothing drew them: the
runtime has no mangoapp. CI now fetches Valve's mangohud-20260902.1 and
the five libraries mangoapp needs that the runtime lacks, at the versions
it was built against (fmt 10.2.0, spdlog 1.13.0, glfw 3.4, libtraceevent
1.8.2, libtracefs 1.8.0 from Valve's mash-20240705.2 snapshot), each
pinned by sha256 in tools/mangoapp/packages.txt. They are staged under
usr/local/lib/mangoapp with a wrapper on PATH; the runtime image is not
touched.
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).
The unpack check compared a hand-bumped stamp only. Two sink changes shipped without a bump, so a
device that had run the earlier build kept the old module: the 40 ms ring floor and the 24 ms
device buffer never reached the AYN Thor they were for. The stamp now carries a CRC-32 of
pulseaudio.tzst. The sink's connect line names the buffer it asked for, and one line ten seconds
in reports the relay's device buffer, ring target and underruns.
The relay's default is 12 ms rounded up to whole bursts. On a device where Android grants a fast
stream (4 ms bursts) that is three bursts; on one where it grants a legacy stream (20 ms bursts,
an AYN Thor on Android 13) it is a single burst, and the Thor's client audio stuttered. 24 ms is
two bursts there and 24 ms on the fast devices. buffer_ms= overrides it.
- module-directaudio-sink keeps the relay's ring at least 40 ms deep. The relay starts at 8 ms,
which the producer inside proot missed while the client loads (two underruns on the FIT's fresh
install, steady stutter reported on another phone).
- Setup > Session logs > Share latest logs zips the newest session folder, wherever it was written
(Downloads, or app-private files when Downloads is not writable), and opens the share sheet via
a FileProvider scoped to that zip.
A second sink module: connects to the relay's socket (lazily, the relay comes up after the
daemon; a clocked null sink until then), sends the hello, maps the render ring it gets back and
keeps it topped up to the relay's target with a futex wait per burst. Every AAudio call, the
adaptive buffer and the route handling stay in the relay, outside proot. With DirectAudio on the
daemon loads this sink instead of module-aaudio-sink; the bundle carries both.
Two bursts (8 ms) of buffer with a write per burst left the client's menu sounds choppy on
device: the daemon runs under proot, where every system call is traced. The buffer now starts at
four bursts or 30 ms, whichever is more, each write hands over half of it, the IO thread asks for
nice -16 like Android's own audio threads, and the log says how big the buffer is and how many
underruns the device counted.
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.
Blocking-write design: the IO thread pulls one burst at a time from the sink and hands it to a
blocking AAudio output stream, whose buffer paces the thread; latency comes from the stream's
playback timestamp; with adaptive=1 the buffer grows a burst per underrun up to a cap. Same
module arguments as before, so the daemon's config line is unchanged. build.sh compiles it with
the NDK against the app's own libpulse* and a small config.h; a workflow checks it on every push.
Four of Armada's patches apply to 3.16.29 as they are (appid-focus fallback, ARM64 night mode,
ARM64 colour-temperature slider, P3 red); two more are ported by hand: realtime-priority Vulkan
queues on GAMESCOPE_FORCE_VULKAN_REALTIME=1 without CAP_SYS_NICE, and the gamepad cursor sprite
following the X pointer. build-gamescope.yml builds Arch Linux ARM's package with them in an
Arch container, checks every NEEDED library against the runtime's own list, and publishes the
binary as a release asset for the apk to stage over /usr/local/bin/gamescope.
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".