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.
Touch on the desktop now works the way Bannerlator's desktop containers do:
the arrow stays where it is and a finger dragged anywhere moves it from there,
tap clicks, a second finger tapping while one holds (or a two-finger tap)
right-clicks, two fingers scroll, hold-then-drag and double-tap-hold drag.
Steam keeps the direct pointer; the drawer pins either.
The runtime's resolv.conf is written from the active network's DNS servers on
every link change — IPv4 first, never Android's link-local IPv6 ones, the
public pair as fallback — which is what glibc, and so Firefox, resolve with.
Routing needs nothing: proot makes no network namespace.
The pointer moved but nothing drew it — the nested desktop asks its parent
compositor for a cursor and ours ignores the request — and touch knew only
move and tap. Now the app draws the arrow where it last sent the pointer, touch
works like a remote desktop (tap clicks, hold then lift right-clicks, hold then
drag drags, two fingers scroll), and a mouse arrives as itself: hover moves,
every button presses, the wheel scrolls, with Android's own pointer hidden so
there is one arrow.
labwc died at "unable to create allocator": wlroots allocates through gbm on a
DRM render node, and ours is a KGSL stand-in gbm cannot use — the wall the
freedreno spike hit, which gamescope only clears with its own Vulkan-WSI path.
pixman uses a shm allocator and no DRM at all, so the desktop boots; clients
on it pay a CPU copy. steamdeck-wlr-renderer in Downloads overrides the
choice for a device with a real node.
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.
With Keyboard and Compatibility tools added, a landscape phone no longer had
the height for it and Stop session fell off the bottom. The panel scrolls, and
the two session-leaving actions ride at the end of the list rather than pinned.
A session had no way to type without a hardware keyboard. The drawer's Keyboard
opens the soft keyboard onto an invisible host View whose InputConnection turns
what the IME commits into the key events that would have typed it, and the
key path carries Bannerlator's two soft-keyboard fixes: a symbol key with no
scan code is worked back from the character it stands for, and a capital gets
the Shift a soft keyboard reports but never sends — not a hardware one, whose
own Shift would be released early by a second.
Compatibility tools, on the main screen and in the drawer, queues a GE-Proton
or proton-cachyos install as a line in the runtime's ~/.bl-proton-extra, which
the session script hands to bannerlator-proton-extra before the client starts;
it shows what is installed and removes a build.
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.
The audio stack, the gamepad model and the shape of a session's host-side
components are brunodev85's, and both apps this was lifted from are Winlator
lineage. The credits and README named the descendants and not the ancestor.
Opening the Steam menu over a fullscreen game and closing it again left the
game as a strip in a corner with an engine on. gamescope forwards a fullscreen
game's own buffers straight through and hands over its composited output while
the menu is up; each is a different window to the compositor, and the engine's
ring and history straddled the switch. The compositor already reports every
change of presenting window, so the engine is disarmed on it and re-armed
against the new one, as it is across a Surface resize.
With the session activity resizable the fold works — with frame generation off.
With an engine on the picture still came back stretched: its ring, effects
chain and multi-present pacing were built against the old extent. The engine is
now disarmed before the window is rebound and re-armed once the new swapchain
has settled, so everything on its side is built again for the new size.
It was declared non-resizable. On a foldable that means the system keeps a
fixed-shape window across the fold and stretches it onto the other panel — the
app never receives a size change, so the compositor's re-fit, the swapchain
rebind and the letterboxing all had nothing to react to. Resizable, the window
follows the panel and the picture keeps its shape with bars.
A foldable opening or closing resizes the Surface rather than recreating it.
The compositor's swapchain, built for the old size, kept presenting, and the
system scaled those buffers onto the new window — on a Fold a 16:9 picture
came back stretched to the square panel, the same width and 2.09x the height.
Rebinding the window on a size change rebuilds the swapchain at the new size,
and the fit mode letterboxes as it should.
Panels and densities vary too much for fixed dp: a 7" 1080p handheld in
landscape has barely 400dp of height and the column did not fit it, with the
logo under the status bar and the credits off the bottom. The column is now
designed at one size and scaled by the height available — logo, text, buttons
and spacing shrink together until it fits, down to 55% — with scrolling as the
last resort and a width cap so the buttons never span a wide panel.
gamescope's display is sized once when the session starts and cannot change,
but the size was recomputed on every surfaceCreated. A foldable recreates the
Surface on the other panel, so a session started on the 21:9 outer screen and
opened onto the square inner one told the compositor its 21:9 buffer was 16:9
and the picture came back squashed sideways. While a session runs its size is
kept; the compositor letterboxes it onto whatever panel is current.
Also the Compose build: BoxWithConstraints' maxWidth read outside its scope.
The main screen, the loading overlay, the HUD line, the drawer and the three
dialogs are composables now, driven by state the activities own. The session's
SurfaceView and the on-screen pad stay Views underneath a single Compose layer,
which is what they are in any Compose app: a compositor surface and a canvas,
not menus. The XML layouts are gone; the activities extend ComponentActivity
and Back goes through its dispatcher.
gamescope's display cannot change size once the session is up, so a foldable
that starts on one panel and opens onto the other can only be letterboxed
differently, never re-shaped. A fixed 16:9 sits with modest bars on either
panel; the panel's own shape fits one and leaves a strip on the other. A device
with a hinge sensor defaults to 16:9; the drawer toggles it for the next
session.
A foldable's inner panel is nearly square, and the session sized its output to
match it. A game given a square display draws for the frame it was made for
and cuts the sides off itself — FlatOut on a Fold lost the edges of its own
menus. The output now keeps the panel's aspect only above 16:9 (a phone's 20:9
is fine), the compositor's scale mode is set to fit rather than its stretch
default so the picture keeps its proportions with bars, and touch is mapped
through the fitted rectangle instead of the whole view.
The engine cannot run without the DLL, so a pickable LSFG row only produced a
setting that quietly did nothing. The rows are dimmed and unselectable until
the DLL is in the runtime's Steam library, and say what to install.
Back used to background the app outright. It now opens a slim panel on the
right with the switches a player reaches for mid-game — the performance HUD,
the frame-generation picker (shared with the main screen), the on-screen
controls' mode (auto / always / never, replacing the file-only override) —
and the two things that leave the session: send it to the background, or stop
it. Back again, or a tap outside, closes it.
Whether frame generation is doing anything was only knowable from a log. The
top-right line counts the game's own frames from the compositor's per-frame
callback, and while an engine generates it shows the engine's telemetry beside
it — "LSFG 2× 60 → 118 fps" — or says the engine is starting, unsupported or
failed. Touches pass through it. steamdeck-no-hud in Downloads hides it.
The compositor was built with its frame-generation leg stubbed out. Bannerlator's
two engines and the DXBC translator they need now ride along, so extra frames
are generated on gamescope's final output — any game, nothing changed in the
runtime. Win-FG needs nothing; LSFG takes its shader chain from the user's own
Lossless Scaling, which in this app is installed from the Steam client and read
straight out of the runtime's library. One picker on the main screen: Off, or an
engine at 2x/3x/4x, applied live on the next return to the session.
The compositor lives for the whole process, so the second Play after a session
had ended re-attached the Surface, started nothing, and left the loading panel
counting up over a dead session — it looked like a login that never came. The
service now starts whenever no session is running; the compositor's one-shot
first-frame notice is re-armed for the new session, or the panel would never
leave; and whatever the previous session left behind (gamescopereaper, the
session script, a dead client's helpers, the audio server) is killed first,
since it is our uid and still holds the socket the new one needs.
Between Play and the client's first frame there was a line of text in a corner
of a black screen, for minutes on a first run. The panel in the middle now
shows the runtime's milestone, the client's own download percentage lifted out
of its log (its bootstrap prints "Downloading update (a of b KB)", which the
milestones never reflected), a clock, and a rotating hint about what is
happening. It reads the log's tail only, twice a second; a session that has
already drawn a frame does not get the panel again.
On a landscape phone the install button sat under the navigation bar with no
way to reach it. The screen now scrolls, everything on it is smaller, and the
side gutter grows with the window width (24dp, 96dp from 600dp, 240dp from
960dp) so the buttons stay a sensible width on a wide panel instead of spanning
it. The logo is the icon clipped to a circle; shown raw, an adaptive icon is a
bare square.
It only checks the schemes an apk's own minSdk requires, so at 26 it reported
v1 as false while the JAR signature was sitting in META-INF the whole time.
The file check beside it says the same thing a second way.
A phone had no way to answer Big Picture: touch moved the pointer and nothing
pressed a button. The on-screen pad writes into the same rings the physical one
does, so the client sees a single Xbox pad whichever is in use and both work at
once. An InputManager listener takes the controls away when a controller
appears and brings them back when it goes; steamdeck-osc in Downloads pins it
either way. Virtual input devices no longer count as controllers — the system's
own synthetic devices would otherwise hide the controls with nothing to
replace them.
AGP drops JAR signing at minSdk 24 and above and the DSL flag does not move it,
release build or not. zipalign then apksigner with the three schemes named
explicitly is the only version of this that can be checked.
A release build fails on ExpiredTargetSdkVersion, which exists because Play
will not take an sdk that old. It is right about the rule and wrong about this
app: 29+ forbids executing a file from the app's own data directory, which is
the one thing this app exists to do.
AGP drops JAR signing from a debug build when minSdk is 24 or higher, whatever
the signing config says, so the check added with the testkey failed on its
first run. Nothing is minified in either variant; this only changes which one
CI hands over.
The session now belongs to a foreground service rather than the activity.
Android demotes a process as soon as its last visible activity goes and the
low-memory killer reaps the guest, so leaving Big Picture to answer a message
came back to a dead Steam; the service holds the process at perceptible
priority, a partial wake lock keeps the CPU from dropping the guest's threads,
and a high-performance WiFi lock keeps a backgrounded download from being
throttled to nothing. The notification is silent (IMPORTANCE_LOW), carries a
Stop action, and returns to the session when tapped. Back now puts the app away
and leaves Steam running; a swipe out of recents ends the session.
The compositor moved with it: it is process-wide, started once, and merely
swaps the Surface it presents into as the activity comes and goes.
Package is com.steamdeck.launcher, signed with the AOSP testkey (v1+v2+v3) so
every build installs over the last one, and CI asserts all three schemes.
The launcher icon is our own controller on a purple glow — the spirit of the
Steam tile the client entry uses, none of its geometry.
The repo is private, so each run is billed. Three caches now carry a rebuild:
Gradle's own through setup-gradle (dependencies, build cache, configuration
cache), the CMake/ninja tree keyed on the C sources and the NDK, and the two
cross-compiled preloads — the last of which also skips installing the cross
toolchain, since a run that changes only Kotlin has no reason to touch a C
compiler. Superseded pushes are cancelled, documentation pushes do not build,
and the apk is stored rather than recompressed.
The preload assertions moved from the cross binutils to readelf, which reads
any ELF whatever the host, so they keep running on a cache hit.
Credits name The412Banner and maxjivi05: the runtime under all of this is
Max's gamescope work from WinNative.
resources.displayMetrics is the window after the system bars and the cutout are
subtracted, so the first session on a 1080p device came up 1920x968 and
gamescope patched its EDID to suit. The panel's own bounds are what a client
should be told the display is.
Matches the exec path this was ported from; the guest command already starts
with env -i, so clearing the host side changed nothing except how far the code
sits from a configuration proven on a device.
Caught by the closure check the previous commit added, before a device had to
find it: the daemon would have failed to start and the session would have run
without audio.
libwayland-server.so links libffi, so loading the compositor threw
UnsatisfiedLinkError the moment Play started a session. The library was there,
its dependency was not — which is exactly the kind of gap a build cannot see
and a device finds immediately, so CI now walks every NEEDED of every .so we
package and fails on anything that resolves to neither the apk nor the
platform.
The session script redirects its own output into BL_LOG with exec, and a
redirection a non-interactive shell cannot open ends that shell — so a denied
storage permission would have killed the session rather than just losing the
log. Downloads is used once proven writable, files/logs otherwise.
Keyboard events now reach wl_keyboard, which is what the client's first-run
login needs on a device with a keyboard attached.
The compositor links libwayland-server.so as an imported target, so CMake never
copies it into the apk; it has to travel in jniLibs like the PulseAudio set.
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.