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".
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.
The screenshot shows the tab crashing while the chrome around it draws text
perfectly, and the 'no fonts' line is an annotation on that crash, logged by
the child. The parent reads the fonts; the child, sandboxed with seccomp and
namespaces that do not behave under proot, cannot, and Gecko takes a fatal
path with no usable face. The prefs for this are advisory and a profile can
override them, so the sandboxes are turned off in the environment the session
hands every program it starts.
Firefox's own network log shows it fully online - DNS answering for both
families, sockets connecting, HTTP/2 and HTTP/3 sessions carrying data - so
the blank pages were never the network. They are the fonts: Gecko reports
'no fonts' with 13 families found, which is what it says when no generic
family resolves to a face. The linked rules map sans-serif onto faces a full
distribution installs; this rootfs has DejaVu and nothing else, so local.conf
names it outright for every generic, and the cache built into the image is
rebuilt once so the new rules are seen.
/etc/fonts/conf.d in the rootfs holds only the README: none of fontconfig's
rules that map sans-serif, serif and monospace onto installed faces. Programs
find the fonts and no default font; Firefox logs 'no fonts' and draws pages
without text. The desktop launcher links the 33 default rules in when the
directory has none.
The session reaches the internet over both families (r32's probe), so a
browser saying otherwise has to explain itself: with the debug marker set,
Firefox logs its name lookups, connects, HTTP transactions and link/offline
decisions to ~/.cache/steamdeck/firefox-net.log.
Firefox decides whether it is online by dumping the routing table over
netlink, which Android denies to apps; it sees no default route, switches to
Work Offline and every page fails while the network is fine. A defaults file
staged into Firefox's pref directory turns that off, and drops the process
sandboxes that have nothing to set up under proot. Debug runs now log a name
lookup and a fetch over IPv4 and IPv6 from inside the session.
labwc's Wayland backend, configured with 0x0, opened wlroots' default
1280x720 window and the whole desktop was upscaled from it - the r29 and r30
logs both show the output at 1280x720 whatever rc.xml and wlr-randr asked.
The configure now carries the output's size; gamescope is told the same
numbers on its command line, so nothing changes for the Steam client.
The panel was there all along, a bare strip at the top: the first package's
panel.conf described a panel it never named in [General] panels=, so
lxqt-panel built itself a default one with no plugins. panel.conf now travels
with the app like the rest of the desktop's defaults, and the old nameless
file on a device is replaced once. The nested labwc output was 1280x720 -
wlroots' default window - with the desktop upscaled from that; rc.xml now
carries the app's surface size and the autostart sets it again via wlr-randr.
Debug runs keep each program's full output under ~/.cache/steamdeck/.
The desktop came up black with the panel alive but unseen. pcmanfm-qt's
desktop surface, unconfigured, paints solid black over swaybg's wallpaper; it
now gets the wallpaper. With ~/.steamdeck-desktop-debug present, labwc logs at
debug level and the panel and file manager log their shell, layer and output
protocol traffic, capped, so where a window went can be read off the session
log.
The desktop came up with a wallpaper and nothing on it the second time. The
runtime directory outlives a session, and lxqt-panel and pcmanfm-qt are
single-instance programs that leave a lock and a socket there; finding last
session's, they decided they were already running and exited without a word.
The launcher now clears everything but the compositor's own socket, runs labwc
under dbus-run-session so the panel's menus and the file manager have the bus
they expect, and logs what the panel and the file manager say. The launcher
and labwc's defaults are staged by the app over the desktop package's copies,
so a fix here no longer costs a 426 MB re-download.
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.