FrontEndScreen replaces the main screen on this branch. Steam's installed games come from the
client's own appmanifests (both libraries, with the library-cache art) and launch straight
into a Steam session with steam://rungameid; the emulators list their games from the ROMs
folder (a system folder as a hint, by extension) and launch under gamescope with the game on
the command line (EXTRA_PROGRAM_ARGS). A session running in the background is the first thing
on the rail and tapping it goes back to it. Landscape: rail beside content; narrow: above.
Every dialog and setting from the main screen is reachable from the rail's lower list.
Handing off from the desktop, the new session claimed the folder the desktop was still
finishing, and the desktop's collector then stopped the new session's app.log capture. The
folder is taken from the shared slot as the session stops and handed to its collector; the
next session's beginOrCurrent finds the slot empty and makes a new folder.
The replaced desktop's proot exits on SIGTERM within a tenth of a second - before the new
session's start thread had numbered itself - so its exit still matched the current number and
ended the new session (4 s on the ended card, then the main menu, with the client audible in
the background). The number is taken in onStartCommand now, ahead of any thread.
Hand-off from the desktop: the new session started, and a second later the old proot - killed
by its own teardown - reported its exit, which the service took as the current session ending
("The session ended 137"). Exits are matched to the session that started the process. The same
race took the new session's log capture and its folder (stopFor / release by folder), and a
finishing activity unhooked the replacement's end listener (compared by identity now).
Hand-off from the desktop to a Steam session: the new activity attached its Surface, then the
old one's surfaceDestroyed detached it - the compositor presented nothing while gamescope sent
frames by the thousand (black screen, sound only). detach() now takes the Surface being destroyed
and ignores one that is not attached. BL_LIBRARY_LABEL was added to the guest list after the
script's mode and reached the Steam client as an argument; it is an environment variable now.
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 choice belongs inside the client - the drive drop-down on Install, Settings > Storage - so
the app no longer needs a setting for the common case. Default = automatic (the first SD card
present); the cog keeps Internal only and a chosen folder.
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.
Two field bundles: an S25 running with Turnip sysmem on (it turns tiled rendering off - a
corruption fix, not a speed setting, and the text did not say so), and a Fold 5 ending, again,
in dozens of 'Function not implemented' lines from proot's seccomp acceleration failing Steam's
xalia helper. The ended-session message now reads the log's tail off the main thread and, when
it finds that storch, names 'Run proot without seccomp' / 'Skip xalia' in Performance.
One chip per emulator the desktop packages installed, starting it fullscreen under gamescope;
the ? says what they are, how to use them (ROMs at /root/ROMs, the phone at /root/Storage) and
why they are here rather than on the desktop (labwc on pixman offers no dma-buf).
RPCS3 on the desktop died at its first frame with VK_ERROR_SURFACE_LOST: labwc composites in
software (pixman) and initialises no dma-buf, so no Vulkan program on the desktop can get a
swapchain. The GPU path that works is gamescope's, which Steam's games already render through.
MODE_RUN starts a program inside the runtime as a session of its own under gamescope (the
script's run mode: AppImage, script or binary), taking Steam's display/driver/HDR settings; the
apps dialog offers ▶ for every installed emulator (AppImages, PPSSPP, RetroArch).
Volume Up/Down/Mute were forwarded to the guest as keys and never reached Android, so the
device's volume never changed during a session; they now fall through, on the media stream.
Display, HDR, Runtime driver, Display driver, Touch & controls, Audio (Steam) / Renderer
(desktop), each found by name instead of by scrolling past the others.
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.
A program's file dialog inside the session never showed the phone: internal storage was bound
at its own path, but proot binds do not appear in /proc/mounts and nothing under home pointed at
it. Every session now binds internal storage at /root/Storage and the folder chosen with the new
ROMs folder tile at /root/ROMs - a bind, so a folder on an SD card works the same.
Folder and file choosing is the app's own picker (files/), Bannerlator's File Manager pick mode
carried over: StorageRoots verbatim for the drive menu, the same intent API (InAppFilePicker),
folders first, an up-arrow floored at the volume root, the last folder remembered. Both driver
imports use it instead of the system document picker.
A session folder is finished whichever way the session ends: SessionArtifacts is shared by the
ordinary stop, an uncaught-exception handler (App/CrashHandler, writes crash-app.txt and finishes
the folder before the process dies) and a sweep at the next app start for a folder without its
.complete marker (a kill or a native crash), which gets ended-without-teardown.txt and the crash
buffer. A Session logs tile turns the folder off: it then lives in the cache and is deleted at
the end.
The main menu is two columns of tiles, each with its setting's value under its title.
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.
The README had grown a section per feature in the order they were written, and
the parts that describe the whole had fallen behind: it still said version 0.1,
the ledger listed none of the drivers, audio, cores, session fixes or logs, and
the architecture diagram showed neither the audio relay nor that there are two
Turnips with two import lists.
Now: the ledger covers everything the app does with an honest state against
each - and says outright where something sets what it claims but has never been
measured; the diagram shows both driver slots and where a game's audio can go;
the session-log folder has a section of its own instead of being an appendix to
the device switches; and Installing mentions the one Android setting that
decides whether a session stays up at all, since that has cost two people an
afternoon.
Leaving the desktop hung the app until Android ANR'd it, and the log I had just
added is what showed why: stopSession runs on the main thread - the
notification's Stop action arrives there - and it called the collector
synchronously. That meant copying the compositor's log, scrubbing every Steam
log line by line (42 files on the measured run) and then waiting on logcat, all
on the main thread: about four and a half seconds, ending in signal 3 and
stack traces written to tombstoned.
20:45:32.721 session ended: 137
20:45:34.280 collected 42 Steam log(s), scrubbed <- main thread
20:45:34.282 started logcat -b crash <- main thread
20:45:36.537 reacting to signal 3 ... Wrote stack traces
The collector now runs on its own thread. Nothing in it touches the service's
state after the components have stopped, and the app process outlives the
service, so there is nothing to race.
The crash dump was also wrong twice over: it polled for the output being
non-empty, but the lines arrive through a callback on another thread, so it
declared the buffer empty every time and still paid the wait; and it printed
exec()'s return value as "rc" when that is a pid, never an exit status. It now
waits on the process ending, with a real timeout, and says which of the three
things happened - could not start, did not finish, or genuinely nothing has
crashed.
Nothing else in that session had gone wrong: labwc and Firefox's shutdown noise
is ordinary, and proot needing SIGKILL on the desktop path is expected.
Two events the log was silent about, both of which a report needed this week.
A window that changes the size of the frames it commits has rebuilt its
swapchain, and that is what a game suddenly filling one corner of the screen
looks like from the compositor's side: FlatOut came back from the Steam menu in
a smaller swapchain and the log said nothing at all, so the size had to be
inferred from a screenshot. It now says both sizes on the line.
And a window already on screen being renamed is something else taking it over:
gamescope keeps one toplevel and retitles it as the game takes the screen. That
was silent, which left the log showing a window "closed" under a name it had
never been logged as opening - reading like a missing event when it was really
a rename. Both are rare by nature; there is nothing to rate-limit.
Closes the four gaps that were left after the session folder: everything the
app itself decided was reaching logcat and nowhere a user can get at, sound had
no record at all, the network had none either, and a session the system killed
left its only trace in a buffer nobody was reading.
app.log mirrors this process's logcat for the length of the session - which
driver was chosen and why, the audio line, a rival client stopped, a helper
missing, the exit status. An app may always read back its own entries, so this
needs no permission; where a ROM refuses, the file says so rather than being
absent and looking like silence.
crash.log is Android's crash buffer at teardown. proot once died before main
over a missing library and said so there and in no other log, and a session the
OS kills for spawning children says it there too.
audio.log takes the PulseAudio daemon's output and the relay helper's, both of
which went to logcat only. The 17.0-module failure that cost the microphone a
whole round of builds would have been one line in this file.
network.txt records the transport, whether the link validated, the address
families, the DNS the system offered and the resolv.conf the runtime was
actually given - never the network's name, which is identifying. It also states
plainly that the client's own "wifi is off" is cosmetic, since that reading has
now cost two people an afternoon.
Left out on purpose: a Vulkan dump. It needs a new native entry point for
information wayland.log already carries on its first lines.
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".
xalia is an x86 Windows program Proton launches to give Windows programs
gamepad navigation. Under FEX it cannot load the session's aarch64 preload
shim, so its socket() and memfd calls reach the vendor's seccomp filter raw -
and where that answers ENOSYS, measured on a Galaxy Fold, it storms: 89
refusals of each in a few seconds, then a broken Wayland pipe and a session
that is gone 6-12 seconds after Big Picture appeared. Fourteen sessions in
eighteen minutes died that way on one device while the same build ran for
minutes on another.
Proton has its own gate for it, PROTON_USE_XALIA, so the switch sets that
rather than interfering with the helper. Off by default: xalia is Valve's, and
on a device whose seccomp answers its syscalls normally there is no reason to
take it away.
The two tunables the Fold experiment needs, as switches under Performance
rather than files in Downloads: TU_DEBUG=sysmem for the runtime's Turnip and
ZINK_DESCRIPTORS=lazy for the client's menus. The Downloads files still win.
Downloads/steamdeck-env holds KEY=VALUE lines that go into the session's
environment as written, after the app's own, so a line here wins. For a device
that cannot be reached with a debugger and an experiment that would otherwise
cost a build per attempt: Zink and Turnip tunables, gamescope's, the client's.
The case at hand is a Fold whose Steam menus run at 14 fps on the A8xx Turnip
while a game on the same session runs at 89 - the client's Chromium goes
through ANGLE and Zink, and ZINK_DESCRIPTORS=lazy is the first thing to try.
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.
The bundle carried a PulseAudio 17.0 glibc build of module-pipe-source (and
-sink): NEEDED libpulsecore-17.0.so and ld-linux, beside a 13.0 bionic daemon.
It could not dlopen, the DirectAudioMic source never existed, and the Steam
client reported no input device - the first thing the microphone port ran into
on a device. Bannerlator replaced both modules with its own 13.0 builds
(997acba2); this is that bundle, byte for byte, the other 75 files unchanged.
The component also unpacked the bundle only once, ever - a device kept the
modules from its first run whatever a later apk shipped. It now unpacks whenever
BUNDLE_STAMP changes, so this fix and the next one reach an installed app.
The list spans three lines inside one string, and the case pattern looked for
the name between two spaces - so the last name on each line, followed by a
newline, could never match. libaaudio.so is one of those, and the DirectAudio
relay helper is the first library we ship that needs it.
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).
Turnip's debug flags reach the runtime's driver and everything on it through
TU_DEBUG. Downloads/steamdeck-tu-debug now sets it verbatim, like the other
device switches. With nothing there, an imported driver from the A710/A720/A722
legs gets "sysmem" on its own: both the patch's author and our release notes
advise it for those GPUs, and nothing else in the list needs it.
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 app gains a second thing it can run. Where the Steam mode hands the
session to gamescope, the desktop mode hands it to labwc with LXQt's panel and
file manager, Xwayland for the X11 programs, and a catalog of packages hosted
beside the runtime: the desktop itself, an emulator set, and the standalone
emulators that have no ARM64 Linux build upstream and are compiled from source.
With it come the pieces a desktop needs and a game does not: a pointer the
touchscreen can drive as a touchpad or directly, a resolver written from the
live network so a browser reaches the internet, fontconfig defaults so text
has a face, and the child-process sandboxes turned off for programs that
cannot set them up under proot. The Steam side gains one thing: starting the
client offline, decided before launch because the client reads it only once.
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.
The client runs without a connection - installed games launch, Proton and the
frame-gen are all local - but only with credentials from an earlier sign-in,
and it decides by two keys in loginusers.vdf that it reads once while it
starts. That is why the switch belongs here and not in the session's drawer:
changed during a session it would write the file and change nothing until the
next launch. The button is disabled, and says so, until the client has signed
in once. The client rewrites the file when it exits, so the request is kept as
the app's own preference and written in again at every session start.
/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/.