Every Electron AppImage hung without a window or crashed at its first page.
Four causes, each enough on its own:
- The session preload's hooks found the real functions with
dlsym(RTLD_NEXT). Before glibc 2.34 a thread's first dlsym callocs, and
Chromium's PartitionAlloc calls syscall() from inside its first malloc:
the allocator waited for itself. On older glibc (the x86 runtime's 2.31)
next.c now walks the libraries after the preload with no allocation and
picks each function's default version, as glibc 2.36 does; the old
lookup handed realpath() and posix_spawn() their compat versions, and
realpath(path, NULL) failed. arm64 keeps dlsym.
- libpci's sysfs backend exits when /sys/bus/pci/devices is listable but a
device's files are not (a RedMagic's PCIe root port), taking every
Chromium GPU process with it. An empty directory is bound over it on
devices where that happens.
- FEX_OUTPUTLOG=stderr made FEX log to its own duplicate of descriptor 2.
Chromium dup2()s its IPC socket onto that low number in each child it
starts, then lists /proc/self/fd, which FEX logs into the socket. Empty,
FEX writes to descriptor 2 itself.
- The tracer preload served every /proc/<pid>/status as a memfd, which is
open for writing. Chromium hands each renderer its status as a read-only
file and checks that it is one. Only a process's own status (pid, tid,
self, thread-self) is rewritten now.
Each game's Windows components now come from the list its own source
trusts, and the ones that list names turn on by themselves. The user's own
on/off switches always win; each automatic row says why it is on.
- Steam games: the Steamworks Shared redistributables Steam installs with
the game (appmanifest SharedDepots, else the client's appinfo.vdf).
Steam keeps its install scripts marked as already run.
- GOG: the build manifest's dependencies. Epic: the manifest's
prerequisite (Unreal's maps to VC++ and DirectX). Amazon: fuel.json
PostInstall. Older GOG/Epic installs get their list fetched once with the
saved sign-in; game files are never downloaded again.
- Custom games (the + button, Add all, migrated folders): one shared Steam
match per game, also used for artwork. An appid from the game's files
turns components on; a name-only match stays Recommended; no match falls
back to the folder scan.
- Steam app info for apps the client never saw comes from an anonymous
Steam PICS lookup (CM websocket, anonymous logon, product info), cached
per appid; the user's Steam sign-in is never used.
- Missing components download in the background on one low-priority
worker that pauses during sessions and waits on battery saver, heat or a
metered network unless the user asked. A game Steam installs during a
session is readied right away so it can be played without ending the
session; the launch waits up to 10 s only for a component still on its
way.
- Page: "On for this game" with reasons, a quiet "Checking what this game
needs..." only for games never worked out, rows that ease in, pieces that
need a download greyed out while offline. The intro names no other app.
- The folder scan skips Unity's *_Data/Managed (Mono, not .NET Framework).
Preload the FEX fault report only where x86 loaders look
droiddeck-fex put the x86 libfaultreport.so in LD_PRELOAD, which FEX's own
arm64 loader reads too - and FEXServer's, and every child FEX's - so each
x86 start printed "ERROR: ld.so: object '.../libfaultreport.so' from
LD_PRELOAD cannot be preloaded", and an i386 child added "wrong ELF class".
It is now listed in the x86 runtime's own /etc/ld.so.preload, which only
the loaders inside FEX read, as /usr/$LIB/droiddeck/libfaultreport.so.
faultreport.c builds for i386 too, so 32-bit programs get the report as
well instead of an error.
* Run programs, scripts and AppImages from the desktop
Windows programs open with the Proton Steam has chosen, and a Steam game's own
launch program is handed to Steam. x86-64 Linux programs and game scripts run
under FEX, and an exec the kernel refuses goes to droiddeck-open through
libblsession's binfmt.c. Programs on noexec shared storage pass KIO's checks; a
folder of Linux programs there, and sharun AppImages, are added by the app once
and run from the copy. Executable scripts run without KIO's prompt, mpv plays
on the software desktop, FEXServer's socket moves under XDG_RUNTIME_DIR, and
Proton runtimes no longer show up as games.
* Switch to Desktop from Big Picture without Deck mode
Big Picture's Switch to Desktop asks the client's SteamOS manager, which the
client only has when started as SteamOS (Deck mode). Without it the request
failed and the screen stayed on "Switching to Desktop..." for good. The live
compat helper now sees the request on its way to the client and hands it to
steamos-session-select, which opens the KDE desktop as Deck mode already did.
* Run x86 programs from shared storage where they lie
FEX asks for the guest's PROT_EXEC when it maps a program or library, which
the kernel refuses on noexec shared storage; FEX never runs those pages
itself, so libblsession maps them without it in FEX's processes. An x86
game, its libraries and its start script now run in place instead of being
copied in; only native code on noexec storage is still copied.
A Steam library's games are never copied, and a Linux game's launch program
opened from the desktop goes to Steam like a Windows one.
* Front end: draw the capsule wall without clipping it every frame
The tilted wall clipped each capsule to a rounded rect and recomposed its columns on every drift
step, and a rotated rounded clip is rasterised in software on the render threads: the idle main
page held about 1.7 cores. The art is now loaded at the capsule's size with its corners already
rounded, each column draws its capsules itself in one layer moved by the drift, and the drift
advances at most every 15 ms. The idle page takes about a third of a core.
* libfakeinput: let calls on descriptors it does not serve skip its lock
Every read, write, ioctl, close and poll of every process in a Proton session went through the
controller lock and the three map lookups, Wine's wineserver pipes and the game's files alike.
A bitmap of the descriptors the maps hold, set before one is handed out and cleared when it is
closed, lets everything else go straight to the kernel.
* Pad motion: ask the display for its rotation four times a second
The rotation was looked up for every sensor reading, every 2 ms, and cost more than the rest of
the reading; it is now refreshed every 250 ms, and the readings no longer allocate pairs.
* Session: keep the library passes out of a game's launch
The redist seeder stamped each prefix with the checksum of the whole set of names, which differs
between the session's pass and the game launch's (the game's own names are added), so every
launch re-appended the markers to every prefix in the library while the game loaded. Each
prefix's marker now lists the names it was given and only the missing ones are added.
The beat's seed and compatibility passes, and the Lossless Scaling ownership probe (which loads
steamclient.so), also wait while a wineserver runs: a launch rewrites the manifest and made them
due at exactly that moment. The game's own launch seeds its prefix regardless.
* Agent: keep the focus watcher's X connection open while nothing changes
The connect timeout stayed on the socket, so with no property change the read timed out, and the
watcher dropped the connection, reconnected and took a fresh snapshot every few seconds.
* Game environment: start Cyberpunk 2077 without REDlauncher
The prelauncher opens REDlauncher, a Qt WebEngine window that hands over to the game half a
minute later and then stays up beside it, well over a core busy for as long as the game runs; its
own --launcher-skip started the game twice. The game's executable is started directly instead.
* netif: answer Wine's network driver from a reading of the link
Mortal Kombat X has Wine's network driver enumerate every interface, address and route table
hundreds of times a second. Each enumeration read /etc/droiddeck-net several times, asked the
kernel for its interface dump again (a netlink socket bound under proot, refused each time) and
looked the real functions up with dlsym. The link file is now read at most once a second, a
refused dump is not asked for again within that second, and the real functions are looked up
once.
* Fast path: create memfds without a stop in proot
proot looks at a memfd's name only to refuse the few that are then executed or locked; every other
memfd_create is now issued from the trampoline. The session's /proc/net and status copies made
one per read, a stop each. Without memfd in the kernel the call still goes to proot's ashmem.
* Logs: run each redaction rule only on a line that could match it
A session's end copies some 70 MB of the Steam client's logs and puts every line through all
twenty-odd rules, most of a minute of a core on the phone after every session. Each rule now runs
only on a line holding what it cannot match without: its keyword, the five separators of a MAC,
the four dashes of a GUID, a long enough hex or token run, the learned account or address text.
Lines with non-ASCII text still meet every case-insensitive rule. Over 895,000 lines of real
client logs, with the device's accounts and addresses learned, the output is identical and the
pass takes a twentieth of the time.
The Windows components catalog, the desktop and emulator packages, the Linux runtime and the
Steam seed now live on the organisation's components repository, mirrored and built there by
CI; the app reads the catalogs from its main and the files from its releases. The personal
winlator-contents mirror keeps serving older app versions and Bannerlator.
Games folders saved before the settings section went away were still
registered as Steam libraries on Automatic, with no way left to remove
them. Once, at app start and before any scan, each game such a folder
listed becomes an entry of its own, keeping everything keyed by its folder
(chosen exe, shortcut appid, name, Start in, launch options, art,
removal); store installs and steamapps are skipped. The folders stay bound
at their old guest paths, so no shortcut changes, and the saved roots are
cleared. A Games folder stays a library only when Steam installed a game
into it; the others leave the library list, and the runtime forgets them.
The full list left out only the 'installed for this game' rows, never the recommendations, so
every recommended component appeared twice (DirectX 9 on Final Fantasy Resonance, both rows
switched on).
perf: only read the phone's motion sensors when Steam wants them
The gyro and accelerometer ran at 4 ms for the whole session, even in games without gyro, costing about 6% of a core. Steam already tells the Deck pad when it wants motion (setting 48), so libfakeinput now passes that on and PadMotion follows it, with a short delay to ride out Steam's brief toggles.
Use the bare hex value accepted by older NativeAOT runtimes and CoreCLR. Cover the default and shared and per-game overrides.
Co-authored-by: James Yeung <[email protected]>
* Controller: optional Select button that opens the Steam menu
* Controller: translate the Select-opens-Steam-menu option
* Controller: clearer wording for the Select-opens-Steam-menu option
* docs: evidence for the Select-opens-Steam-menu option
---------
Co-authored-by: xXJsonDeruloXx <[email protected]>
Fix JNI local reference leak in createSubprocess
The argv and envp loops took one local reference per element from
GetObjectArrayElement and never released it, so the references piled up
until the native call returned. ART aborts the process at 512 local
references ("local reference table overflow"), and both loops share the
table, so a long argument list plus a large environment could hit it.
Delete each element's local reference once its chars are copied and
released, after ReleaseStringUTFChars since the chars depend on the string.
Also release the cwd chars against cwd rather than cmd; the call passed
the wrong jstring.
Adds a host-side test that runs termux.c against a mock JNIEnv that counts
local references and checks release pairing. It needs a jni.h (JNI_INCLUDE,
JAVA_HOME or the NDK) and skips without one.
Fixes#507
Its files sit in a container no extractor opens and may not be hosted, so a recording cannot
source them; installing it means running the installer in the prefix, which no runtime here
does. With it gone the waiting section is empty.
Store and GDK folders name their game in MicrosoftGame.config or
appxmanifest.xml. Picking the top-level Unreal bootstrap instead failed
with "CreateProcess() returned 2" when the Store had renamed the
shipping exe.
Co-authored-by: xXJsonDeruloXx <[email protected]>
art2kmin.exe is Setup.exe + Data1.msi in a wrapper 7-Zip opens, so the MSI engine lays it out
like any other package; it was on the setup-program list by assumption.
With parallel task execution, verifyRuntimeInputs and stageSessionScripts could run at the
same time; main's build at 3d50e6b1 hashed droiddeck-netmanager mid-copy and failed with
'Missing or changed runtime input'. The check now runs after the staging tasks.
* Windows components: hide Vulkan RT, whose loader every Proton here ships
vulkan-1.dll is in lib/wine/x86_64-windows and i386-windows of Proton Experimental (ARM64),
GE-Proton and proton-cachyos alike, so the catalog's VulkanRT entry would only shadow it. It
joins the list the page leaves out.
* Windows components: the waiting section says what is really missing (a recording), not 'coming next'
* Windows components: leave out the IE8 security update, which patches a browser no prefix has
* Windows components: the waiting caption without an apostrophe the resource compiler rejects
The publish workflow's push trigger fired again on the branch's rebase and overwrote the
release asset with a freshly compressed copy, so main's first build failed the checksum and
fell back to the registry pull. The env file now pins the asset as published; the workflow is
dispatch-only, defaults to a new tag, and refuses to replace an existing asset unless asked.
* CI: host the Debian image the preloads build in on a release of this repo
The preloads step pulled debian:bullseye from a public mirror on every build whose preload
cache missed; the mirrors rate-limit the shared runner addresses and failed two builds in an
hour tonight. Debian 11 is frozen upstream, so the image is published once (workflow
ci-debian-image.yml, release ci-debian-bullseye-r1, pinned in tools/linuxfs/debian-image.env)
and the build downloads that file; only if that fails does it pull, with retries.
* CI: pin the published Debian image (ci-debian-bullseye-r1, 34 MB)
* CI: a new Debian image rebuilds the preloads (env file joins the cache key)
Proton Experimental (ARM64), GE-Proton and proton-cachyos all carry Malgun, MS Gothic, YaHei
and SimSun in share/fonts and Wine's PowerShell wrapper, so the catalog's cjkfonts and
powershell entries would only shadow what is already there. They join gecko and mono in the
list the page leaves out.
* Windows components: install from a recording of the installer's result
A catalog entry may name a 'snapshot': what its own setup program leaves in a prefix,
recorded once in Wine by winlator-contents' tools/snapshot/record.py (the files it places and
the registry values it writes, the self-registering DLLs' CLSID entries included). With one,
the setup program never runs here: the files come from the archive beside the recording, or
out of the vendor's installer by the paths the recording maps (small generated ones inline),
and the registry values go to registry.json for droiddeck-wincomponents to write at launch.
The steps a recording makes redundant are skipped; DLL overrides still apply. A component
with a recording counts as installable; its prerequisites must still be.
* droiddeck-msi-install --unpack: open archives nested inside an installer
QuickTime's cabinets and an NSIS pack's payloads sit inside the unpacked installer; they are
opened in place into '<name>.nested' folders, two levels deep, the same layout the component
recordings map their file sources to, so a recording's paths resolve on the device (QuickTime:
593 of 593).
* feat: Steam's touch controller as on-screen controls (beta)
* fix: Steam touch controls appear once the session makes the device; tidier default layout
* fix: Steam touch layout decoding, saved layouts and new sessions
* docs: Steam's touch controls
* feat: Steam's touch icons, labels and colours on the touch controls; pick and save them like Steam
* feat: Steam touch icon picker as in-session panels; layout colour; docs
* Give each physical controller its own player slot
Every controller fed the one exported pad, so two pads played as one.
The first controller keeps slot 0 (the Deck in a Steam session); further
ones get slots 1-3 as separate Xbox 360 pads, created on first input and
removed on disconnect. In Deck mode only the Steam client sees those
pads and hands them to games through Steam Input. Rumble follows the
slot it names.
* feat: Steam Link's touch menu, per-game input modes, action set editing, add/copy/paste, oval controls, paste control
* feat: Steam touch controls drawn as Steam Link draws them; Steam Link's touch menu and layout tray
* feat: Steam touch controls in DroidDeck's style, D-mark guide button, shake to hide
* refactor: Steam touch controls become an On-screen controls choice instead of a second dropdown
---------
Co-authored-by: maxjivi05 <[email protected]>
* perf: fast path resolves links, /proc and glue as proot does, and answers uname, isatty and passwd
The fast path handed back to proot whatever crossed a symlink, /proc or a directory proot
only shows as glue, which is most of what Steam and Wine ask. It now walks a path the way
proot's canonicalize() does, so those calls stay in-process:
- links are followed in guest terms and their text detranslated as detranslate_path() does,
including readlink into a buffer the kernel cut short and /proc's exe, cwd and fd links;
- a binding proot dropped at start is dropped here too, glue above a binding is ENOENT, and a
last component proot cannot stat is left to the kernel;
- pipe and socket dirfds are ENOTDIR, rmdir("dir/") removes dir, and "." and ".." go to proot;
- a blocking accept() with a connection already waiting no longer goes to proot.
Also answered: uname() and gethostname() (proot's utsname never changes), isatty() (glibc
2.43 asks with TCGETS2, which proot rewrites) and getpwnam/getpwuid when nsswitch.conf
sends passwd to files first and the entry is there (Xwayland asks for every client).
equiv.py, equiv2.py and equiv3.py match line for line with and without the fast path
(222, 507 and 924 lines). Per call on the RedMagic: realpath 240 us -> 1.0 us,
realpath /proc/self/fd/N 408 -> 9.9, getpwuid 41.5 -> 4.6, uname 28 -> 0.006.
* perf: LD_AUDIT module skips library lookups proot would fail
ld.so looks each library up along the game's search path, and Proton puts the game's
directory, Steam's runtime and video directories and its own library directories ahead of
/usr/lib: 72% of ld.so's opens in an Among Us launch failed, each a round trip to proot.
libblaudit.so answers la_objsearch() for a candidate proot would certainly fail with ENOENT
(its directory walked as canonicalize() walks it, the name or a directory on the way missing).
It has no libc and maps the fast path's trampoline itself. Each lookup's first candidate is
still opened, since a skipped one fails with ld.so's errno as it stands, and the default
directories are never skipped, so dlerror() reads the same. droiddeck-proton and the extra
wrapper add it to LD_AUDIT when the fast path is on.
* perf: droiddeck-proton checks a tool's components with one sha256sum
Each program the launcher starts pays a dozen traced library lookups under the game's search
path before it does anything. One sha256sum over every listed file replaces one per file;
the check itself is unchanged.
* perf: build a locale archive for C.UTF-8
Every program loads the locale LANG names before main(). glibc ships C.UTF-8 as a directory
it looks for once per category under two spellings, before reading gconv's module lists:
about 30 opens, each a stop in proot, in every process of the session. droiddeck-session
builds /usr/lib/locale/locale-archive (and gconv's cache) once per glibc package, so that is
one open.
localedef names a finished archive by hard link, which Android refuses; with
DROIDDECK_LINK_RENAME=1 libblsession turns a refused link into a rename that will not replace
an existing name.
* perf: library beat starts its helpers only when what they read changed
Every 15 s the session ran droiddeck-seed-redists (some forty programs under proot) and, when
a library directory had changed, steam-compatibility: a burst of thousands of proot stops each
quarter minute, game loads included. The seeder now runs when a library changed or a prefix
(or Proton's template for them) is unmarked, the compat pass when a manifest, depot, tool or
config.vdf changed, both checked from bash without starting anything, and each every five
minutes regardless. The seeder reads its stamp and the time without a subshell each.
* perf: fakeinput opens pad.log on its first line
Every process the library is loaded in opened the session's pad.log at start, a stop in proot
on shared storage, and most never write to it. The file is now opened when there is a line to
write.
* docs: the fast path as it ships, the audit module and measured launch times
* fix: a game the app launches after an idle spell comes to the front
The Steam client brings a game it launched forward only while it counts someone as present,
which it reads from gamescope's input counter. A game the app launched after four or more
minutes without a touch started, mapped its window, and stayed behind the library page until
the next input (device: Palworld, Stardew Valley, Fallout: New Vegas, Mortal Kombat X; the same
on main). The session now watches a requested launch, and when the game's window is focusable
but the interface keeps focus for two polls, counts one input on gamescope's counter, as a key
would. Nothing changes for a launch the client already brings forward.
Adding and managing your own (non-Steam) games moves out of the Steam
settings into the Games tab, and works like Steam's own non-Steam
Properties.
- A + beside "Games N" opens a compact in-app picker: the storage first,
then that storage's root, every folder, .exe files only. Picking an exe
adds the game with its name, art, Custom badge and Steam shortcut.
- "Add all games in this folder" adds every game folder inside at once.
A summary opens immediately and fills in as each game is found, with a
"?" where the exe was a close call and a red X to remove a game.
- An edit button on each Custom game opens its editor: Name, Target
(ranked exes, Other exe…), Start in, Launch options, the four pieces of
art with thumbnails, and Remove. Edits go through the same Steam
shortcut, so the appid never changes and nothing duplicates.
- Art chooser per piece: images in the game folder, Steam's official art,
SteamGridDB alternatives, your own image, or back to automatic.
- Finding the exe: up to five folders deep, every exe scored (name match,
Unreal -Shipping builds, bin folders and size up; launchers, crash
reporters, installers, redistributables and depth down).
- Real titles from the game's files (Steam manifest, GOG info, the exe's
version info), keeping the folder's spacing when both spell the same
title, underscores read as spaces.
- Art: the exact Steam appid when the game names one, strict name
matches otherwise, Steam's hashed asset paths for newer games,
SteamGridDB as a fallback (built-in key from the SGDB_API_KEY secret,
optional own key in Setup, sealed with the Keystore), and a cover made
from the exe's icon when nothing else has one.
- Removing a game is instant and keeps its files; adding it again brings
it back. Steam's shortcut list is rewritten once, in the background.
- The Games section is gone from Steam settings; folders added before
keep listing their games. Artwork switch and SteamGridDB key are in
Setup.
* refactor(ui): share End session's step-out as StepOut
* feat(ui): let PageFlood carry a tile's art
* feat(stores): bring the launcher's motion to the stores pages
* fix(stores): lay out the pane's rows in a column
* fix(stores): a game page no longer leaves the pad's focus on a dead card and crashes the next press
* perf(stores): GOG remembers its non-games and the library cache loads off the main thread
* fix(ui): a step-out's backdrop never takes the pad's focus
* feat(stores): modes step out of the store chip, each store in its own colour, search in the header
* feat(stores): sign-in pages in the Steam tab's style and an are-you-sure for sign out
* style(stores): drop the sign-in page's store eyebrow, which the title already names
* style(stores): the sign-in page is the store's name and Sign in
* fix(ui): the focus ring follows a control until it stops and meets one still rising into place
* feat(stores): the store a pad's focus is on shows without A, and a press never lands on where it sent focus
* perf(stores): keep chip motion responsive while libraries change
The stores' sign-ins (files/stores/<store>/credentials.json: access and refresh
tokens, account id and name) were plain JSON. Other apps could not read them, but
every program in the Linux session runs as the app and could. They are now sealed
with AES-256-GCM under a non-exportable AndroidKeyStore key (StrongBox when the
device has one), so the session only ever sees ciphertext.
- Format: a versioned envelope {v, alg, iv, ct}, written atomically and readable
only by the app. No new dependencies (javax.crypto and KeyStore).
- Migration: an existing plaintext file is sealed at the next start, checked by
decrypting it, then the plaintext is removed; users stay signed in.
- No Keystore on the device: the file stays plain and the log says so once per
start.
- A sealed file that can never be opened (tampered, key gone or invalidated,
broken envelope) is removed and that store asks for sign-in again. A transient
Keystore error keeps the file and retries.
Session folders, tool and installer logs and the stores' log now all live under the
app's private files/logs instead of Download/DroidDeck, and every existing "Share
logs" action bundles them into one zip that goes through a final scrub.
- Where: LinuxRuntime.logDir() = files/logs; sessions keep their dated folders and
30-session pruning, tools/ and stores/ beside them. The "Logs" setting keeps its
meaning. Game save backups stay in Download/DroidDeck/Saves.
- Migration: at first start the old session folders, tools/ and loose logs move from
Download/DroidDeck into files/logs (Saves untouched), and every moved folder is
scrubbed under the current rules.
- Scrubbing: one rule set (SecretScrub) for the stores' log and the zip. The scrub
marker carries the rules version, so a rules change re-scrubs every folder at the
next start (in the background, a few at a time). Steam login account names and
emails, account-name fields, OAuth codes, tokens in URLs and private LAN
addresses are removed; Windows exception codes and server addresses are kept.
- Share: one zip of the current session folder plus the last 7 days of stores/ and
tools/, written to cache/share and handed to the share sheet.
* Windows components: lay out installer .exe wrappers without running them
The components page could only take .msi packages apart; most catalog entries ship an
installer .exe, so VC++ runtimes, .NET Framework 4.0-4.8 and the .NET Core/5+ runtimes sat
under 'Need a Windows installer'. droiddeck-msi-install now opens those wrappers and lays
out the packages inside the same way:
- WiX bundles (Visual C++ 2012-2022, .NET Core/5+ desktop runtimes): the .wixburn section
names the containers, each a cabinet found by its header (signing moves them); payloads
get the names the bundle manifest lists, so each package finds its cab1.cab beside it.
ARM64 packages are skipped, x86 and x64 both laid out.
- Self-extracting cabinets (Visual C++ 2005-2010): cabextract, external cabinet beside the
package (vc_red.cab).
- Self-extracting 7-Zip archives (.NET Framework 4.0-4.8): 7-Zip, now bundled with the
msitools (Arch Linux ARM 7zip 26.04: 7z + its 7z.so plugin, licences beside the others).
The complete product (netfx_Full_*) wins over the 4.0 parts and the per-Windows copies.
- An .exe that is its own setup program (NSIS, InnoSetup) reports that it holds no package.
- --unpack opens an archive for the app: DirectX's redistributable for XACT's cabinet picks.
The app executes the remaining catalog steps offline: set_windows (HKCU\Software\Wine
Version), set_register_key, get_from_cab (through --unpack) and 'uninstall Wine Mono' (the
mscoree override beside it is what matters), and reads url/file_name the catalog nests under
'environment' on a few .NET entries. Setup-program installers stay in the waiting section
with a note that says what they are.
Verified on this branch's engine with msitools, cabextract and 7z: VC++ 2010 x64 (19 files,
61 values), VC++ 2022 x64 bundle (26 files, 66 values), .NET 4.0 (1053 files, 6056 values,
Framework64\v4.0.30319 with clr.dll and mscorlib.dll, NDP\v4 keys) and .NET 4.8 (1633
files, 522 GAC assemblies, 6495 values). Not yet run on a device.
* Windows components: the pressed row greys out and shows its own two-step progress bar
Pressing a switch now greys that row at once and runs a thin bar across it: the download
0..100, then the install 0..100, with a caption naming the file or the stage (a dependency's
name in front while that installs first). Turning a component off greys the row the same way
until the pick is saved. The page-wide bar at the bottom is gone; Done waits on the busy row.
droiddeck-msi-install reports how far along it is ('progress 37% placing files'): reading,
each cabinet, every 5% of files placed, the registry, done - and a wrapper maps each package
into its share, so a bundle's two packages run 10..55 and 55..100. The app parses the percent
and passes a typed Progress (component, stage, phase, percent) up to the editor.
* Windows components: the ones turned on sort to the top of the list
In 'All components' the picks for this game come first, in their usual alphabetical order
among themselves; turning one off returns it to its place. The row moves once the install has
finished and the pick is saved, not while its progress bar is running.
* Windows components: an 'Installed for this game' section under the recommendations
What is turned on for this game gets its own section between Recommended and All components,
alphabetical; the recommendations stay where they are (a recommended pick stays there). A
component in the section leaves All components, and goes back to its place when turned off.
LB/RB jump through the new section too.
A Stores section (rail, under Games; Setup › Stores gate) with each store's sign-in,
library, storefront shelves and a download queue. Each store has two tabs, Store and
Library, and Library filters to All or Installed; libraries open from a saved copy
and refresh in the background, with install sizes on every card. Games install to internal storage,
or a card chosen at Install time, and are registered in the Steam client as non-Steam
games, so they launch through Steam and droiddeck-proton from either the front end or
the client.
- Downloads: GOG, Epic and Amazon engines in Rust (app/src/main/rust/stores, built by
cargo-ndk, nothing compiled in the tree). One adaptive fetch core with a latency
brake, stalled-host handoff and every CDN a store offers. Per-stage progress,
pause/resume, two-step cancel that deletes, failed downloads kept for retry, a
foreground service with Wi-Fi and wake locks and notification controls.
- Games tab: store source chips (Steam, GOG, Epic, Amazon, Custom) and store art.
- Epic: per-launch exchange code from the app and from Steam's Play button,
"Epic sign-in (EOS)", "Launch offline", and a game's web links opened in Android's
browser for EOS consent. The EOS overlay crashed games and is left out.
- Cloud saves (GOG, Epic): pulled before launch, pushed after the game or the session
ends, no upload before a first download, conflicts never overwritten, backups both
ways.
- Logs: every store line redacted; the engine log goes to daily files.
* feat: replace the LXQt desktop with SteamOS-style KDE Plasma
* feat: leaving the KDE desktop logs out through Plasma and returns to gaming mode
* feat: DroidDeck logo wallpaper as the KDE desktop default
* feat: build the KDE wallpaper from the DroidDeck mark artwork
* ci: keep the built desktop package as a short-lived artifact for device testing
* fix: the desktop loading screen no longer promises Steam on the desktop
* docs: the output resize comments name the desktop's KWin
* fix: the desktop package verifies every package's Arch Linux ARM signature and never replaces a published tag
* fix: programs sent to the GPU from the desktop drop its software rendering overrides
* fix: an interrupted desktop install no longer counts as installed
* feat: pin the desktop package to its DroidDeck-Components release instead of the catalog
DirectAudio from its own release: driver set per Proton audio interface (CachyOS fix), the Steam client's own DirectAudio sink, binaries out of tree
Games
- Two driver sets ship from the pinned DirectAudio release (tools/directaudio/release.env =
directaudio-linux-v1.1.0, sha256-checked by tools/directaudio/fetch.sh in CI and
build_local.sh): linux-wine11 for Valve Proton 11 / Experimental / GE-Proton, and
linux-wine11-systhread for Proton-CachyOS, whose Wine carries the system-thread mmdevapi
interface. steam-compatibility picks by whether the Proton's own winepulse.so imports
PsCreateSystemThread, never by the version string (both say wine-11.0; the wrong set is
silence). No matching set: DirectAudio off, Proton's audio runs, logged.
- Proton-CachyOS's launcher pre-sets WINE_AUDIO_DRIVER=pulse,alsa and its mmdevapi reads that
before the registry (Valve's and GE's do not read it): the wrapper exports
WINE_AUDIO_DRIVER=directaudio,pulse as well as writing the registry. On CachyOS this also
makes DirectAudio engage on a game's first launch.
- A wineserver from an earlier Steam verb can save its in-memory registry over the appended
Audio value; the wrapper says so when one is live. Writing through it (wine reg add) and
creating a missing prefix first (proton run wineboot) were tried on the device and withdrawn:
a second Proton/Wine run inside Steam's launch sequence got launches aborted.
- Tests: both sets, no set, no prefix (34 pass).
Steam client audio
- DirectAudio = module-directaudio-native-sink, the game driver's AAudio engine inside the
PulseAudio daemon, one step from Android (adaptive buffer, two-burst floor, primed ring with
fades, reopen on route change, watchdog, limiter). Default. Shared output = the previous relay
route, kept. New pref clientAudioRoute; the old boolean is not read. If the native sink cannot
open its stream the daemon keeps running (.nofail) and ensureClientSink loads the plain AAudio
sink instead, logged to audio.log. Audio bundle stamp bumped.
Out of tree
- app/src/main/assets/directaudio/ is generated and ignored; tools/directaudio-relay/ and the
vendored module-directaudio-sink.c + da_relay_proto.h are gone; tools/aaudio-sink keeps only
module-aaudio-sink (the fallback). The release carries the relay and sink field fixes by
MaxsTechReview (#338), credited there and in its AUTHORS.
Device-proven on an AYANEO Pocket FIT: client sink 12 ms / 4 ms bursts, 0 underruns; FEZ on
cachyos-11.0-20261005-slr on DirectAudio (took pulse before); FEZ on Proton Experimental
unchanged; Curse of the Dead Gods on CachyOS from a cold prefix, DirectAudio on first launch.
Co-authored-by: The412Banner <[email protected]>
The compositor takes touch and pointer positions over its whole output and
removes the letterbox bars itself, but the session sent them already
measured from the picture's edge, so the bars were taken off twice. Any
session whose output is a different shape from the panel (16:9 on a 16:10
tablet or a foldable, a fixed resolution on a wide phone) had taps and the
touchpad pointer pulled away from the centre, by up to a bar's height at
the edges: on a 2400x1504 panel showing 1280x720, a tap near the top
reached the first row and one near the bottom landed 32 rows low. Stretch
to panel hid it because that output has no bars.
Direct touch, the touchpad and mouse pointer, and the second-screen pointer
now send positions over the whole view, so the compositor's own mapping
puts them on the pixel under the finger.
* fix: keep Steam's directory listings on FUSE libraries
Before it creates, moves or deletes a content file the Steam client lists
the whole destination directory about ten times to match names
case-insensitively. On Android's FUSE storage every listing goes through
the media provider, so reserving space grew quadratically with the number
of files: 33 minutes for Haunted Paws Demo (3,938 files) in a Games folder.
The client's listings under steamapps/ on FUSE are now kept while the
directory's stat is unchanged, for at most five seconds, and patched by
the client's own creates, renames and removals. Other changes fail the
stat check and the directory is read again. BL_NO_DIR_CACHE=1 turns it off.
Haunted Paws Demo into the internal shared Games folder: reserving space
33m08s -> 25-52s, committing 6m15s -> 19-30s, install 39m50s -> 1m12s-1m51s.
Internal storage is unchanged (5s reserve).
* fix: drop stale directory listings on coarse-timestamp volumes
NTFS reports whole-second directory times and a fixed size, so a change
made outside the client's own calls could leave a cached listing stale.
Listings now carry an inotify watch and are dropped on any event the
client's own patched changes don't account for. A forked child stops
caching, since it shares the parent's queue.
Covers rewinddir, telldir and seekdir on cached listings, and directories
opened by the fast-path preload's opendir, which goes through fdopendir.
Update strings.xml
Improvements to the Japanese Translation
I have rewritten the translation to make it sound more natural in Japanese.
Also, since “Loader” in “Decky Loader” is a proper noun, I believe it does not need to be translated.
Co-authored-by: xXJsonDeruloXx <[email protected]>
Translate the 103 missing strings (screen effects, scaling, Wine sync
backends, FEX for AppImages, Steam repair, device info) and fix existing
ones: inverted meaning in mode_suspend_note, content_add_roms and
wadb_connect_address, double space in the update/install buttons,
outdated AppImage/Proton hints, "1 приложений"-style counts, gender
agreement with substituted names, lowercase fragments that are joined
into sentences, and inconsistent terms (Setup, Developer options, Stop
session, Button mapping).
Co-authored-by: xXJsonDeruloXx <[email protected]>
* Support CJK text with the device's own fonts
The Linux runtime's rootfs ships DejaVu alone, which has no CJK
glyphs, so Chinese, Japanese and Korean text in Steam Big Picture and
on the LXQt desktop draws as tofu boxes (issues #118, #206, #249).
Bind the device's own CJK fonts - Android's world-readable Noto CJK
.ttc, or DroidSansFallback / Source Han - into the guest under
/usr/share/fonts/droiddeck-cjk, so nothing is added to the ~790 MB
runtime image and the coverage follows the device. A new
droiddeck-fonts script, run by droiddeck-session before any program
that queries fontconfig, links the default rules, writes local.conf
with DejaVu first and the CJK families after it, and rebuilds the
fontconfig cache once per set of font files. The desktop's own
fontconfig block moves there, so every mode gets it.
* Fix CJK and Thai: bind the device font directory, not one file at a time
The first cut bound each device font with its own proot -b host:guest. A
per-file binding into a plain directory is not applied here: the guest sees a
zero-byte placeholder where the font should be (stat size=0, fc-scan empty),
so Chinese stayed tofu even though the app had "bound" the font. A directory
binding is what the rest of the runtime relies on, so bind /system/fonts as a
whole under /usr/share/fonts/droiddeck-device. That also brings Thai, Arabic,
Devanagari and the emoji fonts, which the runtime did not have either.
Verified on the AYN Thor: the session's fontconfig probe reports
sans=DejaVu Sans, zh=Noto Sans CJK SC, th=Noto Serif Thai, and Steam Big
Picture in Simplified Chinese renders without tofu (screenshot under
docs/development/evidence/).
---------
Co-authored-by: xXJsonDeruloXx <[email protected]>
* i18n: complete Spanish
Spanish gets the strings added since it landed and the ones that are only a format or a brand name, so it has the same keys as English, in the same order. Each English term keeps one Spanish word across the file, and a few existing strings are reworded to read right with the values they are filled with.
* i18n: use compilación for build in Spanish
* i18n: translate new strings to Spanish
---------
Co-authored-by: Kurt Himebauch <[email protected]>
Co-authored-by: xXJsonDeruloXx <[email protected]>
* feat: run a guest agent in every session for commands, focus and the Steam client's DevTools
* feat: give the agent bridge game, input, UI and experiment commands behind a Setup toggle
* feat: droiddeckctl commands for the new bridge, a scenario runner and contract tests
* feat: run cleanup steps after every scenario run so a failure cannot leak into the next
* feat: restart a session that died between scenario runs with start --reuse
* fix: give droiddeckctl stop its own, longer wait for the session's logs
* fix: harden debugging tools access and output
Steam saves a shortcut's Properties to shortcuts.vdf as soon as they
change and keeps its appid. The session rewrote every added game's entry
from the app's list before and after each client run, keeping only the
launch options, so a Target or Start In set in Steam, a new name or icon,
the hidden and overlay switches, the last played time and Steam's sort
name were all put back at the next exit.
droiddeck-steam-shortcuts now merges instead: an entry keeps every field
Steam has, and the app's exe is written only where the app changed it.
What the app last asked for and what Steam then had are recorded beside
shortcuts.vdf (.droiddeck-shortcuts.json). A Target changed in Steam to a
file inside the game's folder becomes the exe chosen in Steam > Settings >
Games, and a later pick there replaces it in Steam, so both show the same
exe. The appid now stays with the folder, so changing the exe no longer
turns the game into a new one with a new prefix, playtime and launch
options. A game whose folder is missing is set aside with its settings
and comes back with them. The exit-time merge runs before Steam's logs
are copied.
With Second library on Automatic, every writable Games folder is now a
Steam library: it gets the same private prefix and tool binds as the card,
the app hands the session the list (BL_STEAM_LIBRARIES), and
droiddeck-steam-library registers, relabels and forgets entries from it.
Steam offers them in the install dialog as "<folder> (<volume>)", and
Proton mapping, esync, the redist seeder, desktop entries and the compat
beat read every registered library instead of only the card. Games folder
libraries stop at the fast path's bind budget.
Steam's preallocation on Android's FUSE storage either failed outright
(NTFS answers EPERM, so installs were cancelled as "not enough disk
space") or zero-filled the whole download first (exFAT, minutes for a
small game). After fallocate is refused, the Steam client's ftruncate
extension of its own staging files is reported as done; the writes that
follow fill the file. A 76 MB install to a USB SSD now preallocates
instantly instead of taking about 3.5 minutes.
A library that is not there is left out of the list and forgotten until
it returns, imports whose folder is missing are set aside rather than
offered as broken installs, and the front end refreshes when storage is
mounted or removed.
A "Linux system" entry in the file manager's Storage list (the rail and the
drive menu) opens the root of the Linux system the sessions run in - the
app's files/linuxfs, the desktop's / - so its files can be browsed, copied
in and out without the desktop. Shown once the runtime is installed; Back
stops at that root like a drive's, and the drive label reads Linux system
anywhere under it. The label is translated in all nine languages.
Steam ships no CJK font and the rootfs only has DejaVu, so a phone set to
Chinese, Japanese or Korean got Steam in that language with every character
drawn as a box. Bind Android's /system/fonts into the guest, index it again
whenever it changes, and prefer the matching Noto CJK face for each language.
Opening the manager at a folder (a game's files, its Proton prefix, the
saves folder, or the picker's remembered folder) ran openDrive() on the
start dir, which pinned the up/back floor to that folder: the toolbar
arrow was greyed out and Back closed the screen. The start dir now opens
with loadDirectory(), keeping the volume-root floor computed for it, and
folders in the app's private storage (internal Steam library, prefixes)
floor at the app's files dir instead of themselves.
Adds a ".." (Up one level) row at the top of the list and grid whenever
the current folder is above its floor, sharing one goUp() with the
toolbar arrow. Covers both screens built on FileManagerScreen: the File
Manager and the in-app file/folder picker. Strings for all 9 locales.
* l10n: move hard-coded UI text into string resources (#212)
About 650 user-facing strings were written in Kotlin and Java, so no
translation could reach them and the UI stayed half English in every
language. They now live in values/strings.xml.
- Dialogs, menus, status lines, toasts and errors shown to the user go
through getString/stringResource.
- Code that matched on English text now uses IDs: installer and loading
stages, Decky stages, storage labels and the Back button's order labels.
- Choice lists (upscalers, FPS limits, textures, themes, presets) carry
@StringRes labels instead of English.
- Robolectric tests read the app's resources (includeAndroidResources).
- Six strings nothing used any more are dropped.
Existing translations are untouched; the new strings show in English
until they are translated.
* Layouts: fit longer text in other languages, and small phones
Found by screenshotting every page, tab and menu in each language on a
phone, a tablet, a 640dp phone and a foldable.
- Rail labels shrink to fit instead of being cut off (FitText).
- Tab rows tighten when they overflow and scroll the chosen tab into view.
- Menus fade over a down arrow when more entries are below, and open
scrolled to the current choice.
- On a small phone the desktop card's button moves under its title,
emulator tiles go two across, the GPU support verdict wraps and the
Updates page stacks the channels under the status.
* l10n: translate every string into Spanish, Japanese, Korean and Chinese (#187)
Simplified Chinese, Traditional Chinese (Taiwan) and Traditional Chinese
(Hong Kong, reviewed for Hong Kong wording) are complete, as are Spanish,
Japanese and Korean: no missing keys and no placeholder mismatches.
* Language setting: follow the system, or a language picked in Setup; Steam uses it too (#187)
- Setup -> Launcher starts with a Language row: "System default (...)"
and each language by its own name. With nothing picked the app follows
the system language; a pick applies at once and stays until System
default is picked again.
- Steam keeps its language in ~/.steam/registry.vdf under both
Valve/Steam/language and Valve/Steamsteamglobal/language. Before each
start of the client the guest helper droiddeck-steam-language writes
both (and exportedsettings.json), and the client gets -language.
SteamLanguage maps the app's language to Steam's names: english,
spanish/latam, japanese, koreana, schinese, tchinese.
* l10n: translate the strings main added since, and Chinese review fixes
- Translate the 32 strings added on main since (Windows components,
device details, Force SSBS, native Steam sleep) into all six languages.
- zh-CN: busy labels say what is in progress again (正在取消, 正在导出
...), as the other languages do.
- zh-CN: "set up" is 配置, as Setup is; 设置 stays for Settings.
- zh-TW: proton_compatible_note says 前綴 rather than "prefix".
About 650 user-facing strings were written in Kotlin and Java, so no
translation could reach them and the UI stayed half English in every
language. They now live in values/strings.xml.
- Dialogs, menus, status lines, toasts and errors shown to the user go
through getString/stringResource.
- Code that matched on English text now uses IDs: installer and loading
stages, Decky stages, storage labels and the Back button's order labels.
- Choice lists (upscalers, FPS limits, textures, themes, presets) carry
@StringRes labels instead of English.
- Robolectric tests read the app's resources (includeAndroidResources).
- Six strings nothing used any more are dropped.
Existing translations are untouched; the new strings show in English
until they are translated.
* Build the DirectAudio relay from source
The relay helper shipped as a prebuilt libdirectaudiorelay.so with no source in the
repo. Its source (The412Banner/directaudio @ 3548a4a, the commit the bundled driver
names in version.txt) now lives in tools/directaudio-relay, unmodified, and CI and
build_local.sh compile it into jniLibs the way proot is built. It shares
da_relay_proto.h with the PulseAudio sink, which is byte-identical to its own copy.
* DirectAudio relay: prime before playing, one consumer, safe buffer floors
- The output stream started before the client had its ring, so every connection
opened with a run of short callbacks: six clicks per stream on a RedMagic, even idle.
The callback now plays silence until the ring holds its target, and only a real
underrun afterwards counts.
- An underrun ends the block with a short fade and waits for the ring to refill to
target before resuming with a fade in, instead of zero-padding every short callback.
Each callback spent starved grows the target, so one stall sizes it for the next.
With a producer freezing 60 ms every 2 s: 14 underruns before, 1 after.
- During a reopen the outgoing and incoming streams could both drain the ring. Only
the promoted stream reads it now; any other plays silence.
- The ring target starts at 40 ms instead of two bursts (8 ms) and keeps what it
learned across reopens.
- The device buffer is at least two bursts; 12 ms was a single burst on devices that
grant a 20 ms legacy stream.
* PulseAudio: the classic path plays through the native-rate AAudio sink
The classic path loaded the prebuilt 0.1.5 module, which opened AAudio at the daemon's
default 44.1 kHz: Android gave it a legacy stream (20 ms bursts) and resampled, after
PulseAudio had already resampled the 48 kHz clients down. Its data callback also waited
on the daemon's IO thread, and it started at two bursts and grew only after an audible
underrun. The app's own module-aaudio-sink, built by CI into the same bundle, now
carries that path:
- The stream opens at the device's rate (48 kHz) with the buffer at two bursts or
30 ms, whichever is more, still growing a burst per underrun.
- A write that times out part way keeps the rest for the next write instead of
dropping it.
- A stream reopened after a route change asks for the rate the first one got, so the
sink keeps its format instead of unloading.
* Audio: DirectAudio sink at audio priority, deeper client buffers, no decay probing
- module-directaudio-sink's IO thread asks for nice -16 like module-aaudio-sink's, so
the client's producer keeps up under game load.
- PULSE_LATENCY_MSEC=60 for the session: libpulse clients under FEX (the Steam
client, winepulse when DirectAudio is off) keep enough queued to ride out a late
wakeup.
- BANNER_AUDIO_DIRECT_DECAY=0: the driver's decay shrinks the device buffer every
10 s of quiet until it underruns again, a click by design.
Both sit ahead of Downloads/droiddeck-env and per-game environment, which still win.
* Audio: drop the session-wide PULSE_LATENCY_MSEC
On an AYN Thor it took the Steam client's stream to 110 ms (60 ms tlength on top of the
sink's 40 ms) with no underruns to show for it, and the delay was audible. Clients pick
their own buffers again; the per-game option still sets it.
* PulseAudio sinks: wait in the poll loop, never in a blocking call
Clients' audio reaches a sink as messages its IO thread handles in pa_rtpoll_run. The AAudio
sink spent each iteration inside a blocking AAudioStream_write for up to a burst, and the
DirectAudio sink inside a futex wait for up to a burst, so a client's data sat unread and its
stream ran dry: a Chromium-shaped client (44.1 kHz float, 30 ms tlength, 10 ms minreq) had
304 underflows in 30 s on the AAudio sink, heard as crackling in the Steam client. Bursts of
20 ms (a legacy stream, an AYN Thor) made it worst.
- module-aaudio-sink writes without blocking, renders only what the device has room for, and
comes back after half a burst. 0 underflows in 30 s, with and without 8 busy cores.
- module-directaudio-sink tops the ring up on the same half-burst timer instead of sleeping
on the relay's futex.
* DirectAudio relay: vendor the source the shipped binary was built from
The import took 3548a4a, the commit the bundled driver names in version.txt, but the relay
binary that shipped came from e4727e9 on the same branch (feat/linux-relay-mic): its strings
match a build of that source exactly. 3548a4a has no --mic-fifo, so with the microphone on the
relay printed its usage and exited, and DirectAudio played nothing. This is e4727e9 as
published; the fixes made on top of 3548a4a follow in the next commit.
* DirectAudio relay: prime before playing, one consumer, safe buffer floors (on e4727e9)
The same changes as ca5d39b, carried onto the e4727e9 source: playback waits until the ring
holds its target, an underrun fades out and re-primes, starved callbacks grow the target, only
the promoted stream drains the ring, the ring target starts at 40 ms and survives reopens, and
the device buffer is at least two bursts.
* Steam client audio: DirectAudio by default
A fresh install plays the Steam client through the DirectAudio relay. Choosing Classic in the
Steam cog is stored and kept; only an install that never chose falls back to DirectAudio.
* Windows components: install .msi packages (XNA, PhysX, GFWL, MSXML...) without running them
droiddeck-msi-install reads a Windows Installer package's tables with msiinfo and unpacks its
cabinets with cabextract, then lays out what Wine's msiexec would have installed: files at their
64-bit Windows paths (Program Files (x86), syswow64...), .NET assemblies in the GAC, side-by-side
assemblies in winsxs, and the registry values in the view the component writes through. Conditions,
features and the folder-setting custom actions are evaluated as on Windows 10 64-bit; what needs
the package's own code (DXSETUP, self-registration) is listed in the component's notes.
droiddeck-wincomponents puts a component's drive_c tree into the prefix (keeping the prefix's own
spelling of each folder) and writes its registry values into system.reg/user.reg before launch,
following Wine's link keys (Wow6432Node\Classes, CurrentControlSet...). Turning it off puts every
value back and takes an added PATH entry out again.
The app installs these from the Windows components menu like the DLL ones: it downloads the .msi
into the runtime and runs the installer there (GuestCommand). msitools 0.106 and cabextract 1.11
come from Arch Linux ARM, pinned in tools/msitools/packages.txt and hosted on the msitools-0.106-r1
release. wine-gecko stays hidden (Proton ships it) and wine-mono keeps waiting (uninstall step).
* Windows components: open with the top switch focused, by touch or by pad
The dialog's window starts in touch mode, where a switch cannot take focus, and the first switch
was asked once, two frames after the list loaded, only when a pad opened it - racing the dialog's
own Done focus. Now it always puts the window in pad mode, holds Done while the list loads and
then asks the top switch every frame until it has focus.
* Windows components: give msitools its own libraries; show a failed install on its row
msiinfo and cabextract sit with their libraries in /usr/local/lib/droiddeck-msitools, which the
loader does not search: every .msi install stopped at "libmsi-1.0.so.0: cannot open shared
object file". droiddeck-msi-install now runs them with that folder on LD_LIBRARY_PATH.
A failed install also says so under the component itself ("Could not install: ..."), not
only at the foot of the dialog, past the list.
---------
Co-authored-by: The412Banner <[email protected]>
* Fast path: hold every bind, so added games start in their own folder
libblfastpath kept the first 64 binds and dropped the rest without a word. A session has 87 (two
Games folders are the last two), so /root/Games/<folder>/... was mapped into the rootfs - the
empty mount point - instead of the bound storage. Wine resolves a new process's working directory
by those paths, found no drive for it and started every added game in C:\windows ("could not
find DOS drive for current working directory"): DiRT 3 did not mind, FEZ looks for
Content\Essentials.pak there and quits.
The library now holds 256, and turns itself off for the process when a bind does not fit or the
list runs past that, leaving the call to proot, which never maps it wrong. The app keeps the fast
path off for a session with more binds than that.
Verified on an AYANEO Pocket FIT: FEZ (added game) reaches its title screen; the warning is gone.
* Fast path: say in the session log when it stays off
---------
Co-authored-by: The412Banner <[email protected]>
* Put a game's Windows components into its Proton prefix at launch
droiddeck-wincomponents copies the DLLs of the components picked for a game
into compatdata/<appid>/pfx before Proton starts and hands their overrides to
WINEDLLOVERRIDES, so the registry is never touched. It re-checks every launch
(an older Proton deletes the files it once placed), takes a component's files
out again when it is turned off, and forgets Proton's config_info then so
Proton puts its own builtins back.
* Windows components for added games: picker, packages and their catalog
Settings > Games lists a Windows components row under each added game. Its
dialog switches components on per game; the first switch-on for any game
downloads the package (a catalog of its own beside desktop.json, installed
with the desktop package installer to /opt/droiddeck/wincomponents/<id>).
The picks are kept in the app and published to the runtime at every session
start for droiddeck-wincomponents to apply.
tools/wincomponents/package.py builds the packages: Bannerlator's eight
wincomponents archives re-rooted, and OpenAL from OpenAL Soft's release
(soft_oal.dll placed as OpenAL32.dll, 32- and 64-bit).
* Windows components: list the installed ones when the catalog cannot be reached
Offline the dialog only looked up ids it already knew - the catalog's and
the game's picks - so a component downloaded earlier never showed.
* Windows components: Bannerlator's list with Recommended, on the game's launch settings
The picker moves from Settings to a Windows components card in an added
game's launch settings. It offers Bannerlator's whole catalog
(components.json on winlator-contents): what the game's folder points at
first (Bannerlator's DependencyDetector, mapped to the DLL-copy twin where
there is one, oalinst -> oalinst_dll), then every component that installs
here, then the ones that need a Windows installer, greyed until DroidDeck can
run one.
A component installs by running its catalog steps into the runtime once:
archives downloaded (one download per archive for a batch), DLLs found by
name as Bannerlator does and copied to /opt/droiddeck/wincomponents/<name>,
overrides and bundled components written to component.json. The launch
script now takes catalog names (xaudio2.7), expands bundles (directmusic),
places files one level down (drivers/gm.dls) and keeps an override's own
mode (dsound=n). Drops the hand-made packages and their script.
* Windows components: Steam games too, and a pad that can drive the list
The card shows on a Steam title's launch settings as well, keyed by its
appid - the compatdata folder droiddeck-game-env reads - and Recommended
scans the game's install folder.
With a pad: Done takes focus while the catalog loads and the first
component once it is there (the list was not drawn yet when the dialog
asked for focus, so nothing had it); switches stay enabled during a
download and a press then is ignored (a disabled switch dropped the
pad's focus); LB/RB jump between Recommended, All, the installers and
Done.
* Redist markers from games' own install scripts; VC++ runtime as installed
The client still started a game's own _CommonRedist installer - its
script sits in the game's folder, often with another version in the
value name, and only Steamworks Shared's were read. On ARM64 Proton the
x64 VC++ 2015-2022 bundle then stops with "Microsoft Visual C++
2015-2022 Redistributable (arm64) is required" (reported on an AYN Odin 2
Portal Max).
- Read installscript*.vdf from every installed game in each library as
well; the list is cached until an appmanifest changes, since this runs
every quarter minute.
- Record the VC++ 2015-2022 runtime as installed where its installers
look (VisualStudio\14.0\VC\Runtimes\{x86,x64,arm64}, both registry
views), so any bundle - the client's or a game's own - finds it and
exits; Proton's msvcp140/vcruntime140 serve the game.
- Join the Wow6432Node path instead of sub(): mawk and gawk read the
replacement's backslashes differently (gawk, the runtime's, was right).
* Mark a game's install-script processes as run just before the client evaluates them
An Odin 3 bundle (0.3.1, Proton Experimental ARM64) shows the client
running its install-script evaluator on the first launch of three new
titles (409710, 2161700, 3404260) while their Steamworks Shared
redistributables (VC++ 2010/2012/2019/2022, DirectX) were still
downloading - 2161700 at 22:03:22, the shared app finished at 22:03:29 -
so the periodic seeder had nothing to mark yet and the installers ran.
The client writes the exact steps to legacycompat/evaluatorscript_<id>.vdf
and starts iscriptevaluator.exe through our launcher. droiddeck-game-env
now hands that script to droiddeck-seed-redists first, which marks every
process in it in the template and the prefixes before the evaluator
reads them.
The seeder's reader no longer assumes a depth: a block with a HasRunKey
is a process named by the key it opened under, wherever it sits.
* Windows components: keep a file that was not Proton's, put it back when turned off
A component's DLL replaced whatever had that name in the prefix, and
turning it off deleted ours, so a file a real installer (or the user) had
put there was gone for good. Before replacing a file that is neither
Proton's own (a symlink into Proton, or a Wine builtin/placeholder DLL)
nor an earlier copy of ours, keep it under
compatdata/<id>/.droiddeck-wincomponents-backup and move it back when the
component is turned off. The launch note says how many went back.
* Windows components: recommend what Steam installs, say what the game already has
- Steam titles: the appmanifest's SharedDepots (Steamworks Shared, app
228980) list the VC++ 2005-2022 and DirectX redists Steam installs with
the game; they live outside the game folder, so the folder scan never
saw them. They join Recommended as "Steam installs VC++ 2010 with this
game" (SteamRedists).
- Bannerlator's PrefixInstalledDetector, on compatdata/<id>/pfx:
OpenAL32, Mono, PhysX and Games for Windows Live that are already there
from elsewhere show "Already in this game". Files droiddeck-wincomponents
placed itself do not count.
- The game page passes the prefix and, for Steam titles, the appid.
* Windows components: friendly names
The list showed catalog keys (oalinst_dll, vcredist2010_dll, dotnet452,
d3dcompiler_47). Bannerlator's catalog has no display name and some of
its descriptions do not fit the entry, so WinComponentNames gives one:
families by pattern (Visual C++ <year>, .NET Framework x.y[.z], XAudio,
D3DCompiler, MSXML, Wine Mono, DirectMusic parts), the rest from a
table, the key itself for anything new. The key stays in the row's
detail for logs and bug reports; both lists sort by the name, and the
game page's card names the picks the same way.
---------
Co-authored-by: The412Banner <[email protected]>
* Force SSBS for a game's Wine: libssbs.so, opt in per game
Wine's arm64 signal code resumes threads from a Windows CONTEXT whose
Cpsr never carries PSTATE.SSBS, so every signal return leaves the
thread running with speculative store bypass disabled - slower on cores
such as Oryon. Bannerlator fixes it in its own Wine builds
(force_ssbs.diff, Sonicadvance1) and with an LD_PRELOAD adapter for
layers it cannot rebuild. DroidDeck's Protons (Valve, GE, CachyOS ARM64)
come prebuilt from Steam but run the same aarch64 Wine signal path, so
the adapter applies here: tools/linuxfs/ssbs/ssbs_adapter.c, the same
source built for glibc (+ sysv_signal), staged to
/usr/local/lib/libssbs.so.
droiddeck-game-env preloads it when the game's (or the shared)
environment sets DROIDDECK_FORCE_SSBS=1, and only into an ARM64 Proton.
Off unless asked for: the gain is unproven on Snapdragon 8 Gen 3.
For testing: SSBS_ADAPTER_OBSERVE=1 counts without changing anything
(the baseline) and SSBS_ADAPTER_DEBUG=1 logs the fixes; both also count
signals that ARRIVE while a thread runs without SSBS, which shows
whether anything besides signal returns clears it. Checked on an ARM64
Linux kernel: a handler that clears SSBS left 4 of 5 later signals
arriving with it clear; with the adapter, 0 of 5.
* Force SSBS: a switch under the FEX preset in Steam settings
Steam settings > Games gets "Force SSBS" right under the FEX preset, off
unless turned on. Like the preset it goes into the game environment's
defaults (DROIDDECK_FORCE_SSBS=1), published at once and read by
droiddeck-game-env on the next game launch, so a game's own environment
can still set it to 0.
Measured on the device with the adapter's counters (AYANEO Pocket FIT,
DiRT 3 on GE-Proton11-7-aarch64): observing only, 8192 of 8277 signals
found the thread running without SSBS; with the adapter, none did - it
keeps the bit set everywhere Wine clears it, as a rebuilt ntdll would.
The speed-up itself is reported on Oryon cores and was not measurable on
the FIT's 8 Gen 3, hence off by default.
---------
Co-authored-by: The412Banner <[email protected]>
* Device details on the GPU card, and the Adreno model when the kernel leaves it out
The Components page called an AYANEO Pocket FIT (Adreno 750) "Adreno"
and warned it was "Below tested hardware (Adreno 725 and newer)": its
kernel names the GPU "Adreno33v2", GpuInfo wanted three digits, and the
SoC model is blank. When KGSL's name has no model, take it from the
phone's Vulkan driver (once this process has asked it) or from the
platform code name (pineapple -> 750, kalama -> 740, sun -> 830, ...).
The GPU card now opens with a tap or A onto the device's details,
gathered once off the main thread (DeviceInfo): SoC (maker, model,
platform, soc0 id), GPU (model and where it came from, the kernel's
name, clock, load, temperature, support), CPU clusters, memory, system,
the phone's own Vulkan driver - device, API, driver version and build,
conformance, limits, the extensions DXVK and VKD3D lean on, Android's
certified version and level - and the drivers DroidDeck uses. A button
copies it all for a bug report; each section takes pad focus so the
d-pad walks the list.
Vulkan is asked through a new small native library (deviceinfo,
vkinfo.c) that opens the system loader with dlopen and keeps nothing
open.
* Device details: the chip's model and name, CPU clusters, GPU clock range
Ideas from ClusterTune (GPL-2.0, its approach, not its code), which
tunes clocks on AYN, Retroid and other handhelds:
- The SoC model is read where vendors put it - ro.soc.model,
ro.vendor.qti.soc_model, Android's SOC_MODEL, ro.fota.platform (unless
it is a branch name like "MSM_13.0") - since handhelds often leave
Android's blank (Pocket FIT: SG8350P).
- SocNames names it: model codes we are sure of (QCS8550 -> Snapdragon
8 Gen 2, SM8750 -> Snapdragon 8 Elite, ...), else the platform as a
family (pineapple -> "Snapdragon 8 Gen 3 family"). The GPU card's
subtitle shows it: "Snapdragon 8 Gen 3 family (SG8350P)".
- CPU clusters come from the cpufreq policies (the cores each covers,
min-max speed) rather than grouping cores by top speed.
- GPU clock range from KGSL's step list, or a devfreq GPU node on
non-Adreno chips (Mali), not only the top clock.
* Device details: drop Qualcomm's own "Driver Build:" label from the driver build line
---------
Co-authored-by: The412Banner <[email protected]>
* Create values-zh-rCN
values language of Simplified Chinese
* Delete app/src/main/res/values-zh-rCN
* Add Korean,Japanese and Tranitional Chinese (zh-TW,zh-HK,zh-MO) translations
* Delete app/src/main/res/values/values-ko/strings.xml
* Create strings.xml
Add korean translate
* Add French translate
* Add Russian translate
* Fix French translate error
* Add the fr,ko,ja,ru in locales_config.xml
Fix French bulid error
* Overlap #234 and #259
---------
Co-authored-by: Kurt Himebauch <[email protected]>
On Android 16 phones (Galaxy S25 SM-S931B, SM-S948B, RedMagic NP05J)
every Firefox and LibreWolf tab dies at once: Firefox seals the size of
the shared memory it hands a tab, adding the seal fails there, Firefox
only warns, and the tab refuses the unsealed memory ("IPDL protocol
error: Shared memory PlatformHandle is not safe to map") and exits on
signal 11. The window opens; no page ever loads.
MOZ_SHM_NO_SEALS=1 is Firefox's own switch to skip the seals
(ipc/glue/SharedMemoryPlatform_posix.cpp). Set it beside the sandbox
switches in all three places a browser starts from: the desktop session,
AppImages, and Flatpak sandboxes.
Co-authored-by: The412Banner <[email protected]>
A game whose resolution differs from the screen's (DiRT 3 at 1280x720)
fights --force-windows-fullscreen: every forced resize makes it rebuild
its swapchain, and the picture flickers between its own size and the
screen's - often from the first menu, before the player can reach the
game's resolution setting. It also drove the Xwayland dma-buf leak fixed
separately.
The switch stays in Settings > Games and the session drawer, now off
unless turned on; its hint says when to turn it on (a game coming back
small or in a corner after switching apps, e.g. FlatOut) and when to
leave it off (flicker). Users who already set it keep their choice. The
session script's fallback when the app passes nothing is off as well.
Co-authored-by: The412Banner <[email protected]>
Close Zink's emulated GEM handles on KGSL (Xwayland dma-buf leak)
The session preload hands out GEM handles on the KGSL render node by
duplicating the dma-buf descriptor, and released them only through
drmCloseBufferHandle. Zink gives handles back with
drmIoctl(DRM_IOCTL_GEM_CLOSE) instead, which KGSL refuses, so every
duplicated descriptor stayed open and kept its buffer alive.
Xwayland asks for a handle for each buffer it imports through gbm. With
"Force game windows fullscreen" fighting a game's own resolution
(DiRT 3 at 1280x720), the game recreates its swapchain many times a
second and Xwayland piled up ~2,000 old swapchain images (6+ GB) in
seconds until Android killed the session. A preload trace on the device
put 2,090 of 2,118 open dma-buf descriptors in Xwayland on
drmPrimeFDToHandle called from Zink.
Handle GEM_CLOSE on the KGSL node in drmIoctl, and check the three DRM
symbols are exported in CI.
Co-authored-by: The412Banner <[email protected]>
- #166: export the fake-input ring paths with the spelling the session tree is bound under
(Context.getFilesDir(), /data/user/0/...) instead of the canonical path. Where /data/user/0
resolves to /data/data (ColorOS 16) the guest has nothing bound at the canonical prefix, so
libfakeinput could not open the rings and every pad, physical and on-screen, was dead. The ring
path logic moves to FakeInputRingPaths so it can be unit-tested without the native library.
- #219: play a game's rumble on the physical controller that last drove the pad, when Android
exposes its motors (strong and weak separately on Android 12+), and fall back to the device's
vibrator for the on-screen pad or a controller without rumble. PadBridge records the active
controller; the on-screen pad taking over moves rumble back to the phone.
- #153: when a program takes a pointer lock (a mouse-look game through gamescope), capture a
physical mouse and send its raw deltas as relative motion, so the camera no longer stops where
Android's pointer meets the screen edge. Capture is released for the drawer, the PC keyboard,
picture in picture, focus loss and pause, and when the lock ends.
* Clarify display sizing, game fit and scaling controls
* Replace aspect and resolution controls with one dimension menu
* Organize Steam session settings into tabs
* Keep session resolution out of the live controls panel
* Group live session controls and add a conditional second-screen tab
* Refine session sliders, tab icons and contextual controls
* Simplify screen effects with presets and collapsed advanced controls
* Give in-session tab icons more spacing
* Center the loading cancel button glyph
A client stopped part way through updating itself can be left unable to
draw anything. Its files and our install stamp are still on disk, so the
installer never looked at it again and every session sat on "Waiting for
Steam" with nothing retried.
The session now adds a line to package/droiddeck-unconfirmed each time it
starts the client, and the app removes the file on the client's first
frame. After two starts in a row with nothing shown, the installer:
- drops Steam's update manifests, the stamp, .crash and steam.pid
- deletes package zips whose sha1 no longer matches their name
- downloads the client again over the old one
steamapps, config and userdata are left alone. An automatic repair runs
once per failure, until a start is seen again.
The Steam page's Client settings get a "Repair Steam client" item that
queues the same repair for the next start. The request is only cleared
once the download completes, so an interrupted repair is retried.
The client runs in Steam Deck mode, where a title's "Default" is not the
"0" mapping but Valve's own pick for it: the Steam Deck compatibility
recommended_runtime (proton-stable, a pinned Proton, or native for the
Steam Linux Runtime) or an app_mappings entry in the SteamPlay manifests.
A title the session had not mapped yet - not installed, or installed
while the client was running on a build without the live helper - showed
"Default (Proton Stable)" and launched with a Proton that cannot run
here.
Every playable Windows title in the appinfo cache with such a pick is now
mapped to the default tool up front, before the client starts and in the
live helper, the same way installed titles are. A runnable tool the user
picks for one of them is kept, and they follow a change of the default.
In the Valheim and PEAK runs esync used the least CPU per frame. fsync
stays available, and esync still hands a Proton build without a pack to
droiddeck-fsync until its pack is published. A sync someone already picked
is kept.
* Make the Compatible Proton ARM64 the default and label compatible builds
The CompatToolMapping "0" default always points at DroidDeck's Proton
ARM64 tool, and per-game picks of adopted GE/CachyOS builds are kept,
including when the shortcuts writer remaps.
The tool is named after the Proton build it runs, Proton Experimental
ARM64 (Compatible), or Proton 11.0 ARM64 (Compatible) once that newer build
is installed. Adopted builds carry the same Compatible mark, read from a
string resource the app stages into the runtime, and the Proton page
explains which builds have the prefix fixes.
The registrar is renamed steam-compatibility and the tool
droiddeck-proton-arm64. Existing installs migrate on the next session: the
old tool directory is removed, every mapping that named it moves to the new
tool, and the retired bannerlator-steam-compat script is removed from the
runtime. CI and local packaging include the renamed script.
* Drop the duplicate R import left by merging main
* Add userspace ntsync for Proton on Android
Android kernels have no /dev/ntsync and its app policy refuses futex_waitv,
so every Proton 11 build here fell back to wineserver for every wait.
libblsession now serves /dev/ntsync itself: wineserver opens a memfd region,
each sync object is a timerfd that carries its slot, and the kernel ntsync
ioctls are answered in shared memory with futexes, following the kernel
driver's wake order. Valve's Proton Experimental ARM64, GE-Proton and
proton-cachyos use it unchanged.
Locks record their owner so a killed process cannot wedge the others,
signals are held off inside each critical section, dead processes and
exiting threads are swept, and object lifetimes follow close, dup2, dup3,
close_range and received descriptors.
Fast sync is on by default under Performance > Session fixes, and
PROTON_NO_NTSYNC=1 turns it off per game.
* Add droiddeck-esync as the default sync for Proton on Android
Without a kernel ntsync driver, Proton 11 builds here fell back to
wineserver for every wait. droiddeck-esync brings eventfd and shared-memory
sync to the exact Proton build a game runs on: a pack holds that build's
ntdll.so and wineserver with the patch series in tools/droiddeck-esync
applied, and the guest runs Proton from a shadow tree that links the stock
tool and swaps in those two files. A wineserver from a pack still prefers
/dev/ntsync when a kernel driver or droiddeck-ntsync is there.
Packs are built in Valve's SDK image from each release's own wine source
(Proton Experimental ARM64 tags, GE-Proton and proton-cachyos releases); the
control build must reproduce the release binaries byte for byte, and a
signed index lists the packs. The APK bundles the packs pinned in
release.env, the app fetches newer ones, and Steam's compatibility tool
picks the pack for each Proton build, so new Proton releases get packs
without an app update.
The ntdll side reads, writes and polls its eventfds with raw system calls,
so the guest's LD_PRELOAD wrappers stay out of the sync path. The server
abandons a mutex once a thread killed by TerminateThread has really exited
and when its process dies, and a mutex's owner and recursion count change
in one atomic step, so a killed owner cannot touch the next owner's count.
droiddeck-esync is on by default and PROTON_NO_ESYNC=1 turns it off per
game; droiddeck-ntsync, the userspace ntsync, is now an opt-in toggle.
* Watch Valve's Proton releases and build droiddeck-esync packs for them
A scheduled workflow reads the Proton ARM64 builds Steam ships (anonymous
steamcmd app info) and the upstream source tags, compares them with the
previous watch state and starts the pack build for any build that has no
pack yet. The pack build applies the patch series with --fuzz=0 and runs a
smoke test of each new pack against the esstress suite before it is
published.
* Add droiddeck-fsync, keep Steam and the app on one Proton, fix DirectAudio
droiddeck-fsync: libblsession answers futex_waitv(2), which the app
sandbox refuses, in userspace, so Proton's own fsync runs on the stock
Proton files with no pack. Waiters register the words they wait on in a
table beside the fsync shared memory and sleep on a futex of their own;
FUTEX_WAKE on the fsync memory wakes the registered waiters. Slots carry
one owner word that the sweeper and the owner claim with a CAS, and the
mapping table is rebuilt under a lock held across the real munmap, so a
reused address or a recycled slot cannot lose a waiter. It stands in for
a Proton build without a pack (droiddeck-ntsync where fsync is off), takes
over for games with esync off, and the "droiddeck-fsync first" toggle
(BL_FSYNC_FIRST, also per game) puts it ahead of the packs. The games
Proton keeps off fsync and esync never get it. A launch into a prefix
whose wineserver is still running keeps that server's sync choice.
One Proton for Steam and the app: the Components page's Proton is the
default Steam uses, applied live through the client while Steam runs
(droiddeck-steam-compat) and at the next start otherwise; a choice made in
Steam's menu comes back to the app. Requests are ordered by a sequence that
never goes backwards, and the page no longer jumps back to the default
after a swap or restore.
DirectAudio: the wrapper wrote the driver key with its backslashes
stripped, so no game ever used DirectAudio. Wine 11 also loads a builtin
only from the prefix, so the driver is now linked into system32 and
syswow64 before "directaudio,pulse" is selected; mmdevapi falls back to
winepulse whenever DirectAudio cannot start.
* Pick esync, ntsync or fsync from the Steam settings' Games group
A three-way tab row above the FEX preset sets the sync Proton games use. It
writes the existing switches together (droiddeck-ntsync, droiddeck-fsync
first, droiddeck-esync), so the Performance toggles and the tabs always agree,
starts on esync and keeps the choice until it is changed again.
* Make droiddeck-fsync the default sync for Proton games
A fresh install, or one that never picked a sync, starts on fsync; a choice made
in the Steam settings' Games group still sticks. Games Proton keeps off fsync
still fall back to droiddeck-esync.
* Add wineserver to the Wine sync tabs and order them ntsync, fsync, esync, wineserver
wineserver turns every in-process sync off so games wait through wineserver;
picking any other tab turns sync back on. The tabs shrink to fit a narrow pane.
* Shorten the Wine sync description
* Answer openat2 and execveat for FEX in the session preload
FEX opens every file of its x86 rootfs with openat2(RESOLVE_IN_ROOT) and
only falls back to the host path on EXDEV. Under proot on Android openat2
answers ENOSYS, so each lookup of an x86 library silently went to the host
and missed. The exported syscall() now turns openat2 into openat once the
real call reports ENOSYS: RESOLVE_IN_ROOT paths are taken relative to the
root fd (FEX has already resolved the links inside the rootfs), and an
absolute path under RESOLVE_BENEATH fails with EXDEV as the kernel would.
execveat with an absolute path, or relative to AT_FDCWD, and without
AT_EMPTY_PATH or AT_SYMLINK_NOFOLLOW, is plain execve; it is sent as such
because proot refuses execveat with a directory fd, which FEX uses when it
re-executes itself for a child x86 program.
* Run x86 Linux games and programs through FEX
Steam: Linux-only titles that have no ARM64 build (from the client's
appinfo cache: type game, demo, application or beta) are mapped to a new
compatibility tool, droiddeck-fex ("Linux x86 (FEX)", linux to linux,
no required runtime). Valve's own "fex" tool needs pressure-vessel, which
cannot run under proot, so a mapping to it or to a Steam Linux Runtime is
replaced; any other pick the user made stays. Titles that also ship for
Windows stay on Proton. When such a title is installed and FEX or the
sniper runtime is missing, the client is asked to install them.
droiddeck-fex runs the program under the FEX that Steam installs (app
3127680), with Steam Linux Runtime 3.0 (sniper, app 1628350) as the x86
rootfs:
- the sniper platform is linked in place from its usr-mtree (relative
links only, files/usr -> .), once per runtime version;
- the x86_64 and i386 builds of libblsession and libfakeinput go into the
runtime's lib/<triplet>/droiddeck and its etc/ld.so.preload, mirroring
the session's own list, so x86 programs get the pads and the session
fixes (FEX passes their system calls straight to the kernel, so the
ARM64 preloads cannot reach them);
- Config.json sets RootFS and the GL and Vulkan thunks, so x86 games draw
with the device's Turnip and Mesa;
- the FEX preset and the game's own environment from the Steam settings
apply, and ARM64 entries are dropped from LD_PRELOAD/LD_LIBRARY_PATH.
It also links ~/.steam/sdk64 and sdk32 to the client's x86 steamclient
when the client has one and nothing is there yet.
Desktop: x86_64 and i386 AppImages can be imported now; they unpack
through FEX (or unsquashfs) and record their architecture. Scripts,
AppImages and programs started in a run session use FEX automatically
when they are x86, and each added app has a "Use FEX" Auto/On/Off choice
in its edit dialog. The launchers' "still running" checks also look at
FEX processes, whose exe is FEX rather than the program. A GitHub release
without an ARM64 AppImage falls back to its x86_64 one. Decky's x86
loader starts through droiddeck-fex instead of the fex-mesa rootfs path,
which no runtime ships.
The x86 preloads are built against glibc 2.31 (Debian 11) so they load
in sniper: tools/linuxfs/build-x86-preloads.sh, run in CI and by
build_local.sh. fakeinput's stat hooks fall back to __xstat on a glibc
without a stat export, and mmap64 takes off64_t for the i386 build.
* Keep a FEX unixlib from outliving its DLL
A FEX package or original bundle without the aarch64-unix helpers left
the previous version's libarm64ecfex.so / libwow64fex.so in the Proton,
so the new DLLs loaded a unixlib built for another FEX. A swap now removes
the helpers the package does not carry; FEX then runs DLL-only, which it
already falls back to when Wine cannot load the unixlib.
* Fix the Wine sync choices an audit found
- wineserver tab: it passed Proton's environment through, so a build with
esync built in (GE, cachyos) still ran esync and a kernel ntsync would
still be used. It now sets PROTON_NO_ESYNC, PROTON_NO_FSYNC and
PROTON_NO_NTSYNC (unless the game's own environment sets them).
- ntsync tab: a Proton whose wineserver has no /dev/ntsync client got
wineserver-only sync although fsync would work. The hash cache records
whether the wineserver has one; without it the launch goes to
droiddeck-fsync, and no stand-in or pack turns droiddeck-ntsync on.
- A build with esync built in returned before the per-game fallbacks, so
a game that turns esync off ran with no in-process sync. It now gets
droiddeck-fsync, or droiddeck-ntsync where fsync is off too.
- When a pack's proton cannot start and the stock one is used, or the
selection fails, the prefix's record now says stock, so a second
launch into the same running prefix no longer picks the pack's ntdll.
- droiddeck-fsync: its mutex is held across fork and re-made in the
child, a closed or replaced shm fd is forgotten (close, dup2, dup3,
close_range, closefrom), and a waiter woken while it was leaving on a
timeout or signal reports the wake instead of losing it.
- BL_SYNC_MIN_NOFILE applies per game to every verb, debug logs carry
prefixes.json, the device report names the chosen sync instead of the
three raw switches, and PATCHES.md and the proot notes describe the
selector, the fsync default and droiddeck-fsync's futex_waitv.
* Name the runtime's scripts and files droiddeck, migrating old installs
The guest scripts are droiddeck-* now (droiddeck-session and the other 19),
as are the files the app and scripts generate (etc/droiddeck, the network
state files, the components dir, the Steam install stamp, the redist marker,
the extra-Proton wrapper and backups) and the compositor's own names
(libdroiddeckwayland, droiddeck_* sources, DROIDDECK_* switches, the
wl_output and seat names).
Installed runtimes carry on: LegacyNames moves etc/bannerlator and
share/bannerlator over and leaves relative links at the old paths before
every proot command, turns any old script into a link to its new name,
moves the generated files and points menu entries at the new scripts.
The session scripts and steam-compatibility move their markers on first
use, the components dir's desired.tsv is rewritten with it, and the
runtime counts as installed under either session script.
Kept: the banner_ahb_v1/banner_desktop_v1 wire names and BANNER_AUDIO_DIRECT
(read by prebuilt Turnip and Wine builds), Banners-Turnip and other
third-party names, BL_* switches, libblsession, and credits to Bannerlator.
* Unpack x86 AppImages without FEX, then ask Steam for FEX
Importing an x86 AppImage failed when FEX was not installed yet ("needs FEX
to unpack and no unsquashfs is installed"). AppImages now unpack with
uruntime (VHSgunzo, MIT, v0.8.1 aarch64, pinned by sha256 and shipped
unmodified with its licence), which reads SquashFS and DwarFS images of any
architecture natively. FEX's own --appimage-extract and unsquashfs stay as
fallbacks, and an ARM64 image whose own runtime cannot unpack it goes the
same way.
An x86 import with FEX missing records that FEX is wanted, so the next
Steam session requests FEX and the Steam Linux Runtime, and the program's
page says FEX is not set up yet and how to get it.
* Run Vulkan Qt Quick AppImages on Wayland
OpenNOW (Qt 6.8, Qt Quick on Vulkan) closed within two seconds in a run
mode. The session sends toolkits to gamescope's Xwayland, and Qt's xcb
backend only presents with Vulkan when Mesa's X11 WSI does, which needs
DRI3; Xwayland on KGSL has none, so Qt found no queue that can present and
aborted ("Failed to create RHI").
An AppImage that bundles Qt Quick, the Vulkan loader and Qt's Wayland
plugin now runs on gamescope's Wayland socket (the desktop's, outside
gamescope), where Turnip presents as it does for games, with Qt's xcb
backend as the fallback. SDL_VIDEODRIVER and XDG_SESSION_TYPE follow, so
helpers that pick their window system from them (OpenNOW's streamer) agree
with Qt. Everything else stays on X11; a qt file in the app's directory
(wayland or xcb) overrides the choice.
* Keep gamescope's WSI layer away from Qt apps on its Wayland socket
OpenNOW still closed at once: its FFmpeg (the Raspberry Pi fork, libavutil
61) creates its Vulkan queues internally synchronized whenever the driver
offers VK_KHR_internally_synchronized_queues, which DD-Turnip does. The
gamescope WSI layer registers every queue of a new device through
vkGetDeviceQueue, which only returns queues created without flags, so it
got VK_NULL_HANDLE and vkroots aborted on assert(obj) before Qt opened a
window.
Programs that draw on gamescope's Wayland socket do not need the layer,
which only carries X11 windows past Xwayland, so it is turned off for
them. OpenNOW bundles no Vulkan loader (it reaches Vulkan through Qt and
dlopen), so a binary that uses QVulkanInstance counts as a Vulkan app too,
and a Qt Quick app left on X11 says why in the session log.
* Keep the end of the session log, and say when FEX is missing
The session sends its output through head -c into session.log. head
writes through stdio, so it held up to 4 KB that proot's --kill-on-exit
threw away when the guest ended: every log stopped mid-line at a 4096-byte
boundary, losing exactly the lines that say why a program quit (an x86
AppImage's "x86 Linux programs need FEX" among them). dd writes each read
as it comes and keeps the same cap.
A run mode always ends with status 0 (gamescope does not pass its child's
on), so a program that could not start just closed. When the log says FEX
is missing, the ended screen now says so and how to get it.
The app reads FEX's readiness from status.json, which only a launch or an
import wrote, so it still said "not set up" after Steam installed FEX.
droiddeck-fex status rewrites it, and a Steam session ends with it while
FEX is wanted. FEX is also looked for at the tool's root and under files/,
and a tool folder without it lists what it holds.
* Let FEX and its runtime install runnable on an SD card library
A Steam library on shared storage loses the executable bits, so native
tools are staged in private directories bound over it; only Proton and
SteamLinuxRuntime folders were. FEX (3127680) installed there could never
run, droiddeck-fex saw no FEX and kept asking Steam for it. FEX and the
sniper runtime (1628350) now get private directories from their first
download, whatever their install folder is called.
* Keep fsync off in the pack smoke test
A runner whose kernel allows futex_waitv started stock Proton with fsync,
so the server-side baseline never said what the test waits for and every
pack was waved through as untestable; the esync run would have measured
fsync too.
* Judge a pack against stock's own esstress failures
Stock Proton fails esstress's dup wait all under server-side sync as well
(a Wine bug, seen on the phone too), and any stock failure made the test
call the runner unusable. A pack now fails only when it crashes, hangs or
fails a check stock passes.
* Ask Steam for FEX and its runtime one at a time
Both steam://install requests were started together, and the running client
took only the first: FEX installed, the sniper runtime FEX needs never did.
Each request now waits for the client to take it before the next.
* Bundle the first published droiddeck-esync packs in the apk
* Log how an x86 program under FEX ends
FEX keeps its own errors quiet unless asked, so a program it could not run
left nothing in the session log. Its log now goes to stderr, and the AppImage
launcher records the program's exit status or the signal that ended it.
* Show the log a failed AppImage program wrote
Programs like Eden stop themselves on a failed check and say why only in
their own log under the home folder. When the program ends badly, the end of
any log it wrote during the run is copied into the session log.
* Report where an x86 program under FEX hit an illegal instruction
A tiny x86_64 preload, built without libc, catches SIGILL and writes the
address, the instruction bytes and the mapping they belong to before the
program dies as it would have. droiddeck-fex preloads it into x86_64 programs.
* Load the fault reporter from the x86 runtime
Programs under FEX look libraries up in the x86 runtime, so the reporter is
copied there with the other preloads and named by its path inside it.
* Hand FEX its x86 runtime and thunks through the environment
Valve's FEX runs portable and reads its config beside itself, so the
Config.json droiddeck-fex writes was not seen: programs ran without the
runtime. The runtime, thunk libraries and thunk list now also go in FEX's
own environment variables, and each launch says what it was given.
* Use the sync pack built for the installed Proton release
Some Proton releases ship byte-identical ntdll.so and wineserver (Valve
11.0-2b/2c, CachyOS 0702/0703), so a pack built for a newer release
matched an older install exactly and the older install never fetched its
own pack. A pack from a newer release no longer counts as an exact match;
among matches the one built for the installed version wins, then rev.
The app ranks the pack a tool already uses the same way and only fetches
a pack that ranks higher, reads the release epoch from tools.tsv and
refetches when tools.tsv changes. A prefix whose server is still running
re-checks its recorded pack on waitforexitandrun and starts fresh when a
better one now applies.
* Keep published sync packs and revocations consistent
The publish step never replaces a pack that has an entry beside it; a
pack left without its entry by an interrupted run is replaced so the
build stops repeating. Discovery counts revocations the signed index does
not show yet, including superseded packs only the flavor releases list,
so the index is signed again. The release watch always builds every
flavor.
* Fetch the droiddeck-esync packs from DroidDeck-Components
The packs, the signed index and the apk bundle now live in the releases
of Droid-Deck/DroidDeck-Components, together with the pipeline that
builds them, so this repository keeps only the app and the guest code
that consumes them. The app's index and asset URLs point there,
release.env names the repository its bundle comes from, and the apk
builds drop bundled packs that the published index revokes.
* Stop the apk build when the published sync index cannot be read
* Ask Steam for one FEX dependency at a time
The client shows a confirmation dialog for each steam://install, and a
second request replaces the first dialog, so FEX was never installed
when the runtime was requested right after it. The next dependency is
now requested once the previous one has an app manifest.
* Use generic names in the AppImage test fixtures
* Give x86-64 apps under FEX the Vulkan thunk
Apps that bring their own glibc and Vulkan loader skipped FEX's thunk
overlay and loaded their bundled desktop Mesa drivers, which crash on the
phone's GPU during Vulkan setup. droiddeck-fex now adds an x86-64 audit
module when a program's bundled glibc and libstdc++ meet the thunk's
version needs. The module sends libvulkan.so.1 lookups to a nodelete stub
that needs FEX's thunk path. That keeps the thunk mapped when an app
unloads Vulkan and loads it again, since FEX keeps its first trampoline
links and the old addresses get reused.
The fault reporter now restores the default action before re-raising,
because FEX ignores SA_RESETHAND and the handler looped.
Builds esync packs for new Valve, GE and CachyOS Proton 11 releases on arm64
runners, smoke-tests them against stock with esstress, and on main publishes
each pack and a signed index as non-latest releases. A watcher checks Steam
and the GE/CachyOS releases every 20 minutes and starts a build for new ones.
The app side lands separately with feat/userspace-esync-ntsync.
* Fix runtime removal races and show removal progress
* Bootstrap Steam from the selected client channel
* Show runtime progress below its Setup row
* Always stage current session helpers and verify APK assets
* Initialize Steam UI scale once using the handheld default
The wall waited about a second and a half for the game list on every start: Steam's own
manifests are quick, but the added games' folder walk and art lookups are not. The last list
is now kept in the app's files (LibraryCache) and shown before the first frame, and the fresh
one replaces it in a single update. The wall's order is fixed (last played, then game ID), so
the same games always land in the same capsules and the refresh moves nothing. The added-games
scan on every resume also leaves the main thread, which it stalled for most of a second.
Backing out of the loading screen, closing the ended screen and the session ending on its own
used the slide-down card exit. Now, while the session is on screen, the blue spreads out of
the throbber's ball (the middle of the screen once a game is up) until it covers the screen,
and the front end opens on it and draws it back into the button the session came from, as it
already did after the drawer's Stop. A retry, a session closing off screen and animations off
still close at once; a timeout closes it should the flood never run.
All string resources get a Spanish translation except brand names and strings that are only a
format. The app declares English and Spanish in a locale config, so it is offered in Android's
per-app language setting and follows the system language otherwise. Short rail labels keep
to the rail's width (Configuración, Actualizar).
It was drawn with the current selection, so leaving it for another rail item turned the
outgoing page into a copy of the new one for a few frames, which then faded out before the
real page rose in: a flash and a dip moving from Games to Steam.
Opened with a pad, the editor and its variable dialog put focus on their first control; as
windows of their own they otherwise gave the pad nothing to move from. On/off variables pick
from their two values in the same dropdown as other choices instead of a switch. App dialogs
keep the page's focus ring out of sight behind them. The footer's Add variable button only
appears once the settings are loaded: drawn disabled first, it came up without its box.
The settings row gets a one-line hint and the same secondary button as every other row. The
editor moves from a stock Material alert, with a paragraph of explanation and a column of text
buttons, into the app's dialog: a profile chip with its menu, the variables in a panel with
each one's value as a chip whose menu changes, removes or restores it, and a footer to add a
variable, reset the profile or finish. Editing a variable uses the same dialog, its name and
value as chips or the app's text fields, and the variable's note in the footer.
LB, RB or a tab now start focus at the top of the new page instead of wherever it was last
left on that page; closing a menu or reopening the drawer still returns to where it was. On
Settings the top option is Android apps when DroidDeck is the Home app.
The ring fills in as it pinches to a dot, crosses as one round dot on a timed ease with no
stretch, and opens into the control as it gets there instead of settling as a circle first.
- A held direction runs the ring along as one piece and keeps its speed between steps;
restarting each edge from rest (and the trailing edge's delay) made it stutter.
- A jump of more than 360dp pinches the ring to a drop, carries it over and opens it onto
the new control instead of stretching a box across the screen.
- The ring keeps out of launch, stop and return floods (RingVeil) and appears on the
focused control once they are done.
- Leaving a page by moving along the rail no longer pulls focus into the new pane; only
backing out of a page hands focus to the control it was opened from.
- Selection is a radio dot or a tick, never a ring: the Updates channels and test builds,
the GPU modes and driver pairs, the Games list and suggested app icons lose their
persistent rings, and the GPU cards use the sliding ring for focus like everything else.
The cog's tile stretches over the pane on the same loose springs as Play, far edges first and
blobby in flight, while the page behind sinks; the settings rise in once it is covered. Back
draws the tile down into the cog on critically damped springs. The focus ring stays out of
it and appears on the focused control once the motion is done, replacing the ring riding the
edge of a stretching box.
The bloom is now the opening control's own rounded box stretching out to the pane, not a
circle. With a controller the focus ring rides its edge out to the pane's frame and springs
in onto the page's first control; on Back it rides the box back into the control, which
takes focus again. By touch, with no ring up, the bloom traces the same edge itself.
Closing a page used to leave focus on the page while it animated out, so nothing took it
back afterwards.
The page grows from the cog as a filled disc with an accent rim and folds back into it on
Back, while the pane under it sinks and returns. Pages also keep drawing while they leave
instead of falling back to the missing-page note for a frame.
Opening the settings no longer scans the added-games folders, reads the driver files and the
storage volumes on the main thread first: the page opens at once and those land while it
animates in.
Compose already scales its animation clock by the system animator duration
scale, so Motion.tw no longer multiplies durations a second time; Motion.ms
stays for coroutine delays. The drawer tabs, launch flood and the default-spec
expand/fade transitions now go through Motion, keeping their current speeds.
* Add your own apps to the Desktop page
An Add tile on Desktop takes a .sh script, an AppImage, a GitHub repo
(newest ARM64 AppImage release) or a Flatpak ID, with an optional name
and icon. Script folders are linked into the session, or copied when
they hold Linux programs (shared storage is noexec); folders outside
internal storage are bound at their own path.
Each app's page opens it under gamescope, edits its name and icon (with
icons suggested from its GitHub repo), checks a GitHub app for a newer
release and updates it in place, or removes it. Replaces the beta
AppImages section.
* Put Add at the end of the installable emulators
Installed emulators and added apps keep the top section, shown once there is one. Add closes the installable grid, or the installed one when nothing is left to install.
* Import DwarFS AppImages and their SVG icons
uruntime images built with sharun keep one program under several names
as hard links, which Android denies apps, so dwarfsextract aborted. The
extraction runs with proot's link2symlink, and each linked name becomes
a file of its own afterwards; their AppDir is followed through the
squashfs-root link.
An image's icon is a PNG of at least 128 px from the name its entry
gives, else its SVG (drawn with AndroidSVG), else the largest PNG, else
.DirIcon.
* Give AppImages the session's pads
sharun images start the app through their own ld.so, patched not to read
/etc/ld.so.preload, and drop LD_PRELOAD unless allowed, so the app never
loaded libfakeinput and saw no controller. The launcher now hands the
preload list over in LD_PRELOAD with SHARUN_ALLOW_LD_PRELOAD set.
Pad defaults also put Player 1 on the controller for yuzu-family
emulators, once, while it is still on the keyboard.
* Redesign the Add and Edit app dialogs
Source and appearance sit in two panels side by side, with a live
preview of the Desktop tile. A disabled primary button keeps an outline
so it still reads as a button.
The drawer's Display page gets the same Upscaler and Upscaler sharpness
choices as the session settings. A pick is saved and handed straight to
the compositor, so it takes effect on the next frame without restarting
the session.
The Wayland compositor already carries the X11 renderer's spatial
upscalers (effects_chain.c: FSR 1 EASU+RCAS, Snapdragon GSR and its
quality variant, NIS, RCAS sharpen) behind
WaylandCompositor.nativeSetUpscaler / nativeSetUpscaleSharpness, but
nothing calls them since the Winlator-era drawer was removed, so the
scene is always enlarged to the panel with a plain linear blit.
Add "Upscaler" and "Upscaler sharpness" to the session page's Display
group (off by default, 75%), stored in SessionPrefs and applied next to
nativeSetScaleMode when the session's surface is set up. The choice is
also listed in device.txt.
With frame generation on, a scene drawn for another window (Steam's
overlay) went into the engine as a game frame. Adaptive counted it as
source rate and planned against that (36 base for a game capped at 30),
and a fixed multiplier gave it present slots while game frames were
dropped (26 -> 51 at 2x). Such a change now waits for the game's next
frame.
The HUD's base rate is the game window's own frames, as Steam's overlay
counts them, instead of the compositor's scene rate.
Preload library answers stat/open/access/readlink in-process via a
trampoline page proot 0014 lets through seccomp. On by default with a
Performance toggle; also sets GLIBC_TUNABLES rseq=0 and malloc top_pad.
* fix: carry the installed Steam build into imported games so Steam stops re-verifying them
A second-library game downloaded by WinNative was registered with a bare
manifest (no buildid or InstalledDepots), so Steam treated it as build 0 and
verified all ~92 GB of Final Fantasy XV through proot. Reuse WinNative's
.winnative-steam manifest, or a completed .DepotDownloader/depot.config, so
Steam only fetches what changed since the recorded build.
* fix: give queued Steam fresh installs of imported games the recorded build
Steam rewrites a bare import manifest into a fresh install at build 0
before downloading anything. While nothing is downloaded or staged, the
WinNative build is still safe to carry over, so Steam skips the full
re-download (FF7 Remake was queued for 98 GB this way).
* fix: mount the game storage at /mnt/droiddeck-sd
The second library was bound at /mnt/bannerlator-sd, a name left from the
Bannerlator port, which is what Steam shows in its paths and errors. Bind
it at /mnt/droiddeck-sd, keep the old path as an alias for links and
prefixes that still name it, move Steam's library entry in place, re-point
import links, read ownership snapshots taken under the old path, and key
added-game shortcut ids by the old path so their prefixes stay put.
* fix: restart Steam once when owned added games are confirmed
On a fresh install the importer runs before any ownership snapshot exists,
so every added game became a non-Steam shortcut and stayed one until the
client happened to restart. Once the watcher confirms owned titles that are
not imported, it asks the session for one restart, while no game runs and
no download is in progress, and the importer turns them into Steam entries.
* feat: pick GPU drivers as matched pairs, set automatically for the device's GPU
* DD-Turnip Driver Support (#106)
---------
Co-authored-by: MaxsTechReview <[email protected]>
* perf: answer Turnip's zero-timeout KGSL timestamp polls without blocking
Turnip polls a timeline point with IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID
and a timeout of 0, which KGSL reads as wait forever; Mesa does it under
a lock on every signalling submit, so vkd3d-proton, DXVK, gamescope and
Zink ran in lock-step with the GPU. The session preload now answers
those polls from the context's retired timestamp and passes everything
else to the kernel. Bannerlator measured a D3D12 demo at 614 -> 4335 fps
with the same answer in its Vulkan adapter. BL_NO_KGSL_POLL_FIX=1 turns
it off.
* perf: frame limiter, live panel refresh, opt-in GPU clock pin, cheaper cursor sync
- A per-mode frame limit (Steam and desktop settings, off by default):
gamescope's -r, the compositor's buffer-release pacer, the display
layer's frame-rate vote and a panel mode the cap divides evenly, as
WinNative's per-shortcut limit does. BL_FPS was always 0 and the
pacer's limit was never set.
- The compositor follows the panel's refresh rate when it changes
mid-session instead of reporting the rate read at start. (WinNative.)
- Performance > GPU: hold the Adreno clock at its top through KGSL's
power-control property, no root (Bannerlator's PerfGpuTurbo). Off by
default; cleared when the session stops and at the next app start if
a killed process left it set.
- The cursor image is copied across JNI only when its serial changes,
not on every pointer move.
* perf: stop re-seeding every prefix every 5 s, and run the library helpers only on change
- bannerlator-seed-redists stamped its change check from a block that
carried the current time, so it never matched: every prefix had ~32
keys appended to system.reg on every beat. The stamp now comes from
the names; Proton's prefix template is seeded too (date kept), with
WinNative's built-in list until the client has the install scripts.
- The library beat runs the seeder every 15 s and steam-compat only
when a directory it reads has changed, or once a minute.
- The client-core pin reads /proc itself and runs taskset -a only for
new processes or ones that grew threads.
- The games watcher backs off after a failed ownership probe and does
not probe while a game runs.
- The Proton launcher runs the newest ARM64 depot by build number, and
PROTON_USE_PIPEWIRE defaults to 0. (WinNative.)
* perf: Mesa's database shader cache, and proot's tracer above the guest
- MESA_DISK_CACHE_DATABASE=1 for the session: the file cache opens a
file per lookup and opens and renames one per store, each a proot
stop; the database is opened once and used with pread/pwrite.
- proot's single tracer thread is raised to nice -6 (under the
compositor's -8) two seconds after it starts, so the first guest
process it forks keeps nice 0. setpriority() directly, not
Process.setThreadPriority, which would also move its cgroup.
* feat(lsfg): take Lossless Scaling from Steam by itself, import it once owned, rebuild on update
- Lossless.kt owns the DLL and its shader cache. The copy the Steam
client installs (runtime library or the card's) is taken by itself
and taken again when the client updates it (size/mtime stamp). The
DLL is copied in and built into a cache named after its SHA-256, so
a new DLL hands the compositor a new path; a build that fails keeps
the working cache. Builds are serialized.
- The Steam session asks the signed-in client once per session whether
the account owns app 993090 until the answer is yes, then records it
and the app stops asking (BL_LOSSLESS_OWNED). Once owned, the frame
generation menu offers "Import Lossless.dll…", on the main screen and
in the session drawer; an import stays until Steam's copy changes.
- The compositor restarts the LSFG engine when its cache path changes,
and a new engine selection clears an earlier start failure, so an
updated DLL or a retry no longer needs the app restarted.
- The HUD says when LSFG is waiting on Lossless Scaling instead of
"starting"; frame generation strings are resources.
- Removed LsfgNative's unused DLL variant and cache-match calls.
* Find Lossless.dll in added Games libraries too
* LSFG: 120, 90 and 60 fps targets alongside 2x-4x
The pacer's target-rate mode picks 0-3 generations per game frame. While it
is warm the input ring and shared chain follow every frame, so frames that
generate nothing don't leave the next one without history.
* Credit LSFG: Camille LaVey / Eden, ported to WinNative and DroidDeck by @maxjivi05
* LSFG: aim 0.1 fps under the target so output never passes the refresh rate
* feat: turn Deck mode on for new installs and add a performance overlay toggle
* feat: hide the session fps HUD where Deck mode's performance overlay runs
* feat: add an Updates page that installs Stable, Nightly or test builds in-app
* style: put update channels on the left and the build on one line
* style: make the update card read like a system updater
* feat: expand update notes, show the build beside the title, fit with button hints
* style: check for updates with the refresh icon Components uses
* feat: run Flatpak apps through a proot bwrap stand-in and add a Flathub store
* feat: give Flatpak apps the KGSL GPU, the controllers and a CPU LLVM can target
* feat: Flatpak games on the desktop, Store apps on the Desktop page, safer remove
* test: cover the sandbox planner and store helpers; docs: explain Flatpak on DroidDeck
* fix: keep controller focus through Store tabs and app pages; show progress per Flatpak step
* fix: let Chromium, Electron and Firefox Flatpaks start without their own sandboxes
* fix: fetch Flatpak's supporting libraries when the runtime lacks them
* fix: add Flathub without a download, log store commands, keep Electron apps on X11 under gamescope
* feat: import ARM64 AppImages from storage and run them from the Desktop page
* fix: report failed Flatpak installs as failures and deploy runtimes that ship a resolv.conf link
* fix: let Flatpak create its repo where Android denies hard links
* fix: point store commands at the current network's DNS
* fix: keep failed Flatpak extensions non-fatal, give Electron AppImages a bus, X11 and time to relaunch
* docs: AppImage import and what x86 apps would take
* feat: put the Flathub Store and AppImage import behind beta toggles in Setup
---------
Co-authored-by: MaxsTechReview <[email protected]>
The floppy disk beside the Steam cog and its Game saves page are gone. A game's
page now has Manage saves beside Proton prefix, a menu with Import saves (a
GameHub or Winlator zip, picked in the app's file picker; the saves there now are
backed up first), Export as GameHub zip, Export as Winlator zip (a folder picked
the same way) and Open save folder. The game's saves are looked for when the menu
opens. The Proton list and config.vdf reader only that page needed are removed.
A floppy disk beside the Steam page's cog opens a Game saves page, laid out like
Components: the Proton at the top (from the client's compatibility tools, with
bannerlator-proton-arm64 shown as Proton Experimental), the zip type beside it
(GameHub or Winlator, LB / RB turn it), and the games the client runs with that
Proton (CompatToolMapping, its default for the rest), each with Import and Export.
Import picks a zip in the app's file picker, backs up the game's steamuser profile
to Download/DroidDeck/Saves/backups, then unzips drive_c with any user folder but
Public renamed to steamuser, so GameHub and Winlator zips both land. Export finds
the save folders by name under the usual roots (Bannerlator's SaveLocator), or
takes the whole profile, and writes <Game>_<date>.zip to a folder picked the same
way, under steamuser (GameHub) or xuser (Winlator). Imports wait while a session
runs. Ported from Bannerlator's GameSaveBackup and SaveLocator.
* feat: stand in for /dev/uinput so Steam Input's virtual pads reach games
* feat: present the pad to the Steam client as a Steam Deck controller over hidraw
* docs: log the uinput stand-in and the Steam Deck controller
* feat: feed the handheld's gyro and accelerometer to the Steam Deck controller
* feat: let the Steam page choose between the Steam Deck controller and the Xbox 360 pad
* feat: Deck back grips and trackpads on the second screen
* fix: serialize ring teardown with its writers, fall back to the Xbox pad when the Deck cannot be set up, and read motion only for a Deck
* fix: keep the second screen's choice through sleep and bring it back on wake
The Steam menu is an overlay that takes input without changing gamescope's focus
window. A fullscreen wine game minimizes itself when it loses input, and gamescope
only takes a window out of iconic when the focus window changes, so after Resume
the game stayed minimized: a black screen with its caption in the top-left corner
(Titanfall 2 on GE-Proton 11, proven on device).
gamescope patch 0112 remembers the iconify request and puts the window back to
NormalState before input returns to it, then hands focus over again so wine
restores and activates it. The session script also asks gamescope to restore the
game once input comes back from the client (GAMESCOPE_RESTORE_FOCUS_WINDOW);
BL_RESTORE_ON_RESUME=0 turns that off.
* perf: cache pinned downloads, skip unchanged proot rebuilds, and wait out a Docker Desktop restart in the local build
* perf: skip xalia and gamescope realtime queues by default, add a realtime toggle, default the mic off
* fix: show the saved Steam Deck mode on the Steam page before Performance has been opened
The gamepad-cursor port queried the X pointer on every vblank, a
blocking round trip to Xwayland on the paint thread. With no cursor
image the position comes from wlserver as upstream; while the cursor is
hidden for inactivity X is asked every 50 ms. The realtime-queue comment
now says it is a no-op on KGSL Turnip.
The session's watchers slept with sleep(1) and read files with cat, an
exec each under proot; they now wait in bash itself. steam-compat
writes its tool, the adopted wrappers and config.vdf only when they
change, the games watcher probes ownership when its matches change or
every five minutes instead of every 30 s, and Proton no longer inherits
the client's steamrtarm64 library path.
A replaced buffer went straight back to a game with no frame limit, so a
MAILBOX/IMMEDIATE swapchain drew frames that were never shown; it now
comes back once per refresh as in WinNative. gamescope's -r and the
compositor read the rate of the mode the activity asked for rather than
the one the panel was still in, and the zero-copy layer votes it.
Sustained performance mode, which caps clocks on most power HALs, and
the ADPF session, which reported frame intervals as work, are gone.
fake_id0 (loaded by -i for Xwayland's set*id) now traces only the set*id
family and the xattr fixups when the ids given are proot's own, and the
seccomp filter traces ioctl only for the termios and FICLONE requests
proot rewrites. The faccessat2 and statx exit stops are dropped on
kernels that have the calls.
* fix: give the QAM chord a Guide lead that stretches with slow frames so A is not taken as a press
* fix: start the QAM Guide lead at 80 ms and stretch it only when frames are slow
* fix: set the QAM Guide lead minimum to 80 ms
* fix: use Android's 300 ms double-tap window for Back so the single-press action comes sooner
* fix: keep the 500 ms Back double-press window
* feat: pair wireless debugging from a notification for the child-process gate
* fix: pair over loopback, handle Android 12 and 14 child-process routes, use D logo for notifications
* fix: read the adb tls port directly before falling back to mdns
* fix: end pairing on the same notification so progress never lingers
On a foldable's inner panel (Galaxy Z Fold8: 2448x1848, ~4:3) gamescope
is started with the nested size equal to the output, so games are asked
for the panel's own shape. Many cannot do it and letterbox themselves,
and gamescope's scaler has nothing to stretch. "Stretch games to fill
the screen" only forces window size, so it does not help.
New Steam setting "Stretch 16:9 games to a narrower screen" (off by
default): bannerlator-session then gives games a 16:9 surface at the full
output height (-w) and adds -S stretch, so they render at a shape they
support and fill the panel with no bars and no crop. An output that is
already 16:9 or wider is left untouched. Passed as
BL_GAMESCOPE_STRETCH_16X9 and listed in device.txt.
0.2.0 hardcodes the proot utsname nodename to "DroidDeck". Steam sends
it as the machine name, so an install updated from an earlier build
(which reported Android's "localhost") shows up in Steam under a new
name, with the same machine-id and login. /etc/hostname cannot fix it
from inside: proot's -k override wins.
Add a "Host name" field (Performance > Session identity), stored in
SessionPrefs and passed to guestUtsname(). Only a valid RFC 1123 label
is saved; blank or invalid falls back to the old default, "DroidDeck",
so nothing changes for anyone who leaves it alone. The value is also
listed in device.txt.
DROIDDECK_CI_TOKEN never existed, so PR Release Publish skipped its
upload, and a personal token would expire and belong to one person. Both
workflows now mint a token of the org's DroidDeck CI Publisher app,
installed on DroidDeck-CI only with Contents: write (its ID in the
DROIDDECK_CI_APP_ID variable, its key in DROIDDECK_CI_APP_KEY). No
fallback: a main build without them fails its announce job.
Run artifacts need a GitHub sign-in to download. Each signed main build
is now also a prerelease main-<sha> of the public DroidDeck-CI (all
variants and their SHA-256s; the last 10 kept, PR releases untouched),
and the Discord post links its standard apk and the release. Without
DROIDDECK_CI_TOKEN the post links the run's artifacts as before.
A job after the signing of a main build posts to the Discord's builds
channel: the merged PR (or the commit), and a link to the run's signed
apks. The channel's webhook URL is the DISCORD_BUILDS_WEBHOOK secret;
without it the job posts nothing and passes.
A local build was signed with the testkey alone, which a release refuses
as an update ("signatures do not match"): testing on a device with a CI
build meant uninstalling it and losing its data first.
With an untracked .signing.env (RELEASE_KEYSTORE, RELEASE_STORE_PASSWORD,
RELEASE_KEY_ALIAS) in the checkout, or in the main checkout for a
worktree, build_local.sh finishes with tools/release/sign-apk.sh as CI's
main builds do, and the apk installs over a release. Without the file
nothing changes. DROIDDECK_SIGNING_ENV points at the file explicitly.
The desktop forced a software cursor (WLR_NO_HARDWARE_CURSORS=1) because
labwc's cursor never showed when handed to the app's compositor. With a
software cursor wlroots never scans out: every frame of a full-screen game
was composited by labwc first ("Direct scan-out disabled by software
cursor", 2284 times in one Adreno 825 report).
Why the cursor never showed: wlroots' Wayland backend attaches and commits
its cursor surface before wl_pointer.set_cursor gives it the cursor role,
and sets it again with nothing new committed each time the pointer enters.
The compositor released a role-less surface's buffer on commit, so the
cursor surface had no image by the time it became the cursor.
- A role-less surface's last buffer (cursor-sized only) is held until the
next commit; set_cursor publishes it, and a repeat set_cursor of the
surface already shown unhides its last image.
- A dma-buf cursor is read back after the client's implicit render fence,
as the zero-copy path waits for it.
- The desktop no longer forces a software cursor.
Device-tested on the AYN Thor (Adreno 740, 1920x1080): labwc's own cursor
is drawn, with its shapes (I-beam over a terminal); wlroots logs "Direct
scan-out enabled" for a full-screen glxgears. At the same 60 fps on screen
the GPU is 3.0 % busy against 4.6 % with the software cursor, both at
550 MHz.
main's build signed only the standard package, so the pubg, antutu and
ludashi variants existed only after a manual Release APKs run. The build job
now reads the variant list from tools/release/variants.txt, as release.yml
does, and main's signing job signs all of them.
KGSL's files and thermal zone names differ between Snapdragon generations.
Each value now takes the first source that exists, in the order GameNative
and the Winlator forks read them: GPU load from gpu_busy_percentage or
devfreq/gpu_load, GPU clock from devfreq/cur_freq or gpuclk, GPU temperature
from KGSL's temp, devfreq/temp or a zone whose sensor name has "gpu". CPU
zones are any whose name has "cpu" and not "gpu" (cpuss-0, cpu0-silver-usr,
apc1-cpu0-usr, ...), matched without regard to case.
mangoapp's VRAM figure on a Qualcomm driver comes only from the gpu_mem_total
trace event. The tracefs stand-in's event list now declares that event as
Android kernels do, and reading the stand-in's events answers with one
record carrying the total KGSL reports in /sys/class/kgsl/kgsl/page_alloc,
which every Adreno under Android has and the app can read.
Valve's mangoapp reads an Adreno GPU's load, clock and temperatures from the
paths of its own hardware, and the CPU temperature from thermal zones named as
mainline kernels name them. Every Adreno under Android keeps the GPU values in
KGSL's sysfs, readable by the app, and Android kernels name the CPU sensors
cpuss-N / cpu-N-M; the session binds each where mangoapp looks. Thermal zones
are found by sensor name, since their numbering differs per device, and
anything that cannot be found is left alone.
Three things kept Valve's mangoapp from drawing the overlay in a session:
- GLFW 3.4 takes XDG_SESSION_TYPE=wayland with WAYLAND_DISPLAY set as
Wayland-only, and mangoapp needs gamescope's X server; the wrapper sets
XDG_SESSION_TYPE=x11.
- On a Qualcomm display driver it opens a tracefs instance for the GPU memory
total and aborts when that fails, which it always does for an Android app.
The session preload now places the instance in an empty directory of its own
when the real one cannot be made.
- gamescope sends it each frame's timings through a System V message queue,
and Android kernels have no System V IPC. sysv.c now emulates the queue
operations gamescope and mangoapp use, beside its semaphores and shared
memory.
The Quick Access menu's performance overlay toggle and level slider stayed
greyed out: the client exposes SteamClient.System.Perf only with -steamos3,
which SteamOS and Armada pass alongside -steamdeck. Deck mode now launches the
client with both. The session already puts package/beta back to the forced
branch before every start, which keeps -steamos3's own branch switch from
turning into an update loop.
The Deck mode switch moves from the Performance page to the Steam page's
Client settings, next to the client branch it selects by default.
The compositor composed every surface with a blit, which replaces what is under it. gamescope's
Wayland backend hands its overlay and notification planes to the host as full-size translucent
subsurfaces, so a Steam toast, the Quick Access menu or Deck mode's performance overlay blacked
out what was beneath it.
Surfaces whose buffer has alpha and that are not declared opaque now go through an alpha pass
(premultiplied source-over); everything else, and any image the driver cannot sample, keeps the
blit. wl_surface.set_opaque_region is honoured, and only for programs that declare opaque regions
at all, so Wine windows with a meaningless alpha channel are drawn as before. On the SurfaceControl
path a translucent plane is never promoted to the game layer, and the overlay layer is marked
translucent when its plane is.
From WinNative (maxjivi05, feature/wayland-gamescope 69218fc; GPL-3.0), with the SurfaceControl
layer handling added.
Developer options' "Disable child process restrictions" writes only the
feature-flag property persist.sys.fflag.override.settings_enable_monitor_phantom_procs,
never the global setting. Android (FeatureFlagUtils.isEnabled) uses the global
setting when it is set and that property otherwise, but the check read only the
global setting, so a device switched off in Developer options stayed flagged as
unset however often Check again was pressed.
The check now follows Android's order, and the Setup card no longer says "On"
for a setting that is unset or unreadable.
* docs(release): draft 0.2.0 beta notes
* docs(release): credit prs and authors in 0.2.0 notes
* docs(release): add #39 and #45 to 0.2.0 notes
* chore: update readme
* fix(ui): cover the whole screen with the capsule wall
The wall was shifted right and then tilted around its own centre, which
left the bottom-left of the home and loading screens bare. Centre it and
size it to the tilted page's bounding box so every corner stays covered.
* feat(ui): flood from the play button into the throbber
Starting a session from a blue button no longer slides the session up
over the front end. The button stretches out on staggered springs until
the page is the ball's blue, the session opens on that same blue with no
window animation, and the blue gathers into the throbber's ball before
the throbber starts. Re-joining a running session, tiles and reduced
motion keep the old rise.
* docs(release): cover everything since 0.1 in the 0.2.0 notes
0.2.0 is the first release from the org, so list what DroidDeck does
as a whole, grouped by area: org pull requests as #N, pull requests
from before the move as The412Banner/DroidDeck#N, and the early work
that predates pull requests by author. CI and build work gets one
short section instead of a line each.
* docs(release): add screenshots and animations to the 0.2.0 notes
Lead with a game running and a few visual highlights, fold the full
list into collapsible sections by area, and shorten every line to the
feature and its credit. Pre-move pull requests read as legacy#N.
* fix(session): give the desktop its own loading hints
The loading screen's hints were written for Steam, so starting the
desktop or an emulator said the first run downloads Steam and that
leaving the app pauses Steam. Desktop and program sessions now rotate
through hints about the desktop instead.
* fix(build): skip incomplete NDKs when picking one locally
An interrupted sdkmanager install leaves a directory like
28.2.13676358.failed under ndk/, which sorted last and was picked
without a toolchain in it. Only consider directories that carry
source.properties.
* docs(release): show launching straight into Big Picture
* docs(release): credit the launch transition to #46
Twice today a session's Xwayland aborted from one thread ("The futex
facility returned an unexpected error code"), proot lost the tracee, and
the process lived on reparented to init: proot's seccomp filter, with no
tracer left, answers ENOSYS, so Xwayland's main loop logged
"WaitForSomething(): poll: Function not implemented" until something killed
it - 9 GB after Dolphin, 13 GB after PPSSPP. --kill-on-exit and the tree
sweep only reach what proot still traces, and the straggler sweep ran only
at the next session's start.
Now a session stop also kills every process of ours whose executable lives
in the Linux runtime, and bannerlator-session keeps only the first
BL_LOG_CAP bytes (256 MB) of session.log, draining the rest so a writer
that filled it is not killed by SIGPIPE.
Cemu never showed a frame on DroidDeck: its Getting Started dialog blocked
every first launch (the folder picker cannot reach ROMs), the session's
controller helper aborted it, and the desktop's Vulkan renderer drew no
cursor. With the Turnip KGSL fix in the driver and the haptic-optional
Cemu in the catalog, this makes the app side complete:
- bannerlator-pad-defaults seeds Cemu's settings.xml once: the Wii U games
folder, Vulkan, and Cubeb audio on the default device for both the TV
and the GamePad speaker at full volume (Wipeout 3 plays through the
GamePad; Cemu's own default is 20 %).
- The Cemu tile launches full screen (-f).
- libfakeinput.so is preloaded into every session process and exported its
static C++ runtime (~7000 symbols); programs unwound through it and
aborted (shadPS4 in pthread_exit, Cemu's controller thread). Link with
--exclude-libs,ALL, and fail the build if the runtime leaks out again.
- On the Vulkan and GLES desktops the pointer worked but no cursor was
drawn: wlroots hands the cursor to the app's compositor, which draws
none. WLR_NO_HARDWARE_CURSORS=1 makes labwc composite it itself.
Device-tested on the FIT: Super Mario 3D World in-game at 35-37 fps with
sound, Deus Ex HR DC and Wipeout 3 launch with sound; RPCS3, Dolphin and a
Steam session (FlatOut) unchanged on the same driver.
Brings the proot patch set up to WinNative 53836ca9 on top of the #772 port:
- trace prctl, setrlimit and prlimit64 in the seccomp filter only for the
arguments proot acts on, and uname only where the core rewrites it
- answer a seccomp stop's filter flags from the registers fetched for its
translation, reuse those registers, and skip the sysexit stop of wait4 left
to the kernel and accept without a sockaddr; the flags come from proot's list
merged with the enabled extensions, and a syscall an extension replaced
keeps its exit stop
- take a clone3 child's flags and exit signal from struct clone_args
- name a thread's /proc/self by its thread group and resolve /proc/thread-self
- translate fchmodat2, send openat2 callers to openat, and bound sysnum lookups
by the table length
- sweep relatives only for tracees that have children or ptracees, and empty
the per-stop memory collector instead of reallocating it
The session's proot runs with -i <app uid>, and the rootfs names that uid
root, so the client calls itself root while geteuid() returns the app's
uid. The read-only retry required geteuid() == 0 and never ran: the
Proton Experimental (ARM64) update still failed with Missing file
permissions on wineopenxr.dll (2026-09-26 16:07, 2,471 read-only files).
Also recognise the client by the path it was started by: the kernel's
/proc/self/exe names proot's loader for every process in the session.
The runtime setup now also fetches Proton Experimental (ARM64) and its
appmanifest from a catalog of their own (steam-seed.json) and unpacks them
where the client keeps them. At the first sign-in the compatibility tool is
already installed and the default, with no install dialog, no download and
no client restart; the client adopts the manifest and updates it like any
other app.
Done once: an install the client fetched itself, or one the user removed,
is left alone. A failed fetch stops nothing - the session asks the client
for it after sign-in, as before.
The client runs as proot's root, which the kernel does not see, and
leaves many content files read-only; an update that rewrites one was
refused (EACCES) and cancelled with "Missing file permissions"
(Proton Experimental (ARM64), wineopenxr.dll). For the client's own
processes, a refused write-open of a file the app owns now gets the
owner's write bit - on the file, or on the directory a new file goes
into - and one more try. Games and Wine keep ordinary permissions.
With Steam publicbeta 1790380355 the client spent 27 s in its post-logon
compat phase and dropped the install request the session sent 15 s after
sign-in, so a clean install never offered Proton Experimental (ARM64).
Wait for the console log's "Waiting for compat in post-logon took" (90 s
at most), then ask, up to three times a minute apart until the install
starts.
With the check reached from the client's own runs right after login, the Steam client segfaulted
within seconds, four sessions out of four; the same build without the check (test/bisect-no-launch-check)
and the last build before it both reach Big Picture. Now every run that is not a game launch -
the client's probes, app id 0 - is left exactly as before. Each check logs one line.
Steam runs its graphics probe through every registered Proton at once after login, all in
compatdata/0. Prefix system32 links point into the Proton that set it up last, and the other
Protons' Wines, running outside Proton's pfx.lock, wrote their builtin DLLs through them into that
Proton's tree (device: GE's FEX DLLs became Proton Experimental's at session start).
- bannerlator-steam-compat: both wrappers (Valve's depot launcher and the third-party
bannerlator-proton-wrap) give a tool-0 run STEAM_COMPAT_DATA_PATH=.../compatdata/0-<tool>, and
before every other launch check the Proton's files against the app's desired.tsv and put back
anything that differs (same Proton build only), logging to reapplied.log.
- ComponentsManager writes desired.tsv plus unpacked copies of the packages in use into the
runtime after every change; the page and the drawer show 're-applied at launch'.
A Components entry under Desktop opens a page built like the driver pages: a
Proton drop-down, FEX / DXVK / VKD3D-Proton tabs, what is in use, the installed
rows (each Proton build's original bundle and the stored packages; tap to
swap in, trash to delete), downloads from the Nightlies -Linux releases behind
the refresh button, and Import .wcp.
The session drawer gets a fourth tab, Components, for quick swaps between
what is installed. A swap into the Proton a running game uses waits until the
game closes and is applied then (drawer refresh, or the main screen on resume).
ComponentsManager keeps the same package format, originals layout and state as
the Decky Components Manager plugin and takes its data over once. A swap clears
only the component's own DLLs, so files a Proton keeps beside them stay.
The three page dots between LB and RB become icons standing in a line in page order, as in
Steam's Quick Access Menu. The selected one steps forward (full size, bright, a soft glow in the
accent), the others step back to half size and fade, and the line leans a little toward the
selection; tapping or picking a tab with the pad, and LB / RB, turn the page as before. The
first page's heading becomes Display to match its icon.
In touchpad mode the cursor moved but Big Picture highlighted nothing and a tap-to-click
selected nothing: the nested pointer's motion reaches wlserver as touch 0, and in Passthrough
(the touch mode Big Picture sets) a motion for a touch that is not down moves nothing, so the X
pointer stayed where it was. 0111 passes bAlwaysWarpCursor, as the other touch modes already warp.
3.16.29's Wayland backend bound only wl_pointer and wl_keyboard; touch from DroidDeck's
compositor was never read, so Steam could not be touched. 0110 binds wl_touch in the input
thread and feeds each finger to wlserver_touchdown/touchmotion/touchup, the Steam Deck
touchscreen's path, so Big Picture's Passthrough touch mode gets real touches.
On the pointer path (a client that takes no wl_touch - gamescope), touch_down counted the
fingers still down before clearing a sequence whose up had been lost, so the stale point left
any_active set and every later tap or swipe was dropped: in Direct touch mode Big Picture
ignored the screen while the pad worked. The count is taken after the clean-up, and the app
ends whatever the compositor still holds when a new gesture's first finger lands (Android has
ended every earlier gesture by then). The compositor logs once per gesture whether the window
got touch or the pointer.
The client's UI log names the signed-in account on every login change ("Login:
OnLoginStateChange <account> ...", "OnLoginUsersChanged <account>"), ~200 lines on one device.
An email account was caught by the email rule; a plain account name was not, so a user signing in
with one had it in every shared log. Those two lines now lose the account, and every
AccountName and PersonaName in the client's loginusers.vdf is learned (with the device's own
addresses, learnFromRuntime) and replaced wherever it appears. The one-time pass over older
folders runs again for this (marker .scrubbed-2).
A privacy pass over the session folders found three leaks:
- the Steam client's IPv6 check logs "external address <the device's public IPv6>" about
twenty times a session, and the redactor kept IP addresses on purpose, so steam/
connection_log.txt carried it;
- network.txt listed the device's addresses (public IPv6 included) and the session's made-up
MAC, which is seeded from ANDROID_ID and so stable per device;
- session.log, app.log, desktop.log, steam-desktop.log, crash.log were never scrubbed (clean on
device, by luck).
The redactor now learns the device's own addresses from the session's link file (a global IPv6
as its /64, a public IPv4 exactly) and removes them wherever they appear, along with anything
logged as an "external address"; Valve's server addresses stay. network.txt gives every address
as its kind ("<public IPv6>") and masks the MAC. Every text file in a session folder goes
through the redactor when the session ends (killed sessions at the next app start), and again on
its way into the share-logs zip. Folders written before this are scrubbed once at app start.
The drawer's DESKTOP ended Big Picture and landed on the front end. The session activity is
singleTop: startActivity(next) from it went to its own onNewIntent and was dropped when it
finished (on device the next session came up as "steam", not the desktop). It now restarts on
the request's intent (setIntent + recreate), as an agent start does; the desktop's Steam
launchers' handoff to a Steam session goes the same way.
Big Picture's own Switch to Desktop waits on SteamOS Manager and never finishes here. The
drawer's Steam row gets a DESKTOP button beside STEAM and QAM: the session ends and the desktop
starts in its place with Steam's desktop client open (the relaunch the Steam Desktop UI button
uses).
Tried on device: with a session bus the client still sat on "Switching to Desktop..." and
never reached its legacy path (no "No SteamOSManager support" line, steamos-session-select not
called), so the bus was not what it waited on. Sign-in was unaffected.
This reverts commit 40fc5463f3.
Big Picture's Switch to Desktop asks SteamOS Manager over the session bus before falling back to
steamos-session-select (steamui.so). The Steam session had no session bus, the ask never
finished, and the client sat on "Switching to Desktop..."; the new stub was never called. A
bus with nothing on it answers at once, and the client takes its legacy path.
- The front end's "Steam Desktop UI" started a Steam session without -gamepadui, and the client
put itself into Big Picture under gamescope anyway. It now opens the desktop with Steam's
desktop client started in it (BL_DESKTOP_STEAM, the desktop's autostart, the same
bannerlator-steam-launch the menu's Steam entry uses).
- Big Picture's power menu "Switch to Desktop" runs steamos-session-select, which did not exist,
so it sat on "Switching to Desktop..." for ever. The new stub leaves the app a request
(session=desktop in the steam-launch file); the app ends the Steam session and starts the
desktop with Steam's desktop client open.
AutoPlace on its own left the resized window in the top-left corner; with policy="center" it
opens in the middle of the desktop. Tried in a running desktop (labwc --reconfigure).
melonDS remembers a 1x window for the front end's full-screen toggle, too small on a desktop.
A labwc window rule (only the desktop reads rc.xml) resizes it to 800x600 on open and places
it again by the desktop's centre policy.
The compositor threw every wl_pointer.set_cursor image away and the app drew one fixed arrow,
so the desktop never showed labwc's resize arrows at a window's edges and corners. Ported from
Bannerlator (7f19c669): the cursor surface gets a role, its image is snapshotted on commit with
its hotspot, and a NULL surface or buffer is an authoritative hide; the app's overlay draws the
image at the session's scale. New here: labwc on Vulkan/GLES hands its cursor over as a dma-buf
(wlroots' nested backend, usually UBWC), so it is read back through a small linear host-visible
copy (vkp_image_readback). The built-in arrow stays until a program sets a cursor.
The desktop set QT_QPA_PLATFORM=wayland for every program. The melonDS AppImage bundles only
Qt's xcb plugin, so from the desktop it exited at once: "Could not find the Qt platform plugin
wayland ... Available platform plugins are: xcb" (exit 127). "wayland;xcb" keeps LXQt on
Wayland and lets such an AppImage use Xwayland. Proven in a running desktop: labwc mapped
"melonDS 1.1".
The arrow is drawn by the app and, outside a Steam session, never went away once shown - on
the desktop and over emulators started from the front end alike. A button press, a stick,
trigger or d-pad past halfway, or any on-screen control now hides it (PadBridge tells the
activity); the next touchpad or mouse move brings it back, once the pad has been quiet for
1.2 s so a touch during play does not flash it up (Bannerlator's rule for its Wayland pointer).
The guide button's toggle returns to melonDS's saved window size. A 512x288 window (1x wide,
16:9) is stretched by gamescope to fill the panel, menu bar and cursor with it; at 2x and up
the menus stay at their real size, tiny on a handheld. Written once, with the layout.
Hybrid repeats the big screen, small, above the bottom one. Horizontal with the top screen
emphasised fills the panel's height with the top screen and centres the bottom screen in the
rest. Installs set up with Hybrid (or still on melonDS's defaults) move once.
A program's small surfaces (gamescope's 1x1 cursor, Qt's menus and tooltips) announced
themselves after the game's window and took the HUD, which then read 0 fps while melonDS ran
at 60. A new window now takes the HUD only when it is at least as big, and a window quiet for a
second gives way to the next one that draws.
- bannerlator-pad-defaults: new installs start on the Hybrid layout with sharp pixels; an
existing install is moved once, only where still on melonDS's defaults (marker keeps later
choices). Guide = HK_FullscreenToggle. The user's DS/DSi BIOS, firmware and NAND are found in
ROMs/nds/{bios,firmware} and filled in only where melonDS has no path.
- Front end starts melonDS with -f, at the panel's own resolution unless the user chose one.
Scale controls down on short screens with a 9.5 mm face-button floor and
keep a bottom margin. On phones the shoulders, View and Menu sit along the
top edge; bumpers and triggers are drawn as side-by-side rectangles.
Adaptive sticks centre under the thumb and return home on release, with a
toggle in the Controller settings. The editor toolbar is a compact pill.
The on-screen pad is now placed from the panel's physical size: the
sticks and face buttons sit at the two-thumb rest point (about 17% of
the width inboard, 28% of the height up, clamped in millimetres), with
the d-pad and right stick below-inboard, shoulders above, and nothing
ever placed off screen or under a display cutout. Button sizes are
unchanged.
Setup > Controller holds the on-screen controls mode, a tint (Steam
blue #1A9FFF by default), opacity, button size, stick click (double-tap
for L3/R3), a full-screen layout editor, per-button mapping including
LT/RT, and resets for each.
Also from WinNative #772: the fakeinput ring poll backoff is capped at
2 ms instead of 16 ms, the session asks for unbuffered joystick input,
and the on-screen controls request unbuffered dispatch per gesture and
release everything when detached. The QAM button no longer draws at the
top-left corner in full-pad mode.
After a pull request's Build APK run succeeds, its APK is published as the
pr-<number> prerelease in Droid-Deck/DroidDeck-CI, named
DroidDeck-PR.<number>-<commit>.apk, with release notes taken from the pull
request. New commits to the same pull request replace its APK. Pull requests
from forks are resolved by head owner, branch and commit.
A pull request's build is signed only with the committed testkey, so every CI
build installs over the last; the publish checks the certificate against it
and refuses anything else.
Needs a DROIDDECK_CI_TOKEN secret: a fine-grained token with Contents
read/write on Droid-Deck/DroidDeck-CI. Without it the run skips with a warning.
- From the rail: -batch -bigpicture -fullscreen (the same flags as PCSX2's,
src/duckstation-qt/qthost.cpp) - straight into the game, gone when it is quit.
- bannerlator-pad-defaults: a new player starts on DuckStation's first-time setup
with the BIOS folder in the ROMs folder (psx/bios, created, or an existing one),
psx as the game list, Vulkan, the controller on Player 1 and the guide button
on the pause menu (not the analog toggle). An existing install gets the guide
and game-list changes, and once, its own empty BIOS folder moved to the ROMs
folder and Automatic made Vulkan; later choices are kept. Users bring their own
BIOS and games.
- The app opens with the selected rail item focused (its outline shows).
- Right from the rail enters the page on its main button (Play, Open desktop,
Open <emulator>, Launch, Launch in <emulator>); Down from there reaches the
tiles as before.
- Left from anything at the page's left edge - the main button, the first tile of
any row - returns to the selected rail item, not whichever one is nearest.
(Up, where the rail sits above the page.)
Quake III Arena, Team Arena, Return to Castle Wolfenstein and Jedi Academy set their own
resolution and switch the display to it. Under gamescope with the ARM64 Proton that switch is
refused ("calling CDS: failed, bad mode") and the game never gets OpenGL - device-seen on a
Retroid Pocket 5 on Proton Experimental build 25406135 - and a window left at the game's own size
draws small in a corner. Both wrappers (Valve's depot and an adopted GE or CachyOS) now add
+set r_fullscreen 0 +set r_mode -1 +set r_customwidth/height at BL_WIDTH x BL_HEIGHT for those
app ids, unless the user's launch options already set the video mode; the choice is logged.
The same choice as the Steam cog's, in the drawer's Now group, changed while the client runs:
the app writes it to ~/.droiddeck-fill (and again at every session start, so a file an earlier
session left never disagrees), and a watcher in the Steam session hands each change to gamescope
as GAMESCOPE_FORCE_WINDOWS_FULLSCREEN on the root window, which gamescope reads whenever it
changes. The session log says so when it does.
0.1.6 forced every game window to the screen's size (--force-windows-fullscreen), which keeps
FlatOut filling the screen but leaves Quake 3 drawing small in the bottom-left: it sets its own
resolution and never looks at its window again, and OpenGL draws from that corner. The switch
sits in the Steam page's Games group, on by default as before, and reaches the session as
BL_GAMESCOPE_FORCE_FULLSCREEN, which the session script already reads. Listed in the device
report.
- A portrait tile or poster with wide art (a PS3 disc's ICON0, 320x176) shows it
whole over a blurred, darkened copy of itself instead of cropping its sides
('Tomb Raider' read 'B RAI'); box art still fills the tile.
- A game's title drops the dump's tags - (USA), (En,Fr,Es,Pt), (v1.01), [!] -
which stay in the file name and in the cover lookup.
The libretro PS3 collection holds 67 covers (Tomb Raider found none). A PS3 disc
image carries PS3_GAME/ICON0.PNG, unencrypted even on protected discs; it is read
out of the ISO 9660 image, the way an installed game's icon already is. The PS2
serial lookup uses the same reader.
CoverArt looks a game up by its file name in RetroArch's libretro-thumbnails
collection for the system (exactly, then loosely: tags dropped, same region
preferred, against the collection's cached list of names), then by the disc's
own ID - PS2 serials from SYSTEM.CNF against xlenore/ps2-covers, GameCube/Wii IDs
against GameTDB. Covers are downloaded once into files/covers after the list is
shown, and the list is refreshed when some arrive; a game with none is retried
after three days; nothing is fetched offline. PS3 disc dumps use their own
PS3_GAME/ICON0.PNG.
- Back / B on an emulator page went back to Android (the app closed); it now
returns to Desktop, a game page to its emulator, a Steam game to Steam - the
same as each page's '‹' link.
- Games are looked for up to three folders inside the system folder
(ROMs/ps2/games/<game>/<game>.iso showed ARMSX2 with 0 games), and the loose-file
pass over the ROMs root no longer walks into other systems' folders.
- 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.
On device NFS Underground 2 ran at 17 fps in PCSX2 (one core at 100%, the PS2's
CPUs interpreted) and 59.9 fps at 100% speed in ARMSX2 (EE thread ~15%, all
recompilers and fastmem on under proot). Existing PCSX2 configs are left alone;
bannerlator-pad-defaults still covers them.
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).
Positioners and outputs were created without an implementation, and get_popup
created nothing. libwayland aborts on the first request to a resource with no
implementation - the compositor and the app with it. On device: a gamescope
started from the pixman desktop (droiddeck-gpu) beside labwc took the app down
(tombstone: abort in libwayland-server under wl_event_loop_dispatch). Positioner
requests are accepted and ignored, popups are dismissed at once (popup_done),
wl_output.release destroys the resource.
vulkan is the default desktop renderer (settings list it first). When labwc
cannot start on a GPU renderer the desktop falls back to pixman as before, and
now records the renderer with the patched wlroots and Linux driver it failed
on, so later desktops go straight to pixman instead of waiting 20 s each time;
an app update, a different driver, or choosing a renderer again in the settings
(which deletes the record) retries.
- A program from the rail draws with the desktop's Linux driver instead of the
Steam session's. The driver's shader cache is keyed on its build, so with two
drivers an emulator compiled everything once per way of starting it; seen on
device: desktop on the imported Turnip 26.3.0, rail on the runtime's own.
- RPCS3's default shader mode builds 6650 interpreter pipelines every boot.
bannerlator-pad-defaults starts a new install on Async Recompiler
(multi-threaded) and moves an existing one still on the default once; a
marker leaves any later choice in RPCS3's settings alone. Enum strings from
rpcs3/Emu/system_config_types.cpp.
bannerlator-pad-defaults now covers DuckStation, PCSX2, Dolphin (GameCube and
Wii), Cemu, melonDS, PPSSPP and RetroArch as well as RPCS3, from each project's
source: files an emulator would otherwise fill with keyboard-only defaults are
seeded when missing (Dolphin, Cemu, PPSSPP, RPCS3), or given SDL bindings in the
config the emulator wrote itself when nothing binds a controller yet
(DuckStation, PCSX2 with its SettingsVersion, melonDS). RetroArch gets an Xbox
360 profile for its sdl2 driver and is moved off udev, which finds no pads in the
runtime. The desktop re-runs it every 30 s, so an emulator's first config is
covered for its next start.
The PC keyboard takes 47% of the screen height, header included.
- The pad keeps Steam Input's virtual-gamepad identity only in Steam sessions.
SDL ignores a Steam virtual gamepad unless run under Steam, so on the desktop
and from the rail every SDL emulator saw no controller at all; there it is a
plain Xbox 360 pad now, which SDL and evdev map by themselves.
- bannerlator-pad-defaults writes Player 1 on the controller for emulators that
have no profile yet (RPCS3 first: without one it takes the keyboard). Run at
desktop start and before a program from the rail; never overwrites.
- The desktop and everything started from it, and a program from the rail, run
on the game cores when chosen, else every core but the slowest cluster
(program_cores); BL_PIN_CORES=0 turns it off. Game cores reach every mode.
The runtime carries Termux's proot 5.1.107-70 as a prebuilt binary, built from
termux/proot 4dba3afb (every file:line site in it matches that commit). The apk
now builds that same commit with the NDK, with three patches ported from
WinNative's feature/proot-enhancements, and prefers it over the runtime's copy:
- Tracees are hashed by pid, so each ptrace stop finds its tracee without
walking a list of every thread, and terminated tracees are swept only after
a termination.
- A clean absolute path three or more directories deep under the rootfs alone
has its parent opened once and taken as canonical when the kernel's path for
it is the one proot would build, instead of an lstat per component; the final
component of a call that does not follow it is no longer lstat'ed. Extensions
still see the parent's host path, and the f2fs workaround keeps the old walk.
- A clone3 thread keeps its inherited flags when clone_args cannot be read.
talloc is linked in statically from Samba's release tarball, so libproot.so
needs only libc and libdl. On an x86_64 Android 15 emulator the same build ran
a path and thread test suite with output identical to the unpatched commit
(fake id, fake root, link2symlink, with and without seccomp), and a stat-heavy
benchmark ran 25-27% faster, 31-36% with 1500 live threads.
The drawer's Keyboard row has two buttons: Hardware opens ui/PcKeyboard, a full
PC layout - Esc, F1-F12, Ins/Del/Home/End/PgUp/PgDn, the number and letter rows,
Tab, Caps, both Shifts, Ctrl, Super, Alt, AltGr, Space and the arrows - whose
keys are evdev codes pressed and released on the compositor's wl_keyboard, as a
USB keyboard would; Android opens the Android keyboard as before. Keys go down
on touch and up on lift (the program's key repeat works); modifiers are sticky
(tap: next key, tap again: lock, again: off). A controller moves over the keys
with the d-pad and presses with A; B closes it.
xdg-shell has the compositor reply to the initial commit; we only sent one at
get_toplevel. wlroots' Wayland backend (the desktop's labwc) moves its toplevel
to a private queue after the initial commit and waits there; on the vulkan
renderer a buffer import in between read the early configure onto the default
queue, and labwc waited for ever (WAYLAND_DEBUG: commit, then nothing). The
early configure stays for Wine's driver.
The vulkan desktop sat on the loading screen: labwc connected and never opened
its window, and it did not exit, so the exit-based fallback never fired. The
launcher now asks labwc's socket for an answer (wayland-info, 3 s) for up to
20 s, then stops it and starts the desktop on pixman. labwc runs with -d on
vulkan/gles2 so desktop.log shows where it stopped.
droiddeck-gpu runs a program in a gamescope of its own on the app's compositor
(wayland-0), full screen over the desktop - the path the rail's play chips use,
where Vulkan works. droiddeck-desktop-gpu gives every Game/Emulator menu entry a
per-user copy through it (auto: while the desktop is on pixman), keeping 'Run in
a desktop window' as an action.
droiddeck-desktop loads /usr/local/lib/droiddeck-wlroots for the vulkan/gles2
renderers and falls back to pixman when labwc stops at start. The build stages
the wlroots release named by tools/wlroots/release.env, once published.
wlroots 0.20.2 (Arch's wlroots0.20, what the desktop's labwc links) gains an
allocator that exports Vulkan images as DMA-BUFs for a GPU node that is not a
DRM device - the Adreno KGSL stand-in - where gbm and the DRM lease path fail
('unable to create allocator'). linux-dmabuf skips its GEM import check on such
a node so client buffers are not all refused. Built by build-wlroots.yml.
Compositor: without a Wine desktop, a window that opens on top takes keyboard
focus (a gamescope started from the Linux desktop).
With a controller, credits only showed the ripple's faint box on the
dark rail, and the primary button (Play) only grew slightly with a shine
that barely shows on its light fill. Both now get the 2 dp signal-blue
outline the other controls use; credits' text turns blue as well.
With the drawer open the controller still fed the game - PadBridge
consumed every pad key and axis - and nothing in the drawer had focus.
Now, while it is open, pad events go to Android's own handling instead:
the d-pad and stick move focus, and the framework's fallbacks make A
select and B Back (which closes the drawer), as on the app's main
screen. Opening it releases the game's pad (PadBridge.releaseAll), so a
button or stick held at that moment is not left down in the game.
It opens with focus on the STEAM button (in a desktop session, the first
setting), and the STEAM and ... buttons show a focus outline like the
rest of the app's controls. ToggleRow gains a modifier for its box.
On a fresh install the client starts signed out and drops the
steam://install/4427310 the session hands it, so the Proton Experimental
(ARM64) dialog only appeared after the user restarted the client by
hand. Ported from Bannerlator's session (device-proven there 2026-09-22):
- With no remembered sign-in, a watcher on logs/connection_log.txt waits
for the sign-in to complete and asks the running client again.
- A watcher on appmanifest_4427310.acf waits for StateFlags 4, registers
the compatibility tool again, leaves ~/.bl-restart-once and asks the
client to -shutdown; the start loop sees the marker and starts it once
more (not counted against the update cap), so the tool lists and
defaults are current before the user installs a game.
- The client-branch check runs before every start, not once, since the
client rewrites package/beta during its run.
The loading screen no longer says Steam keeps running in the background:
sessions pause when the app leaves the screen (Background behavior).
App name, app ID and code package: DroidDeck, com.droiddeck.launcher
(sources moved to com/droiddeck/launcher, the 68 JNI functions renamed
with them). The new app ID installs beside SteamDeck rather than over it.
Everywhere the name was the app's own: the sidebar title, the session
report, share zips, the shell title, wake-lock and log tags, User-Agents,
the Compose theme, the Download folder and switch files (droiddeck-env,
-osc, -driver, -tu-debug, -no-pad, -pad-log, -no-hud, -wlr-renderer; the
old names are not read), and inside the runtime (droiddeck-desktop,
Firefox's droiddeck.js, the shortcuts tag, the rumble socket, debug and
cache paths). Build scripts, CI artifact (droiddeck-apk), CMake project,
README and the progress log follow.
Kept on purpose: Valve's Steam Deck (the -steamdeck flag,
steamdeck_publicbeta/stable, 'Steam Deck mode', the steamos.cloud
package URLs) and names inside the hosted desktop package (steamdeck.png,
steamdeck-steam, steamdeck-bigpicture). 'Desktop installed' now tests for
labwc instead of the launcher the app stages itself. A committed
__pycache__ .pyc is removed.
The trash can sat inside the row's single clickable area, so with a
driver focused there was nothing to the right for a controller to move
to. Each row is now two focus targets side by side - the driver and its
trash can - linked both ways (right/left); the row's outline still
follows the driver part, so it looks the same.
Opened from the cog, the Steam/Desktop session page had nothing focused:
the cog that was focused is gone and a controller had to find its way
back in. The page now puts focus on its first control (Resolution) as it
opens, so the d-pad works at once; coming back from a driver page still
returns focus to the driver box that opened it. ChoiceRow gains a
modifier for its box.
Backing out of the Runtime or Display driver page rebuilt the session's
settings page from the top with nothing focused. Its scroll position is
now held above the page (SettingsPage takes an optional ScrollState) and
survives the driver page, and controller focus returns to the driver box
that was pressed to open it (ValueChip takes a modifier for its
FocusRequester).
- Every control shows a focus outline: the driver rows (the list at the
top had none), their trash cans (in red), the refresh button, Restore
built-in drivers, the confirmation's buttons, and the page's Back
(shared by every settings page).
- D-pad up from the first driver moves to the refresh button beside the
title; the page opens with the driver in use focused.
- Back (the controller's B too) closes the driver page only, back to the
session's settings, instead of leaving settings altogether.
- The delete confirmation opens on Cancel, so a stray A press never
deletes a driver.
Runtime driver and Display driver open a full page instead of a
drop-down, built from the settings widgets (as previewed): INSTALLED
lists every driver as a tappable row with a radio mark and a tag
(BUNDLED / DOWNLOADED / IMPORTED); AVAILABLE TO DOWNLOAD lists what the
last check of Banners-Turnip and WinNative found, with a Download button
and progress; Import stays at the bottom. The refresh button sits beside
the title (SettingsPage gains an action slot) and is the only thing that
goes online; it spins while checking.
Every driver gets a trash can with a confirmation, the bundled ones too:
deleting a bundled display driver removes its unpacked copy and hides it
(TurnipDriver.hideBundled), with 'Restore built-in drivers' to bring
them back. It stays in the apk and Auto still unpacks it on a GPU that
needs it, so a deletion can never leave a device without a driver. Auto
and Runtime default are settings, not drivers, and have no trash can.
- WinNative-Emu/Drivers joins Banners-Turnip as a source: WN-Turnip
(Android, v1.x line) and WN-Linux-Turnip (Linux, linux-v0.x line), each
in its Balanced and Performance builds for every Adreno.
- Each source's recent releases are read newest first and every variant
is offered from the newest release carrying it. Banners-Turnip stopped
publishing its A8xx Linux build after v26.3.0-20260923, so the runtime
menu listed no 8xx driver at all; it now offers that last one. The
refresh line names each source's newest release and any that could not
be reached.
- Drop-down menus are capped at the window's height and their entries
scroll under the title: a driver menu with its downloads ran off the
bottom of the screen, hiding the refresh line and Import.
Both driver menus (Runtime and Display) now offer the latest release of
The412Banner/Banners-Turnip: each asset that fits the menu and is not
installed yet is a 'Download Turnip <tag>' entry labelled by GPU
(plain / -A8xx / -710-720-Test zips for the display driver, -Linux ones
for the runtime driver; -Wayland ones are never offered). Nothing goes
online on its own: a refresh line checks GitHub when tapped and says
which release it found and when; the result is remembered. A download
goes through the same importer as a picked zip and lands as an ordinary
driver.
Imported and downloaded drivers get a trash-can button with a
confirmation instead of the small cross. Deleting the driver a mode is
set to puts that mode back on its default (Runtime default / Auto), and
a deleted release download is offered again.
The QAM offers Performance Overlay levels but nothing drew them: the
runtime has no mangoapp. CI now fetches Valve's mangohud-20260902.1 and
the five libraries mangoapp needs that the runtime lacks, at the versions
it was built against (fmt 10.2.0, spdlog 1.13.0, glfw 3.4, libtraceevent
1.8.2, libtracefs 1.8.0 from Valve's mash-20240705.2 snapshot), each
pinned by sha256 in tools/mangoapp/packages.txt. They are staged under
usr/local/lib/mangoapp with a wrapper on PATH; the runtime image is not
touched.
Before the client starts, remove .crash, steam.pid and the web helper's
htmlcache Singleton* locks when no client is running - Android ends
sessions without warning and those files leave the next start on the
update spinner or a blank Gamepad UI (SteamOS-ARM-SM8550's RUNSTEAM.sh
does the same). Deck mode exports STEAM_GAMESCOPE_NIS_SUPPORTED=1 so the
QAM's scaling filter lists NIS, and passes gamescope --mangoapp when
mangoapp is present (BL_MANGOAPP=0 turns it off).
SessionActivity finished at once when no runtime was installed - a guard from before the loading
screen could install it - so Play (and Desktop) opened a screen that closed before anything
happened. It now logs and carries on to installThenStart.
Resolution gains Custom…: a width × height box (320×240 to 3840×2160, evened) with one-tap
presets for 4:3 (960×720, 1024×768, 1280×960), 16:10 (1280×800) and 16:9. A custom size replaces
the cap and the shape for that mode - the Shape row greys out and says so - and the compositor fits
it to the panel with bars where the shapes differ. Choosing any cap clears it. The device report
lists it for both modes.
The panel's shape was never allowed narrower than 16:9 (the foldable guard), so on a 4:3 panel
such as the Retroid Pocket's both choices gave 16:9 with bars. A third choice, Exactly this panel,
presents the panel's own aspect with no floor; the other two are unchanged and foldables still
default to 16:9. Offered in the mode settings and the in-session menu.
The package list opens in the front end's pane like the Settings pages. Each tier (Native ARM64,
ARM64 experimental, source builds) is a dropdown row with the rail's caret and an installed/total
pill; Native ARM64 starts open. Each package has its own Install/Remove button, and the one that is
installing shows its own step line and bar in its row (its tier opens when the install starts).
Leaving the page does not stop an install. The rail's Install emulators and Setup's Desktop & apps
highlight while the page is open.
Desktop is enabled without a runtime or the desktop package. The session's loading screen installs
whatever is missing first - the runtime, then the desktop ("downloading the desktop · 212 of 426
MB", checking, unpacking) - and starts the session when both are in. A failure ends on the loading
screen with the reason. Desktop UI and Desktop now give the same non-Adreno warning as Play before
any download.
Play and Desktop UI are enabled without a runtime. The session's loading screen downloads and
unpacks it first, reporting on its usual step line and bar ("downloading the Linux runtime · 332
of 791 MB", then checking, then unpacking), and starts the session when it is in. A failure ends
on the loading screen with the reason. Non-Adreno devices still get the warning before any
download. Setup > Linux runtime is unchanged.
- The client's sound goes back to the adaptive AAudio sink 0.1.5 shipped (bundled unchanged as
module-aaudio-classic-sink, sink name AAudioSink). Through the DirectAudio relay it stayed
choppy on an AYN Thor (Android 13, 20 ms bursts) even with a two-burst buffer.
- Steam cog > Audio: DirectAudio for games (on by default, unchanged) and Steam client audio
(Classic by default, or DirectAudio). The relay also starts when only the client wants it.
- The relay runs with --log and the daemon at info level, so audio.log carries the stream each
path was granted and, on the relay, Android's xrun count every 1000 callbacks.
- Device report shows both settings.
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.
- Microphone defaults to on, and the app asks for RECORD_AUDIO once at first launch, together with
the storage prompt. A refusal is not asked again at start-up.
- modules/armhf held the inherited Winlator AAudio sink and two protocol modules; the app is
arm64-only and nothing loads that folder. Removed, bundle stamp bumped so installs re-unpack.
Everyone who never touched the toggle gets it; anyone who turned it off keeps it off (the setting is
only stored when changed). Off, the client's sound came from AAudio driven inside proot, which
stutters on some devices.
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.
Folders are a list (the single folder from before carries over); each gets /root/Games/<name>,
with a short hash of its path only when two chosen folders share a name, so adding or removing
one never moves another's games (their appids follow the guest exe path). Per-game exe choices
are keyed by the folder's path now. The cog lists each folder with its game count and a Forget,
plus an Add row.
The inherited module is gone from app/src/main/assets/pulseaudio.tzst (74 → 73 modules); CI adds
the app's own at build time and refuses a bundle that already has one. BUNDLE_STAMP bumped so an
installed app re-unpacks.
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.
The five files the audit measured as Winlator's are replaced: a one-method callback interface by
java.util.function.Consumer, the component base by SessionPart (attach an application context,
start, stop), the environment list by HostEnvironment, the process starter by HostProcess, and
the pad's state holder by PadState with named sticks, triggers, directions and buttons. The
frame-generation tree is framegen/ (its contents were never Winlator's: lsfg-vk's port and
Bannerlator's Win-FG), the log tags are the app's, and the fake pad's rumble socket is now one
the app actually serves: RumbleComponent turns a game's force-feedback effects into the phone's
vibrator, which used to go nowhere. The two design notes describing Bannerlator's X11 path are
gone, and the README and credits say what is whose.
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.
Palette (ui/Theme.kt): background, surfaces, lines, type, a primary with its gradient partner,
and a separate signal colour for rules, rings, dots and the eyebrow - the blue in every theme,
while the primary is white in Paper and blue in the other two. Every colour the screens used to
hard-code (the purple gradient end, the green, the two line greys) now comes from the palette,
so a change under Setup applies at once, and the session drawer and the file manager pick the
saved theme up when they open.
--notes "KDE Plasma desktop package for DroidDeck's Linux runtime ($GITHUB_REPOSITORY tools/desktop-kde, $GITHUB_SHA). size $(stat -c %s out/desktop-kde.tar.zst), sha256 $(cat out/desktop-kde.sha256). desktop-kde.packages.txt lists the signature-checked Arch Linux ARM packages it was built from." \
if grep -Eiq '^[[:space:]]*Co-authored-by:.*(Claude|Anthropic|OpenAI|ChatGPT|GPT|Copilot|Cursor|Gemini|Codex|Devin|Windsurf|Codeium|Amazon Q|Tabnine|Cody|\[bot\])' <<<"$messages"; then
echo "::error::AI and bot identities cannot be co-authors; remove the Co-authored-by trailer."
revoked=$(python3 -c 'import json, sys; print(" ".join(p["id"] for p in json.load(open(sys.argv[1]))["packs"] if p.get("revoked") is True))' sync-index.json)
for id in $revoked; do
if [ -e "app/src/main/assets/droiddeck-esync/packs/$id.tzst" ]; then
echo "::warning::$SYNC_BUNDLE_ASSET carries revoked pack $id; it is left out of the APK (point tools/droiddeck-esync/release.env at a newer bundle)"
echo "The apk is the \`steamdeck-apk\` artifact on [this run]($RUN_URL) (signed with the same test key as every build, so it installs over any other)."
echo "The \`droiddeck-apk\` artifact is [on this run]($RUN_URL), signed with the public test key."
echo
if [ "$SAME_REPO" = true ]; then
echo "A release-signed tester build that installs over Stable or Preview is being published now and will appear in DroidDeck's **Test builds** list. Label the PR \`no-test-build\` to skip that."
else
echo "**Maintainer:** add the \`publish-test\` label to publish a release-signed tester build that installs over Stable or Preview and appears in DroidDeck's **Test builds** list. The label is removed automatically after publishing."
fi
echo
echo "Fallback: [open **PR Release Publish**]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/workflows/pr-release-publish.yml) on \`main\` and enter PR \`$PR\`."
;;
failure)
echo "### ❌ Build failed"
echo "### ERROR: Build failed"
echo
echo "The compiler's errors ([full log]($RUN_URL)):"
echo
@@ -352,13 +580,12 @@ jobs:
echo "</details>"
;;
skipped|cancelled)
echo "### ⏭ Not built"
echo "### Not built"
echo
echo "The build did not run (see above, or [the run]($RUN_URL))."
;;
esac
echo
echo "_Updated on every push to this pull request._"
} > /tmp/pr-report.md
cat /tmp/pr-report.md >> "$GITHUB_STEP_SUMMARY"
ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq ".[] | select(.body | startswith(\"$MARK\")) | .id" | head -1 || true)
@@ -370,9 +597,162 @@ jobs:
- uses:actions/upload-artifact@v4
with:
name:steamdeck-apk
name:droiddeck-apk
path:app/build/outputs/apk/release/*.apk
if-no-files-found:error
retention-days:30
# An apk is a zip already; compressing it again buys nothing and costs seconds.
Each apk installs over a DroidDeck release and keeps its data. \`standard\` is the normal app; the others are the same app under another package name, installed beside it.
--notes "The debian:bullseye image build.yml compiles the x86 preloads in (glibc 2.31), saved with docker save and compressed with zstd. Pulled from public.ecr.aws/docker/library/debian:bullseye on $(date -u +%Y-%m-%d); image id $(docker image inspect "$image" --format '{{.Id}}'). Pinned in tools/linuxfs/debian-image.env."
Each apk installs over a DroidDeck release and keeps its data. \`standard\` is the normal app; the others are the same app under another package name, installed beside it.
SHA-256:
\`\`\`
$(cat signed/SHA256SUMS.txt)
\`\`\`"
first=false
if ! gh release view "$tag" -R "$repo" >/dev/null 2>&1; then
sed '1{/^# /d;}' "$notes" | sed -E "s#(\]\(|src=\")media/#\1$raw#g"
echo
echo "---"
echo
echo "Built by [Build APK run $BUILD_RUN]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN) from \`$GITHUB_SHA\`, signed and published by [Release APKs run $GITHUB_RUN_ID]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
} > "$RUNNER_TEMP/body.md"
( cd out && cat SHA256SUMS-*.txt | sort -k2 > SHA256SUMS.txt && rm SHA256SUMS-*.txt && cat SHA256SUMS.txt )
Running engineering log for the SteamDeck app (`com.steamdeck.launcher`). Newest state first, then
the timeline, then lessons and backlog. Companion to the README (what the app *does*) and to
`docs/releases/` (what each version said) — this is *how it got here and where it stands*.
---
## 2026-09-22 (late) — motion front end + run-as-a-game, branch `feat/frontend-motion` (unmerged)
- Front end rebuilt around a motion system (`ui/FrontEndScreen.kt`, same state/actions API): the rail's selection is one pill that springs between rows; sub-lists unfold and stagger their children; a page change sinks out and cascades in over a blurred wash of the selection's art; tiles lift/ring/shine on focus and pop a play badge; the launch button sweeps a sheen and squashes on press; the session activity rises over the front end (`res/anim/session_*`). Durations follow the system animator scale.
- The session now runs as a game: manifest `appCategory=game` + `game_mode_config` (Performance mode welcome; the OS FPS cap and downscaling refused), sustained performance mode, the panel's fastest mode at its size, GameManager game state, and an ADPF hint session over the compositor thread fed with each presented frame's interval (`session/PerfMode.kt`, `session/PerfHints.kt`, `nativeCompositorTid`). One `perf:` line in the session log says what took on the device.
- App libraries link 16 KB-aligned. Still 4 KB-only (prebuilt, need a rebuild): libpulse, libpulseaudio, libpulsecommon-13.0, libpulsecore-13.0, libsndfile, libltdl.
- The session's display caps at 720p by default now, client and desktop alike (`SessionPrefs.defaultResolutionCap`); a cap the user chose still wins. The cog's list marks the default per mode.
- r5 `1ba803b` + TZ in the guest; r6 `39272cf` Setup folds, "Linux desktop environment", help link above the grid (r4 seen running on the FIT); r7 `af3f377` settings without a pop-up: the cog and Performance are pages in the pane with anchored menus under each value, frame generation / logs / offline open in place on the rail. **r7 merged fast-forward to `main` (`af3f377`) and main's auto-build staged as `SteamDeck-r52.apk`; no tag, no release.**
- r8 `90ff23a` (branch only, unmerged): the session drawer in the front end's dress with the same rows and menus (Now / Next session / leave), FEXCore presets carried over from Bannerlator (`core/FexPreset`, Steam settings page + drawer, FEX_* into the Steam session's environment; default = FEX defaults as before), file manager and picker locked to landscape.
- Collaborators: `maxjivi05` (push) and `xXJSONDeruloXx` (push, invited 2026-09-22) on the private repo.
## 🔖 Checkpoint 2026-09-22 22:xx — known-good point for the front end
Valve's **native aarch64 Linux Steam client**, running on an Android phone or handheld: a glibc
rootfs under proot, gamescope as a Wayland client of an in-app Vulkan compositor, Xwayland and
Zink for the client's CEF, and the device's Turnip driver underneath. There is no Wine here, and
no x86 translation for the client itself — it is ARM code running as ARM code. Windows games it
launches go through Valve's own ARM64 Proton build, which is where FEX comes in.
DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big Picture on your Adreno handheld, with Windows games through Valve's ARM64 Proton.
One screen, one button: install the runtime, press Play, Big Picture comes up. Or start the
**desktop** instead — LXQt, a browser and a shelf of emulators, in the same session.
<p align="center"><a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a></p>
Around that: the Vulkan driver each half of the session draws with is yours to import and change,
audio can bypass PulseAudio and carry a microphone, the cores the client and a game get are
separate, and every session writes a folder of logs meant to be attached to a report as it is.
<p align="center"><img src="docs/releases/media/0.2.0/launch-into-steam.gif" width="80%" alt="Tapping DroidDeck on the Android home screen and landing in Steam Big Picture"></p>
## Contributors
> Note: DroidDeck does not have a stand-alone website. Do not click on any download links from websites claiming to be the DroidDeck team.
Building this alongside the owner. A pull request from either of them builds by itself and gets
a comment saying whether it merges, what conflicts, or what failed to compile.
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts. **Stores** (Setup › Stores › "Show Stores in the rail") signs in to GOG, Epic Games and Amazon Games, browses their catalogs and libraries, downloads games into the Games storage and adds each one to the Steam client as a non-Steam game, so it launches through Proton like everything else; the cog on its chip row sets which tab a store opens on and the download speed tier.
<!-- contributions:start -->
_From the repository's pull requests; rewritten when one is opened, merged or closed._
Before Steam launches, you must turn off **Restrict child processes** in Developer options. If this option is not available in developer settings (Android 12 and 13 devices), first launch of Steam will present a "Fix it for me" button, which will help automate the setup process.
Join the [DroidDeck Discord](https://discord.gg/JRGAvawjsm) for help, Preview builds, and device reports. Bug reports go in its **#bug-reports** forum; attach the zip from **Share logs** (Setup, or the session drawer) so the logs come with it.
## Requirements
## Build
- An **arm64 Android device with an Adreno GPU that Turnip supports** — in practice Adreno **730 or
newer** (Snapdragon 8 Gen 1 onward) and the **8xx** series. Mali, Xclipse and PowerVR are not
supported here, and neither is the low-end Adreno 710 (Snapdragon 6 Gen 1): Mesa has no entry
for it at all, so the session log ends at `device (chip_id = 7010000, gpu_id = 710) is
unsupported` before anything is drawn.
- **Android 9 or newer**, and roughly **3 GB free** for the runtime before any games. The desktop
and its emulator packages are another ~1.1 GB if you install them.
- **No root required.** Root only helps if you want to copy an existing Bannerlator runtime across
instead of downloading one.
Run `tools/build_local.sh` with Docker, Java 17, the Android SDK/NDK, Rust through rustup, GitHub CLI, Python 3, and `zstd` installed. It prepares the same native libraries, pinned components and audio sinks as CI, then checks the APK's assets, JNI exports and dependencies. The APK is at `app/build/outputs/apk/release/app-release.apk`. Set `DROIDDECK_PA13_SOURCE_DIR` to an existing PulseAudio 13.0 source directory to skip downloading it. Set `DROIDDECK_SIGNING_ENV` to a signing environment file to install over a release build; relative keystore paths resolve beside that file. To install on an attached device, run `tools/deploy_local.sh`.
## Installing
After preparation, ordinary `./gradlew :app:assembleRelease` builds can reuse the verified inputs. If native sources, component pins or staged files change, packaging stops with instructions to rerun the helper. Generated audio stays under `app/build/`; the tracked base bundle is never modified. `./gradlew -PskipRust=true :app:testDebugUnitTest` needs no native preparation. For an APK without store engines, use `DROIDDECK_SKIP_RUST=1 tools/build_local.sh`; it omits cached engine libraries too.
1. Install the APK from the release page.
2. Launch it and press **Install Linux runtime** — about 790 MB, once.
3. Press **Play**. On the very first run the app fetches Valve's client (a minute or two with
nothing on screen), then Big Picture comes up and you sign in.
Run the helper and preload regression suite with `bash tools/test_local.sh`. On macOS it runs in Linux through Docker, matching the helpers' runtime and CI instead of compiling Linux preloads against macOS headers.
For a desktop, open **Desktop & apps**, install the *Desktop* package, then press **Desktop** on
the main screen.
## Limits
**Do this once per device if the client dies with nothing in its log**: Android 12 and later kill
the extra processes an app starts for itself, and a session is made of dozens of them. Some phones
have a **"restrict child processes"** switch in Developer options — turn it off. See
[If Steam dies with nothing in the log](#if-steam-dies-with-nothing-in-the-log).
Compatibility and performance vary by device; hardware validation is limited. Desktop compositing uses software rendering. Firefox sandboxing is reduced under proot. Logs are kept app-private (`files/logs/`, the last 30 sessions); **Share logs** packs the newest session, with the stores and tool logs, into one scrubbed zip.
## What works today
## Credits and licence
This is version 0.1.2, and most of it has been exercised on one or two devices only. An honest
ledger rather than a feature list — where something has not been run on hardware, it says so:
| | State |
|---|---|
| Steam sign-in, store, install, launch | ✅ proven — *FlatOut* at 144 Hz through ARM64 Proton |
| Importing and choosing a driver | ✅ proven — an imported Mesa 26.3.0 glibc Turnip drew a whole session, and an imported AdrenoTools build ran the compositor |
| Desktop, panel, file manager | ✅ proven at the panel's native resolution |
| Firefox and the network (IPv4 + IPv6) | ✅ proven — live pages at 93 fps |
| Controller as an Xbox 360 pad | ✅ proven |
| Foldable, opened and closed mid-session | ✅ proven |
| Background / foreground, wake locks | ✅ proven |
| The session log folder | ✅ proven — all of it written, including this app's own log |
| Session folder finished after a crash or a kill | ⚠️ built: crash handler + next-start sweep; not yet forced on a device |
| ROMs folder and Storage in the session's home | ⚠️ built; not yet opened from an emulator on a device |
| The File Manager (Bannerlator's) and its pick mode | ⚠️ built; not yet used on a device |
| The cog beside Play / Desktop: resolution, shape, HDR, drivers, touch, audio, renderer | ⚠️ built; the resolution cap and HDR not yet seen on a device |
| Leftover session processes swept at the next start | ⚠️ built; not yet forced on a device |
| The drawer's "Steam menu" (Guide) | ⚠️ built; not yet tried |
| Emulators launched under gamescope (▶ on the rail) | ✅ proven — God of War II HD in RPCS3, ~40 fps on the FIT |
| The front end (rail, games, focus outline, Running tile) | ⚠️ built and used on the FIT; the Running tile and Steam-game ▶ not yet exercised |
| The device's volume keys during a session | ⚠️ fixed (they were swallowed); not yet re-tried |
| Game storage: a second Steam library on a card or a folder | ⚠️ built; not yet seen installing to it |
| The client's games in the desktop menu, launching Steam's desktop UI under gamescope | ⚠️ built; not yet tried |
| Stopping a competing Steam client | ⚠️ it is asked to stop and says so, but one that restarts itself from boot wins the race — uninstall it |
| DirectAudio for games, and the microphone | ⚠️ wired and the modules load; voice not yet confirmed in Steam's tester |
| Client and game core masks | ⚠️ applied and logged; no measured difference yet |
| The four session fixes (xalia, proot seccomp, sysmem, Zink) | ⚠️ they set what they say; each is a hypothesis for a device that misbehaves |
| GE-Proton / proton-cachyos | ⚠️ adopted and registered on device; no game launched through one yet |
| The emulators | ⚠️ they build, publish and install — none has been run with a game |
The two runtimes are independent after that: each app has its own Steam install, its own login and
its own games.
## Controls
A physical controller is republished into the session as a synthetic Xbox 360 pad, which is the
identity SDL and Steam have a mapping for. With nothing attached, an on-screen pad appears —
d-pad, A/B/X/Y, shoulders, select/start and the Steam button — writing into the same rings, so the
client sees one pad either way. Connect a controller and the on-screen one disappears; unplug it
and it comes back. Anywhere the controls are not is a touchpad: touch moves the pointer and a tap
clicks, which is how the client's own on-screen keyboard is used to sign in.
## Desktop
Besides the Steam client the app can start a **desktop**: LXQt on labwc, in the same session and
the same compositor, with Xwayland for the X11 programs. It is a second choice on the main
screen, not a replacement — the Steam button still hands the session to gamescope.
What it is made of is downloaded on request from a catalog hosted beside the runtime, so the app
stays small and nothing ships that a given device will not use:
| Package | What it brings |
|---|---|
| **Desktop** (406 MB) | labwc, LXQt's panel, file manager, terminal, text and image viewers, Firefox, mpv |
| **Emulators** (694 MB) | PPSSPP and RetroArch with 28 cores — PS1, PS2, N64, SNES, NES, Game Boy, GBA, DS, Genesis, Dreamcast, Saturn, PC Engine, arcade — plus ScummVM, DOSBox, Mednafen, Snes9x |
| **RPCS3 · DuckStation · melonDS** | PS3, PS1 and DS, mirrored from the projects' own ARM64 builds |
| **PCSX2 · Dolphin · Cemu** | PS2, GameCube/Wii and Wii U. No ARM64 Linux build exists upstream for any of the three, so they are compiled from source against the same glibc the runtime uses |
The desktop composites in software: wlroots allocates through gbm on a real DRM render node, and
the Adreno stand-in is not one, so labwc runs on pixman. The programs on it still reach the GPU
themselves through Vulkan and Turnip — it is the desktop's own compositing that is on the CPU.
`Download/steamdeck-wlr-renderer` (`pixman`, `gles2` or `vulkan`) overrides the choice.
Programs whose child processes sandbox themselves with seccomp and namespaces cannot set those up
under proot; Firefox's are turned off in the session's environment, without which its tabs crash.
### The front end
The main screen is a launcher: a rail with **Steam** and its installed games (from the client's
own manifests, with the library-cache art), **Desktop** and the installed emulators (their own
icons) and each emulator's games from the ROMs folder (a system folder such as `ps3/` as a hint,
by extension, one folder deep - a dump comes as a folder named for the game) plus what RPCS3 has
installed on its own HDD; then Files, Desktop & apps, Compatibility tools, Frame generation,
Performance, ROMs, Session logs, Start offline, the runtime and credits. The chosen thing sits on
the right with its ▶. A Steam game launches its session with `steam://rungameid/…` directly; a
ROM launches its emulator under gamescope with the game on the command line (RPCS3 `--no-gui`,
Dolphin `-e`, Cemu `-g`, the rest plain). A session alive in the background is the first thing on
the rail; tapping it goes back to it. The controller's focus draws a thin outline. ✅ God of War
II HD booted in RPCS3 from the rail at ~40 fps on the Pocket FIT.
### Game storage: internal, an SD card, or a folder
The client's own library is internal storage and always there. An **SD card in the phone is
registered as a second library by itself** (through the app's own folder on it, the one place a
card lets this app write), so the choice is made where it is anywhere else - inside Steam, on
every Install. **Game storage** in the Steam cog is only for turning that off or pointing it at a
folder chosen in the File Manager instead. The session binds it at
`/mnt/bannerlator-sd` and `bannerlator-steam-library` registers it with the client under the name
the app gives it; the client then asks where to install every game, lists both on its Storage
page and moves games between them itself. Set to internal only, the entry is removed again. A
card and shared storage are FUSE-backed and stream slowly - keep games that stream video or big
assets internal. ⚠️ Built; not yet seen installing to a card.
### The client's games on the desktop
The Steam client writes a menu entry for every game it installs (Games category, the game's
icon), and the home folder has **Steam Games** and **SD Games** links to their files. Those
entries run `steam steam://rungameid/…`; on this desktop `steam` is a shim, because the client
cannot run where nothing can draw with Vulkan. It - and the menu's **Steam** / **Steam Big
Picture** entries - hand off to the app (`bannerlator-steam-launch`), which ends the desktop
session and starts a Steam session in its place: the client's **desktop UI** under gamescope, and the game straight away when one was
chosen. Back opens the drawer as in any session; Stop session returns to the app. The list is
rebuilt at every desktop start. ⚠️ Built; not yet tried on a device.
### Running an emulator with the GPU
The desktop composites in software (labwc on pixman) and so offers its programs no dma-buf: a
Vulkan program on it has no swapchain to draw into and dies at its first frame — RPCS3 with
`VK_ERROR_SURFACE_LOST`. The GPU path that works on this hardware is gamescope's, the one Steam's
games render through. So an emulator is started **as a session of its own**: ▶ beside an installed
emulator under **Desktop & apps** runs it fullscreen under gamescope (the session script's `run`
mode, an AppImage or a binary inside the runtime), with Steam's display, driver and HDR settings.
The desktop remains for files, Firefox and everything that draws in software. ⚠️ Built; not yet
seen with a game.
### The device's files, and the ROMs folder
Inside a session the phone's storage is at `/storage/emulated/0`, as it is outside - but no
program's file dialog will show it: they list "Computer" from `/proc/mounts`, where a proot bind
never appears, and open in the home folder, where nothing pointed at the phone. So the home
folder now carries two entries every session:
- **`Storage`** - all of internal storage.
- **`ROMs`** - the folder chosen with **ROMs folder** on the main screen, wherever it is on the
device, an SD card included (it is a bind, not a link). Point any emulator's game directory at
`/root/ROMs` once and it is the same folder in every session after.
Both are the live folders, not copies: a save an emulator writes next to its ROM lands on the
phone.
### Files
**Files** on the main screen is Bannerlator's File Manager, carried over whole: grid and list,
sort, search, multi-select, copy / cut / paste with conflict handling and a cancellable progress
bar, rename, delete, new folder, properties (read-only and hidden), favourites, and a locations
rail with every mounted card, Downloads, the ROMs folder and the session logs. Its pick mode is
what chooses the ROMs folder and imports a driver. What is not here is what has no meaning without
a Wine container: running a file, adding it to shortcuts, the C: / Z: drives and the archive
unpacker.
## Pointer
The app draws the arrow. On the desktop, touch is a **touchpad** (Bannerlator's desktop-container
feel): drag anywhere and the pointer moves from where it is; tap to click; a second finger tapping
while one holds, or a two-finger tap, right-clicks; two fingers scroll; hold still then drag, or
double-tap-and-hold, drags. In Steam, touch is **direct**: the pointer jumps under the finger. The
drawer's *Touch* switch pins either. A USB or Bluetooth mouse works as itself.
## Networking
The desktop's programs use the phone's network directly — proot makes no network namespace, so
IPv4 and IPv6 both pass through — and the app writes the runtime's `/etc/resolv.conf` from the
active network's own DNS servers whenever the link changes (IPv4 first, never a link-local IPv6 one,
public servers as fallback), which is what Firefox and everything else in the desktop resolve with.
## Keyboard
**Keyboard** in the drawer opens the soft keyboard over the session. Steam's UI and the games
under it are X11 clients of gamescope and know nothing of Wayland text input, so what the
keyboard produces is turned into the key presses that would have typed it — Shift included, so
capitals and the symbol row (`@`, `!`, `_` …) arrive as themselves — and fed to the compositor as
key events. A hardware keyboard works as it is.
## Starting Steam offline
The client runs without a connection — installed games launch, Proton and the frame generation
are local — provided it has credentials from a sign-in that succeeded earlier. **Start offline**
on the main screen is where that is decided, and not in the drawer, because the client reads the
two keys that control it once, while it starts: changed during a session it would write the file
and change nothing until the next launch. Without an earlier sign-in the button says so and does
nothing. The client rewrites those keys when it exits, so the app writes them again at every
session start. Games with always-online DRM will not start offline, and nothing can be installed
or updated.
## Audio
The Steam client always plays through **PulseAudio** — it is a native Linux program with no other
way to make a sound, and its menus, music and voice chat all go through it. **Audio** on the main
screen adds two things on top, each applying at the next session start:
- **DirectAudio for games** — replaces Wine's audio driver *inside the games the client launches*
with Bannerlator's DirectAudio, which talks straight to Android over a socket instead of through
PulseAudio: lower latency, and no resampling in between. The client is untouched. It only pairs
with a Wine 11 Proton (Valve's ARM64 builds, GE-Proton, proton-cachyos); on anything else the
wrapper leaves it off and says so in the log, because a mismatch would be silence rather than an
error. A game's first launch after turning it on still uses Proton's own audio — the driver is
named in the game's prefix, which does not exist until that first launch — and the second uses
DirectAudio.
- **Microphone** — the relay helper opens Android's input stream under the app's own uid (which is
what the recording permission is checked against) and PulseAudio exposes it to the client as a
source named **DirectAudioMic**, so Steam voice chat works. Asks for the recording permission when
turned on; off, nothing in the session can record and Android's indicator stays off.
The helper is a small Android program the app runs beside the session; the driver is a glibc build
staged into the runtime at every launch like the session scripts, so a fix reaches an installed
runtime without re-hosting it. Proven in Bannerlator's Linux session with voice in Steam's own
tester; ⚠️ not yet exercised in this app.
## Cores
**Performance** on the main screen carries the two core masks Bannerlator's Linux session has, kept
separate because they are wanted at the same time and suit different things:
- **Steam client cores** — Steam pins its own interface renderer to a subset of cores it chooses (on
one device 5 of 8, leaving out both little cores and the fastest), which is reasonable while a
game runs and makes Big Picture sluggish when the client is all there is. Turning the override
on pins the client, its UI helper and gamescope to the cores you tick, re-applied every few
seconds because the UI keeps spawning helpers that inherit Steam's choice. Every core ticked is
the usual fix — and, unlike the game mask, it *is* sent when it names every core, since undoing
Steam's pin is the whole point.
- **Game cores** — applied by the Proton wrapper, which execs the game through `taskset` so every
thread inherits the mask from its first instruction. Leaving every core ticked sends nothing;
untick the small cores to keep a heavy game off them.
Both apply at the next session start.
Under the same dialog, **session fixes** for a device the runtime does not sit well on, tried one at
a time. **Skip Steam's xalia helper** (`PROTON_USE_XALIA=0`) is the one to reach for when a session
dies seconds after Big Picture appears: xalia is an x86 Windows program Proton starts for gamepad
navigation, it cannot load the session's preload shim under FEX, and where the vendor's seccomp
answers its `socket()` and `memfd` calls with ENOSYS it storms until the session collapses (89
refusals then a broken pipe, in the log). It costs gamepad navigation in Windows programs that are
not games. Then two for a runtime that renders slowly: **Turnip: sysmem rendering** (`TU_DEBUG=sysmem`, Banners-Turnip's advice for an Adreno 8xx that
looks glitchy or slow; a 710/720/722 gets it on its own) and **Zink: lazy descriptors**
(`ZINK_DESCRIPTORS=lazy`, for Steam's menus, which Chromium draws through Zink — the first thing to
try when they are slow while games are fast). The `steamdeck-tu-debug` and `steamdeck-env` files in
Downloads still win over both.
## Networking and the client's own network page
The session uses the phone's connection directly - proot makes no network namespace, so IPv4 and
IPv6 both pass through - and the app writes the runtime's `/etc/resolv.conf` from the active
network's own DNS servers whenever the link changes.
The client's **Settings → Internet** page and its connection indicator are a different matter:
they are built on NetworkManager, which the client looks for on the system bus as it starts. Without
one, that page stopped with *"StartScanningForNetworks is not a function"* under a header showing no
connection even while downloads ran. The session now starts a small system bus and a **stand-in that
answers for NetworkManager**, reporting what is true and nothing more - one connection that is up
while Android has a route out - and refusing what it cannot do, since nothing in this runtime can
scan for or join a network. The session log says `network: system bus up, NetworkManager stand-in
running` when it is working.
That stand-in is **maxjivi05's**, from WinNative, by way of Bannerlator.
## Compatibility tools
**Compatibility tools** (main screen and drawer) installs GE-Proton or proton-cachyos — native
ARM64 builds — beside the ARM64 Proton Valve ships. The runtime's own registrar downloads and
installs a requested build when the next session starts (several hundred MB), patches it past
pressure-vessel like Valve's, and it then appears in Steam under Properties → Compatibility for
any game. The dialog queues or cancels a request and removes an installed build.
## The cog beside Play and Desktop
Each launch button has a cog, and everything that only matters for that one mode lives behind it,
so the main screen keeps only what applies to both: **resolution** (up to 720p by default; 900p, 1080p, or the
panel's own), the display **shape**, **HDR10 output**, the
**Linux runtime driver** for that mode and the shared **display driver**, **touch**, and for Steam
the on-screen controls and audio, for the desktop its renderer.
**HDR10** is a switch that is greyed out, with the reason, on a panel that does not list HDR10 —
the compositor's HDR gate is decided from the display's own word (`android.view.Display`), once,
when the compositor starts. On, the compositor offers `wp_color_manager_v1` with BT.2020 + PQ and
10-bit buffers, gamescope is started with `--hdr-enabled` and speaks the same protocol to it, and
games get `DXVK_HDR=1`; a game that renders HDR then reaches the panel as HDR on its own display
layer, tagged BT2020_PQ. The compositor lives for the whole app process, so a change applies after
the app is fully closed and opened again. ⚠️ Wired end to end; not yet seen on a device from this
app (Bannerlator proved the same compositor path on a Fold).
## Graphics drivers
Two drivers, two lists, because a zip for one cannot serve the other — the same split Bannerlator's
Contents screen makes. Both are chosen in the cog beside Play / Desktop:
- **Linux runtime driver** — the glibc Turnip everything *inside* the runtime renders on: the Steam
client's UI, every game it launches, and in desktop mode every program on the desktop. Import a
"-Linux" Turnip zip from Banners-Turnip; a plain Android or "-Wayland" Turnip is refused with the
reason, since a Linux process cannot load a bionic library. The choice is per mode, so Steam and
the desktop can run different builds, and it takes effect at the next session start. Nothing in
the runtime is modified: the session is handed the imported driver's ICD manifest and points the
Vulkan loader at it, and the runtime's own Turnip is what a removed or unreadable import falls
back to. This is how a driver fix reaches an installed runtime without a ~790 MB re-download.
- **Display driver (Android)** — the bionic Turnip the app's compositor puts the frame on the panel
with, the last step of every session. *Auto* picks one of the two bundled builds by GPU; an
imported AdrenoTools zip can be chosen instead. The compositor loads its driver once per app
process, so this one applies after the app is fully closed and started again.
Both imports open the app's own file picker rather than Android's document picker: an SD card is a
card in its drive menu, the folder last picked from is where it opens next time, and it hands back
a plain path.
## Frame generation
Extra frames are generated between the real ones on the way to the screen — inside the app's
compositor, on gamescope's final output — so it works for any game with nothing changed in the
runtime. **Win-FG** is built in. **LSFG** uses the shader chain from your own copy of Lossless
Scaling: install it from the Steam client in this app and the app reads its `Lossless.dll` from
the runtime's Steam library (parsed as data, never executed; nothing of it is redistributed).
Pick an engine and 2×/3×/4× on the main screen; it applies immediately, mid-game included.
## Background and foreground
A session belongs to a foreground service, not to the activity, so leaving Big Picture does not
end it: the process stays 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. **Back opens the drawer**: the performance HUD switch, frame generation, on-screen controls
(auto / always / never), the display shape (the panel's own, or a fixed 16:9 — the default on a
foldable, so opening or closing it mid-session only changes the bars, never the picture; takes
effect on the next session), *Send to background* and *Stop session*. Backgrounded, Steam keeps
running and the (silent) notification brings it back. A session ends only when you say so — the
drawer or the notification's **Stop session** — or when the app is swiped out of recents.
## Device switches
Files in `/sdcard/Download`, for a device that cannot be reached with a debugger:
| File | Effect |
|---|---|
| `steamdeck-no-pad` | Turns the controller feature off entirely (a true baseline) |
| `steamdeck-pad-log` | Traces every interposer call into the session log |
| `steamdeck-driver` | `a7xx`, `a8xx` or `system` — overrides the Turnip build the compositor loads |
| `steamdeck-no-hud` | Hides the top-right performance line (fps, and `base → generated` while frame generation runs) |
| `steamdeck-osc` | `always` or `never` — pins the on-screen controls instead of following what is attached |
| `steamdeck-env` | `KEY=VALUE` lines added to the session's environment as written, after the app's own — Zink and Turnip tunables (`ZINK_DESCRIPTORS=lazy`, `MESA_*`), gamescope's, the client's; `#` comments allowed |
| `steamdeck-tu-debug` | Turnip's `TU_DEBUG` for the runtime's driver, verbatim (`sysmem`, `sysmem,deck_emu`). Without it, an imported A710/A720/A722 driver gets `sysmem` on its own |
## Session logs
Every session writes a folder under `/sdcard/Download/SteamDeck/`, and it is the first thing to
look at when something does not start:
```
Download/SteamDeck/session-20260921-161256/
device.txt what this device is, and every setting the session ran with
session.log the guest session: proot, gamescope, the client's own output
wayland.log the app's compositor - what it presented, at what size, and how fast
app.log what the app itself decided and reported
crash.log Android's crash buffer, as it stood when the session ended
crash-app.txt the stack trace, if the app itself crashed
audio.log the PulseAudio daemon and the DirectAudio relay helper
network.txt the link, the DNS the runtime was given, and what it means
steam.log the Steam client's log, scrubbed (Steam mode)
steam/ the rest of the client's logs, scrubbed (Steam mode)
desktop.log labwc, the panel and the programs on it (desktop mode)
```
`app.log` is this app's own logcat, filtered to its process: which driver it chose and why, the
audio line, a rival Steam client being stopped, a helper that was missing, the session's exit
status. `crash.log` is Android's crash buffer at teardown, which is where a session the *system*
killed leaves its only trace - proot once died before `main` over a missing library and said so
there and nowhere else. `audio.log` is the PulseAudio daemon's own output plus the relay helper's,
so a module that refuses to load is visible instead of being a silent absence of sound.
`network.txt` records the transport, whether the link validated, the address families and the DNS
servers the runtime was actually given - **never the network's name**.
A folder is finished **however the session ends**, not only at a clean stop. Everything written
*during* a session is on disk as it happens; only the ending - the compositor's log, Steam's
scrubbed logs, the crash buffer - is gathered afterwards, and three things gather it: the ordinary
stop; the app's own crash handler, which writes `crash-app.txt` with the stack trace and finishes
the folder before the process dies; and a sweep at the next app start, for a folder whose process
was killed outright (Android's phantom-process killer, a native crash in the compositor, a dead
battery) - that folder gets `ended-without-teardown.txt` saying so, and `crash.log` as the buffer
stands then, which still holds the entry unless the device rebooted. A finished folder carries a
`.complete` marker; one without it is what the sweep looks for.
**Session logs** on the main screen turns the folder off. The session still writes its logs - the
scripts and the compositor need somewhere to - but into the app's cache, and they are deleted
when the session ends, so nothing accumulates in Downloads.
A session's process tree is also swept at the next start: proot, gamescope and Steam outlive an
app that was killed or crashed, holding the rootfs, the GPU and Steam's lock, and the next session
then never came up until the app was force-stopped (a Thor Pro report). Anything under the app's
uid other than the app itself is a leftover at that point, and `app.log` lists what was killed.
`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 and kernel, cores and their ceilings, RAM, the GPU
as KGSL names it, the panel, **which driver each half of the session chose**, the runtime version,
and every switch that was in effect. **Nothing identifying is collected** - no serial number, no
device or advertising id, no account name, no network names - and the Steam logs are scrubbed line
by line on the way in: session tokens, machine-auth GUIDs, WebAPI keys, Guard codes and e-mail
addresses are replaced, and a SteamID is masked to its last four digits so lines can still be
correlated. `loginusers.vdf`, `config.vdf` and the `ssfn` files are never copied at all. A session
folder is meant to be attachable to a bug report exactly as it is.
For more from the desktop than it normally says, create `~/.steamdeck-desktop-debug` inside the
runtime: labwc then logs at debug level, each program's window-protocol traffic is recorded, and the
session does a name lookup plus an IPv4 and an IPv6 fetch of its own — which is how to tell a broken
network apart from a program that only believes it has one.
### If Steam dies with nothing in the log
That is usually not Steam. **Android 12 and later kill the extra processes an app starts for
itself** once there are more than a few, and a session is made of dozens - proot, gamescope,
Xwayland, the client and its helpers, Wine, FEX. Where that is left on, the OS ends the session and
no log of ours says why, because nothing in the session did it. Some phones expose it in Developer
options as a **"restrict child processes"** switch; turn it off. Otherwise, once per device:
```sh
adb shell settings put global settings_enable_monitor_phantom_procs false
```
`device.txt` records what this phone reports, and the Performance dialog says so when it is not
disabled.
## Authors
- **The412Banner** — the app, the Wayland compositor, the runtime plumbing and the Steam session.
- **maxjivi05 (Max)** — the gamescope runtime this is built on: the proot session, the session
shim, the fake-evdev interposer and the controller work, from WinNative.
## Lineage and licence
GPL-3.0. The gamescope runtime, the session shim and the fake-evdev interposer come from
maxjivi05's WinNative work and from Bannerlator, both GPL-3.0; the Wayland compositor, the frame
generation engines and the runtime plumbing are Bannerlator's. Underneath all of it is Winlator
(brunodev85, GPL-3.0): the PulseAudio-on-AAudio audio stack, the gamepad state model and the shape
of a session's host-side components are his, and WinNative and Bannerlator are both Winlator
lineage. Valve, Steam and Proton are Valve Corporation's; this project
is not affiliated with Valve.
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
# DirectAudio for the Linux Steam client (relay build)
This is the driver for games that run under **Valve's ARM64 Proton inside a Linux
runtime on Android** - Bannerlator's linuxfs session with the native Steam client -
where the game process is a glibc process and cannot touch Android's audio
libraries itself.
Two pieces, both required:
| piece | what it is | where it runs |
|---|---|---|
| `aarch64-unix/winedirectaudio.so` + the two PE shells | the Wine `mmdevapi` driver, glibc build, **no AAudio inside** | in the game, inside the Linux rootfs |
| `directaudio-relay` | a small bionic program that owns the AAudio output and microphone streams | on the Android side, under the app's uid |
They meet over a unix socket. The driver connects, sends its launch config, and
gets back two shared-memory rings: one it fills with mixed game audio, one the
helper fills with microphone audio. The helper plays and records; the driver
keeps doing everything else it always did.
## Which Proton
Built inside `ValveSoftware/wine` at `proton_11.0`. The private `mmdevapi`
interface it implements is byte-identical across **Proton 11.0 (ARM64)**,
**Proton Experimental (ARM64)** and our own Wine-11 layers (checked 2026-09-19),
so this one build serves any of them. A future Proton on a Wine 12 base will
need a rebuild - the interface is not versioned, and a mismatch is silence, not
an error.
## Installing the driver next to Proton (not into it)
Steam verifies and updates its Proton depots, so files added under
`steamapps/common/Proton .../files/lib/wine/` will not survive. Put the driver
not_available` and `displayBrightnessNits=-1`, so **nothing of it can be seen on this device**,
and turning it on there would actively *cost* us the zero-copy DEVICE-composition win. Verdict
in §8: **not now.**
---
## 1. Game side — how far an HDR request gets today
### 1.1 What a Windows game does
| Call | Where it lands in DXVK | Notes |
|---|---|---|
| `IDXGIOutput6::GetDesc1` → `DXGI_OUTPUT_DESC1::ColorSpace` | `src/dxgi/dxgi_output.cpp:220-259` | **Never queries Vulkan.** Reports `RGB_FULL_G2084_NONE_P2020` purely because `dxgi.enableHDR` is on. |
| `IDXGISwapChain3::CheckColorSpaceSupport` | `src/dxgi/dxgi_swapchain.cpp:655-667` | This *is* backed by Vulkan (below). Note it is a swapchain method, not a factory method. |
| `IDXGISwapChain3::SetColorSpace1` | `dxgi_swapchain.cpp:672-688` → `d3d11_swapchain.cpp:306-331` → `Presenter::supportsColorSpace` (`dxvk_presenter.cpp:319-343`) | Returns `E_INVALIDARG` if the Vulkan surface does not list the colour space. |
| `IDXGISwapChain4::SetHDRMetaData` | `dxgi_swapchain.cpp:690-717` → `vkSetHdrMetadataEXT` | Only `DXGI_HDR_METADATA_TYPE_HDR10`. Silently dropped if `VK_EXT_hdr_metadata` is absent (`dxvk_presenter.cpp:143-149`). |
The platform loader contains the string `native_window_set_buffers_data_space(%d) failed: %s (%d)`
— i.e. Android's own swapchain implementation is what turns a `VkColorSpaceKHR` into a surface
dataspace. So for the **copy path**, the compositor's output HDR is genuinely just "enable
`VK_EXT_swapchain_colorspace` at instance creation and pick a better surface format than
`fmts[0]`". No driver work.
---
## 5. The gap list, ordered, with sizes
Sizes are engineer-days for someone who already knows this code. "Prove" = device-visible in logs
or `dumpsys`, not by eye. **App-only unless marked.**
### Phase A — make the truth reachable (no user-visible HDR yet)
| # | Repo | Work | Size |
|---|---|---|---|
| A1 | app | **Advertise 10-bit DRM formats over `zwp_linux_dmabuf_v1`.** Add `AB30`/`XB30` (and optionally `AR30`/`XR30`, `AB4H`/`XB4H`) to `compositor.c:1323-1326` + `FOURCC` defines near `:415-421`. `vkp_dmabuf_modifiers` and `drm_to_vk` already handle them, so this is a table edit + a log line. Risk: a client picking a 10-bit format while the scene/effects chain stays 8-bit — the import works (`drm_to_vk`), the blit downconverts. Gate behind a flag until A4. | **0.5 d** |
| A2 | app | **Generate + commit `color-management-v1` protocol glue.** Fetch `staging/color-management/color-management-v1.xml` from wayland-protocols ≥ 1.41, run `wayland-scanner` offline, commit XML + `generated/color-management-v1-{protocol.c,server-protocol.h}`, add to `CMakeLists.txt`. Mechanical. | **0.5 d** |
| A3 | app | **Implement `wp_color_manager_v1` v1 server-side** (new `wl_color_mgmt.c`, ~400–500 lines): the global + bind-time feature/intent/primaries/tf events; the parametric creator with `set_primaries_named`/`set_tf_named`/`set_max_cll`/`set_max_fall`/`set_mastering_*`; image-description objects with `ready`/`failed`; `get_surface` → `wp_color_management_surface_v1` and `set_image_description` stored double-buffered on our `struct surface`. **Watch the blocking loop:** Mesa dispatches its queue waiting for `ready`, so we must send `ready` from the request handler's own flush, not defer it to the next frame, or a game stalls at swapchain creation. Advertise only what the *current output* can actually honour (see A5). | **2 d** |
| A4 | app | **Carry the image description onto the Android layer.** dlsym `ASurfaceTransaction_setBufferDataSpace` (+ `setHdrMetadata_smpte2086` / `_cta861_3`) in `sc_layer.c:82-97`; map surface image description → `ADataSpace` + `AHdrMetadata_*`; apply in `sc_layer_present_ahb` (`sc_layer.c:430-470`, next to the existing `setBuffer`). Plumb the per-surface description from `compositor.c` through `ahb_swapchain.c:219`. Also set an explicit `ADATASPACE_SRGB` on the existing 8-bit path so we stop relying on `UNKNOWN`. | **1 d** |
| A5 | app | **Capability gate.** Read `Display.getHdrCapabilities()` / `getSupportedHdrTypes()` / `isHdrSdrRatioAvailable()` (and the external `Display` when `ExternalDisplayController` has one) in Java, pass down over JNI, and *only then* advertise `bt2020`/`st2084_pq` from A3. Without this we advertise HDR on a panel that will tone-map it and drop the layer to CLIENT (§3.4). Include a `banner_log("color", ...)` line naming exactly what was advertised and why. | **0.5 d** |
**Phase A total: ~4.5 days. App-only. No new wcp.** End state: a game with `DXVK_HDR=1` on a
genuinely HDR display gets an HDR10 swapchain, its buffer reaches an `ASurfaceControl` layer
tagged `BT2020_PQ` with real mastering metadata, and `dumpsys SurfaceFlinger` proves it.
### Phase B — the copy path and the effects chain (only if A is kept)
| # | Repo | Work | Size |
|---|---|---|---|
| B1 | app | **10-bit output swapchain.** Add `VK_EXT_swapchain_colorspace` to `vk_present.c:301-311` (`enabledExtensionCount` 2 → 3), replace `chosen = fmts[0]` (`:441`) with a real preference walk (HDR10 + `A2B10G10R10` when the display supports it, else `R8G8B8A8_UNORM` + sRGB). The Android platform loader does the rest. | **0.5 d** |
| B2 | app | **Widen the scene + effects chain.**`SCENE_FMT` (`vk_present.c:54`) and `FX_FORMAT` (`effects_chain.c:29`) → `A2B10G10R10_UNORM_PACK32` (or FP16 for real HDR headroom). Every one of the 13 passes samples/writes it; the debanding pass in particular must be re-tuned because its strength is calibrated for 8-bit quantisation. Memory and bandwidth go up ~25 % (or 100 % for FP16). This is the expensive item and it regresses SDR perf if done unconditionally. | **2 d** |
| B3 | app | **Layer pool at 10-bit.**`sc_layer.c:302` pool format → `R10G10B10A2_UNORM` when the chain is 10-bit, guarded by `AHardwareBuffer_isSupported()`, with the existing `sniff_modifier`/linear-fallback retry extended. | **0.5 d** |
**Phase B total: ~3 days. App-only. No new wcp.**
### Phase C — scRGB, and the one thing that needs a wcp
| # | Repo | Work | Size |
|---|---|---|---|
| C1 | **driver (new wcp)** | Map `VK_FORMAT_R16G16B16A16_SFLOAT` → `AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT` (0x16) in `banner_ahb_format_for` (`patches/wayland/banner_ahb_wsi.py:225-237`) so FP16 swapchains are not hidden by `banner_ahb_skip_format` when zero-copy is on. One enum + a rebuild of eight drivers. **This is the only wcp-requiring item, and only for scRGB zero-copy.** Blocked on proving FP16 gralloc allocation works on Adreno at all (§3.3). | **0.5 d + a driver build cycle** |
| C3 | **proton-wine (new wcp)** | *Optional, quality only.* Synthesise an EDID with a CTA-861.3 HDR static metadata block in `winewayland.drv` so DXVK reports the real panel luminance in `DXGI_OUTPUT_DESC1` instead of its 1499-nit dummy. | **1–2 d** |
---
## 6. Cheap partial wins — and which are visible on a 500-nit panel
| Idea | Verdict on the Pocket FIT | Notes |
|---|---|---|
| **10-bit output (banding)** | **Visible, and the best value here.** | This is the one real win. B1 alone (0.5 d) gets a 10-bit output swapchain; A1 (0.5 d) lets the *game* render 10-bit. Adreno 750 + the DPU handle `RGBA_1010102` natively, and `vendor.display.enable_hdr10_gpu_target=1` says the GPU target path expects it. Pairs with the existing debanding pass — arguably makes it unnecessary for 10-bit-capable games, which saves a full-screen pass. Visible on gradients (skyboxes, fog, dark interiors) where our deband pass currently earns its keep. |
| **Display-P3 wide gamut on SDR** | **Not visible. Do not build.** | `supportedColorModes=[0]` / `ColorMode::NATIVE` — the HWC exposes no P3 mode. `persist.sys.sf.color_mode=9` asks for P3 and is ignored. Tagging a layer `ADATASPACE_DISPLAY_P3` would make SF convert P3 → the panel's native gamut, which on a NATIVE-mode display is an unknown-to-us transform. Net effect on this device: at best nothing, at worst a colour shift. |
| **`setExtendedRangeBrightness` to lift highlights** | **Not available.** | `hdrSdrRatio not_available` / `NaN`, `displayBrightnessNits=-1`, `sdrWhitePointNits=-1`. `Display.isHdrSdrRatioAvailable()` is false, so there is no headroom to claim and SF has no nits data to dim the other layers against. |
| **`setDesiredHdrHeadroom`** | **Not available.** | API 35; device is API 34. Absent from `libandroid.so`. |
| **Set `ADATASPACE_SRGB` explicitly on our layers** | **Invisible but worth doing anyway (part of A4, ~0 extra cost).** | Every layer currently reports `dataspace=UNKNOWN (0)`. It works today because SF treats UNKNOWN as sRGB, but it is an accident we are relying on, and it will become wrong the moment any colour management is enabled. |
**So: of the three "cheap wins" in the brief, exactly one — 10-bit — is real on this hardware,
and it is worth about 1 engineer-day (A1 + B1) on its own, independent of HDR.**
---
## 7. How we would ever prove it
### 7.1 Which games actually emit HDR under DXVK / VKD3D
Requirements: a flip-model swapchain at `R10G10B10A2_UNORM` or `R16G16B16A16_FLOAT`, an
`IDXGISwapChain3::SetColorSpace1` call, and `DXVK_HDR=1` in the environment. Known-good
candidates, easiest first:
* **D3D11 + scRGB:***Shadow of the Tomb Raider*, *Resident Evil 2/3 Remake*, *Devil May Cry 5*,
*Mass Effect Legendary Edition*. RE Engine titles are the usual smoke test on Linux because they
call `SetColorSpace1` early and unconditionally when DXGI reports HDR.
* **D3D12 + HDR10:***Cyberpunk 2077*, *Forza Horizon 5*, *Horizon Zero Dawn*. These go through
vkd3d-proton's stricter path (§1.1) — good for catching a half-done implementation, since a
wrong answer **stalls presentation** instead of degrading.
* **Sanity harness, no game needed:** anything that calls `vkGetPhysicalDeviceSurfaceFormatsKHR`
under the layer. `vulkaninfo` is not available in the prefix, but a tiny Win32 Vulkan probe
built against the layer's headers, or simply DXVK's own `VK_INSTANCE_LAYERS`-free log with
`DXVK_LOG_LEVEL=info` and `DXVK_HDR=1`, will say whether the colour space was accepted. **Start
here** — it is the cheapest signal and needs no HDR display at all.
### 7.2 Platform-side verification (not by eye)
Order of proof, each step gating the next:
1. **Compositor log.** New `banner_log("color", ...)` lines: which primaries/tf we advertised and
why; every image description created (primaries, tf, max_cll, max_fall); every dataspace set on
the layer. This is the only step that works on any device.
2. **Guest log.**`DXVK_HDR=1` + `DXVK_LOG_LEVEL=info` → DXVK prints the chosen colour space and
format at swapchain creation. If `SetColorSpace1` was refused, DXVK says so. Mesa's WSI prints
`Not using HDR metadata to avoid protocol errors` when `is_hdr_metadata_legal` rejects metadata
— catching that string is a direct test of A3's metadata plumbing.
3. **`dumpsys SurfaceFlinger | grep banner_wayland_game`.** Today it prints
unavailable:** the scene goes through the copy path and the HDR frames are shown **untone-mapped
(washed out)**; every such scene is counted and the reason logged.
### 10.5 Proof without root — what a tester's logs say
`Download/Wayland-logs/wayland-*.log`, tag `color` (plus the existing `display` capability line):
| Line | Means |
|---|---|
| `HDR gate OPEN: … "Built-in Screen" … reports HDR types HDR10, …` | the offer is on, with its inputs and which Android colour calls exist |
| `<program> bound wp_color_manager_v1 version 1 …` | Mesa saw it: `VK_COLOR_SPACE_HDR10_ST2084_EXT` is now listed |
| `<window>: colour-management surface created` | a swapchain asked for a non-sRGB colour space |
| `image description #N from <program>: BT.2020, ST 2084 (PQ); mastering …; max CLL …, max FALL …` | the game's colour space + `vkSetHdrMetadataEXT` values |
| `<window> presents WxH buffers in XB30 (10-bit A2B10G10R10) …, gralloc RGBA1010102 (10-bit)` | the 10-bit swapchain really arrived |
| `banner_wayland_game: dataspace BT2020_PQ (0x9c60000) set on the display layer …` | the tag + metadata SurfaceFlinger received |
| `HDR last 10 s: N frames on the display layer tagged BT2020_PQ …` | steady state |
| `display HDR/SDR ratio X (was Y; HDR frames on screen: yes …)` | `Display.getHdrSdrRatio()` — rises above 1.00 only when Android grants HDR headroom |
| `HDR on screen: yes / tagged but NOT confirmed / no, because …` | the verdict (written on changes, at game exit and at session end; the last one counts) |
DXVK's own `<exe>_dxgi.log` / `<exe>_d3d11.log` add `Color space: VK_COLOR_SPACE_HDR10_ST2084_EXT` and
the 10-bit format at swapchain creation; Mesa's `Not using HDR metadata to avoid protocol errors`
(stderr → `wine_debug.log`) means the game's metadata failed Mesa's own legality check.
## 11. Round 2 — as built (2026-09-14, `feat/wayland-hdr`; CI only, not device-proven yet)
Round 1 is proven on the tester's Fold (God of War, DXVK v3.1: 10-bit zero-copy, `BT2020_PQ` on the
layer, HDR/SDR ratio 1.00 → 2.51, `HDR on screen: yes`). Round 2 makes it a setting and removes
round 1's big limitation (§10.4: no tone-mapping, effects/frame generation skipped, washed-out copies).
The gate itself is unchanged (§10.1) apart from its first input.
### 11.1 The setting, and what the launch does with it
- `Container.isWaylandHdr()` (extraData `waylandHdr`) and the shortcut extra `waylandHdr` (`1`/`0`/unset
= the container's) — one resolver, `display/WaylandHdr.effective()`, used by the three editors
(container, game shortcut, XMB game settings) and the launch path. `BANNER_WAYLAND_HDR` in the
environment still overrides it (`1` / `0` / `force`). Editors grey the row with the reason when the
built-in screen does not list HDR10 (`WaylandHdr.unavailableReason`), and say it applies from the
next launch.
- HDR on for the session (setting resolved on AND HDR10 display, or `force`): `DXVK_HDR=1` is exported
after the user's env merge unless the user set `DXVK_HDR` themselves.
- Every Wayland session whose display lists **HDR10** exports `BANNER_WAYLAND_HDR_MAX_NITS`,
`…_MAX_AVG_NITS`, `…_MIN_NITS` (decimal nits from `Display.getHdrCapabilities()`; unknown values and
a max of 0 are left out) — the contract the Wayland layer from versionCode 10 turns into EDID HDR
metadata for DXGI (checked by the Wine side against DXVK's own EDID parser: the Fold's 1351/1351/0 →
DXGI max 1345.43, max full-frame 1345.43, min 0.01). Never on an SDR display, whose EDID would then
claim PQ support. One `session environment:` line logs them. Android has no live absolute nits; the
live evidence stays the HDR/SDR ratio.
- A "DXVK before 3.0 needs layer v10" warning was built and then **removed**: its premise was wrong
(Wine already exposes `VK_EXT_swapchain_colorspace` at instance level; DXVK ≥ 2.1 looks for it in the
device list, logs 0 on every Mesa system — the working v3.1 run logs 0 too — and never uses it for the
HDR decision). Why God of War offered no HDR option on DXVK 2.4.1 is open (Fold A/B).
Every scene with an HDR draw takes one of four routes (logged on each change, with the reason):
| Route | When | What reaches the display |
|---|---|---|
| 0 | the HDR game alone fullscreen, zero-copy on, no effects, nothing above it (or one window the overlay layer can carry) | the game's own 10-bit gralloc frames on its layer, `BT2020_PQ` (round 1) |
| 1 | effects on, a window above on a display that cannot take a second layer, a windowed game, zero-copy off | **the HDR picture**: every draw blitted 1:1 into a 10-bit "mixed" image (each pixel still in its own encoding), ONE encode pass to PQ BT.2020 (SDR pixels sRGB → linear → BT.2020, placed at 203 nits, `BANNER_WAYLAND_HDR_SDR_NITS` overrides), the effects chain run on it in 10-bit (`vkp_effects_set_formats`), copied into a 10-bit (`RGBA1010102`) layer buffer, tagged with the game's description; 8-bit buffers if gralloc refuses 10-bit (logged) |
| 2 | frame generation on | composed into PQ in the 8-bit scene image, through the frame-generation engine, presented through an **HDR10 swapchain** (`VK_EXT_swapchain_colorspace` enabled on the compositor instance in HDR sessions only; first `A2B10G10R10` else FP16 pair the surface lists with `HDR10_ST2084`); where the surface lists none, **tone-mapped** into the ordinary swapchain. 8 bits through the engine: gradients may band |
| 3 | an HDR frame the compositor could not import | layer only; effects/frame generation skipped for it (round 1's behaviour, now the rare case) |
The tone map (mode 1 of `hdr_encode.frag`): PQ → nits → BT.2020 → BT.709, relative to SDR white, maxRGB
curve linear to a 0.8 knee then an exponential roll-off that puts the content peak (the game's max CLL,
else mastering max, else 1000 nits) at 98 %, sRGB-encoded. Which pixels are HDR comes from up to six
top-first scene rects + a mask in the push constants (more windows over the game → the lowest are
treated as SDR, logged once). A composed frame that cannot reach the layer falls back to route 2 for as
long as that lasts (logged once each way).
### 11.3 The live switch (lead's addition): drawer → Graphics → "HDR output"
Shown only while the gate is open (the activity's 1 s HDR sampler flips `waylandHdrAvailable`). Per
session, starts on, nothing saved. Off (`banner_color_set_output(0)`, posted through the host queue
to the compositor thread, logged, redraw): route 0 becomes route 1 with the tone map, the picture goes
on the layer **untagged** on 8-bit buffers (the layer's dataspace returns to `UNKNOWN`); route 2
rebuilds the swapchain as SDR and tone-maps; route 3 cannot (nothing can read those frames) and stays
HDR, said in the log. The game is told nothing (its DXVK_HDR and colour-manager offer were decided at
launch). The HUD reads `Wayland · HDR` while HDR frames are on screen (tagged in the last 1.5 s and
ratio > 1.01 where reported), `Wayland · HDR off` while the switch is off. Frames tone-mapped by the
switch are counted separately in the verdict.
### 11.4 Live headroom (lead's addition after the second Fold run)
The Fold's second run (brightness slider at maximum) showed the ratio at 3.23 for a minute and then
**1.00 for 3+ minutes with HDR frames on screen the whole time**, while the verdict kept saying "rose to
3.23". Android simply gave the layer no headroom (SDR white already at the panel's limit). Now, from the
ratio samples the app already sends (1 s + the display listener): the time with HDR frames on screen and
the part of it with ratio > 1.01; a no-headroom streak (HDR frames on screen, ratio <= 1.01) that after
5 s writes `no HDR headroom for 5 s … the screen brightness is probably at maximum …` (and `HDR headroom
is back …` when it ends), tags the 10 s line and the steady ratio line, puts the HUD on `Wayland · HDR
(no headroom)` and the drawer row on the same hint (`banner_color_hdr_state` = 2); and the verdict
reports `headroom above 1.00 for P% of the HDR time (a of b s), now R` plus the hint while the streak
lasts. Cheap: no new sampling, a few integers under the existing lock.
**Round-2 Fold run (20:23, run 34911468350):** the first no-headroom streak was a **screen recording**,
not the brightness: the user's recording (210.8 s, BT.709 SDR H.264, finalised 20:27:30, so started
~20:23:59) lines up with the ratio falling 2.98 → 1.00 at 20:24:00.9 and staying there to the end. So
Android (at least Samsung's) turns HDR headroom off while the screen is recorded. Every hint now names
both causes; whether maximum brightness does it too is still unmeasured (tester note step H).
### 11.5 The HUD's HDR line (round 2b, the user's layout request)
`40.3ms · Wayland · HDR (no headroom)` ran off the pill's right edge (the capsule's ends curve in, and the
latency line is the bottom of the stack). The display-server label is plain `Wayland` again, and the HDR
state is a line of its own directly under it, fed as a `FusionHdr` code (`FusionHudView.setHdrState`):
`HDR` / `HDR (no headroom)` / `HDR off` / `HDR tone-mapped` (switch on, frame generation without an HDR10
swapchain) / `HDR ready` (gate open, no HDR frames right now). `NONE` - every session whose gate is
closed - draws nothing, so those HUDs are unchanged to the pixel. Pill: the extra line makes the capsule
taller and its ends rounder, so in HDR sessions `fitCapsule()` measures every glyph's ink against the
two rounded ends (plus the outline and 2 sp) and shifts/widens the pill until nothing crosses. Full: an
`HDR` row under `DISP`; Tiles: the *Display* tile's sub-line; Minimal: a line under the footer; Mega: a
fragment after `DISP` in the bottom band. Only the Fusion HUD shows the display server at all; the
classic, GameHub and GameNative HUDs never received it and are untouched.
### 11.6 Round 2d (Fold evidence from 2c)
- **The format scan was a false negative.** FG with the AIO HDR card logged `lists no HDR10 swapchain
format (37/0 37/1000104001 … 37/1000104012)`: `swap_init` read the surface's list into
`VkSurfaceFormatKHR fmts[32]`, and with `VK_EXT_swapchain_colorspace` the Android WSI lists every
format once per colour space (11 on the Fold) - the 10-bit/FP16 rows were past entry 32, and the log's
160-char buffer cut the list too. Now: all pairs read (heap), one line with the total, colour spaces
per format and every HDR-capable pair (10-bit/FP16 first); HDR10 pick A2B10G10R10 → A2R10G10B10 →
FP16 → 8-bit; a driver that refuses the HDR10 swapchain gets the SDR one at once. The only other
fixed array near it (queue families, 16) does not hold surface formats or present modes; FIFO is the
only present mode used, so no list is read.
- **VK_EXT_hdr_metadata** enabled in HDR sessions where the device lists it; the HDR10 swapchain gets the
game's SMPTE 2086 / CTA-861.3 once per swapchain and image description (none if the game sent none).
- **FP16 frame generation for HDR.** The bridge and both engines already take a format (the chain,
the ring); lsfg-vk itself runs HDR in `R16G16B16A16_SFLOAT`. With HDR frames through an HDR10
swapchain the scene (`g_fgscene`), the encode pass (new FP16 output), the effects and the engine run
in FP16 where `fge_format_ok` (storage + linear sampling) passes; the PQ signal is fed as-is (the
shaders' own HDR flags stay off - PQ is already perceptual). A chain that fails to build in FP16 is
sticky inside both engines, so the bridge refuses the format for the session and restarts the engine
in 8 bits instead of marking frame generation failed.
- **Evidence beside the headroom** (app, non-root): PowerManager thermal status (+ listener) and
Loaded 100 of 969 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.