- 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).
- 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.
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).
Two deviations from a normal Steam launch, both removed rather than worked
around — Bannerlator's fix (334955ea), brought across unchanged.
The overlay was being stripped from every game because it landed in front of
open() and read() on /dev/input, letting Wine open the session's pad and then
answering every read of it with nothing. That was the wrong cure for an
ordering problem: glibc reads LD_PRELOAD before /etc/ld.so.preload, and the
interposer lives in the latter, so the overlay was simply ahead of it. Naming
the interposer first in LD_PRELOAD puts it back in front and the overlay loads
as Valve intends, in both wrappers — Valve's depot and an adopted GE-Proton or
proton-cachyos. Every difference between the launch Valve ships and what runs
here is a difference this runtime would otherwise have to account for.
Second, the competing Steam client is asked to stop rather than only named.
killBackgroundProcesses is a normal permission and ends exactly the case that
keeps recurring: GameHub declares boot receivers, so it is running from the
moment the phone starts without ever being opened, and it takes the login off
the client seconds after every sign-in. Done at Steam-session start only; the
desktop signs into nothing.
README: the GPU requirement now says which Adrenos Turnip actually drives. A
user's session log ended at "device (chip_id = 7010000, gpu_id = 710) is
unsupported" — an Adreno 710, which Mesa has no entry for.
Ported from Bannerlator's driver selection so the standalone stops being pinned
to whatever Turnip the runtime image was built with (Mesa 26.2.2 today, while
Bannerlator already runs 26.3.0 in the same role).
Linux runtime driver: LinuxVulkanDriverManager imports a "-Linux" (glibc)
Turnip zip under files/linux_vulkan_drivers/<id>/ with a generated ICD manifest
whose library_path is absolute, validated by the libc the driver links against
(libc.so.6, never libc.so) and by its ELF header. The choice is per mode, so
Steam and the desktop can differ. SessionService hands the session BL_VK_DRIVER,
and the session script — staged from the apk at every launch, so no runtime
re-host — checks the manifest and its library from inside before exporting
VK_DRIVER_FILES; the runtime's own driver is what anything unreadable falls
back to. That block is Bannerlator's, verbatim.
Display driver: TurnipDriver now also imports AdrenoTools zips beside the two
bundled builds and honours a chosen id ahead of the GPU pick, with the same
refusals as Bannerlator's AdrenotoolsManager — a zip with no libraryName, a
Linux runtime driver, or a glibc library dressed as one is refused with the
list it belongs in, never installed as a black session.
One dialog on the main screen carries both lists; a removed import falls back
at once. Documented in the README.
A second thing to launch beside Steam: the LXQt/labwc desktop, a hosted package
the app installs into the runtime like the runtime itself, with the emulators
and apps as further packages from the same catalog (desktop.json in
winlator-contents). A tar package extracts over the rootfs with the runtime's
own extractor; an AppImage lands in /opt/appimages with a .desktop entry and
runs extracted, since proot has no FUSE. The session script's lxqt mode runs
the desktop's labwc in place of gamescope.
From Bannerlator's feat/linux-gamescope-runtime since this repo was cut:
- Session teardown (4509d788): proot was SIGKILLed, which never lets its
--kill-on-exit cleanup run; its tracees lived on with no tracer, every
seccomp-trapped syscall failing with ENOSYS, spinning at a full core for over
an hour. Now SIGTERM, a grace for proot's own cleanup, SIGKILL only if it
will not go, then a sweep of the tree it had — snapshotted first, each pid
checked against its start time.
- The Steam client's controller interposer is its own source
(fakeinput_steam.cpp, 28922e7c), separate from the one preloaded into Wine.
- The seeder runs on a beat so a prefix the client creates mid-session is
covered before its install scripts run (6ee86bff).
- GE-Proton and proton-cachyos as compatibility tools: the registrar
bannerlator-proton-extra and the compat script's adopt_extras (03867656).
- space.c in the session shim: a library's own free space, not the phone's.
The env-vs-args ordering bug (98d38931) does not apply here: nothing is added
after the script name.
Lifts Bannerlator's Linux/gamescope runtime out into an app of its own: the
Wayland compositor, the proot session, the runtime installer, the fake-evdev
controller path and the PulseAudio server, with none of the Wine, container or
store machinery around them.
One activity installs the runtime from the same winlator-contents catalog and a
second one runs the session: compositor on a SurfaceView, gamescope and Valve's
native arm64 client under proot, a physical pad republished into the fake-input
rings, and the session's milestones on screen until the first frame arrives.