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).
94 KiB
DroidDeck - Progress Log
Running engineering log for the DroidDeck app (com.droiddeck.launcher; called SteamDeck,
com.steamdeck.launcher, until 2026-09-23 - entries below that date keep the old name). 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-10-10 - feat/wincomp-auto-sources: Windows components picked by the game's source
- Per source (
WinCompSources): Steam - Steamworks Shared depots from the appmanifest or the client's app info (SteamAppInfo, appinfo.vdf v27-29, cached per appid, weekly retry); GOG - the build'sdependencies(now kept in the sidecar); Epic - the manifest's prerequisite (now parsed and kept); Amazon - fuel.jsonPostInstall; Custom - Steam's list when its files name the appid (on by itself), when only the name matches (Recommended); else the folder scan. Unmapped entries are listed with their own names; mapping tables in one place. - One Steam match per game (
SteamMatch,files/steam-matches.json), used by the art fetch and the components alike; the earlier art lookup is taken over. - On by itself (
AutoComponents): at store install, add/edit, page open and before a launch from DroidDeck;wincomponents.jsonkeepsauto(with reasons) anduserswitches beside the effectivegameslist (still version 1). A user switch always wins. The page gets "On for this game" with reasons and "Also listed". - Device fix: on a Steam game (Need for Speed Payback) only the already-downloaded d3dx9 went on; vcredist2013/2019 stayed Recommended, never downloaded (Steam games have no install event here). Every refresh (page open, launch, an app-start sweep over Steam games, install/add/edit) now queues the missing automatic picks for a background download and turns each on when it lands; the open page shows the progress and reloads.
- Gentle on the device: one low-priority worker and one queue (games and components each once, what the user asked for first); nothing while a session runs; the start-up sweep and pre-launch looks only queue, wait ten seconds after start, and skip battery saver, a warm device or a metered network; runtime helpers under nice 19 / ionice idle; a game's list kept and worked out again only when its files change, on the worker. Tests: download lands, queue paused while a session runs, unfavourable device, dedupe.
- Fluid: after a session the Games tab reads Steam's manifests first (no wait for the added games' walk), a newly installed game eases into the list and its components' list is queued on the worker. The components page shows "Checking what this game needs…" (small spinner) until the list is worked out, never "nothing found" before; "On for this game" expands in, components on their way sit there with their progress and a "Turning on N components…" line, and rows ease in (~250 ms; instant with animations off).
- Quieter: "Checking…" only when the game's list was never worked out; a kept list shows at once and is checked again silently, its changes easing in. The lists wait for the catalog and fade in once; "Loading the components…" only after 300 ms.
- Offline: with no validated network, components not downloaded yet are greyed and cannot be switched ("Available when online"), automatic picks on their way included (no progress bar); downloaded ones stay usable. The page follows the network live, and downloads wait in the queue until it is back.
- Every source, older installs: the start/after-session pass covers all games (Custom, GOG,
Epic, Amazon). GOG/Epic games installed before the sidecar kept the list get it once from the
store's build manifest with the saved sign-in (
StoreListBackfill; no game files; weekly retry on failure). Fresh installs keep the keys even when empty. - Apps the client never saw (Dragon's Dogma 2, 2054970 not in appinfo.vdf): the client ask
gave nothing (no
SteamClientcall returns depots), so Steam is asked anonymously instead (SteamPics: CM list, WebSocket CM, anonymous logon, PICS access tokens + product info, ≤25 apps a request, one connection a pass, on the worker; Kotlin, no new library). Prototyped first against Steam from the build host: 2054970 → depot 228988 (VC++ 2019), 1190460 → one shared depot. Cached in steam-appinfo.json like the client's, weekly retry on a miss. - Installed during a session: the Steam libraries' appmanifests are looked at every 5 s while
a session runs; a game turning fully installed gets one in-session job (waits while a game runs)
that writes its automatic picks and downloads only its missing components; the launch waits up
to 10 s for components listed in
wincomponents-pending.json, never when nothing is pending. - Checks: a store list counts as read only when the manifest was read whole (
listRead; GOG: a manifest with depots, Epic: the prerequisite fields read); older empty lists are fetched once more. Log:backfill: <game> gog deps=[...]/epic prereq=<name|none>, plus the manifest's depot/file count. Unity's*_Data/Managedis skipped by the folder scan (Mono assemblies, not .NET Framework). - Docs:
docs/development/windows-components-sources.md. Tests:WinCompSourcesTest(each source's fixture, certainty rules, appinfo reading, auto picks, user overrides, shared match),AddedGameArtLookupTestupdated. Not yet on a device.
2026-10-10 - fix/games-folder-migration: Games folders become entries, not Steam libraries
- Seen on a device: Steam's Install dialog listed "Games (Internal shared storage)" and "Games (android)", the user's two Games folders saved before #492, still registered as libraries while Second library is Automatic; with the settings section gone they could not be removed.
- Migration (
GamesFolderMigration, once at app start and before any scan, idempotent): every game a Games folder listed becomes an entry (AddedExes), keeping its chosen exe, stored appid, name / Start in / launch-option overrides, art choices and removal (all keyed by its folder). Store installs andsteamappsare skipped. The folders stay bound at their old guest paths (SessionPrefs.gameFolderBinds), so the listing and every Steam shortcut are unchanged;addedGamesDirsis cleared. Log:games: migrated <root> → N games. - Libraries:
GameStorage.gamesFolderLibrariesreturns only a Games folder Steam installed into (an appmanifest in itssteamapps/); the others drop out ofBL_STEAM_LIBRARIESand the runtime'sdroiddeck-steam-libraryforgets the/root/Games/...entries it had registered. The user's two folders havesteamapps/without manifests (checked on the device), so both go. - Tests:
GamesFolderMigrationTest(exe / appid / name / removal kept, guest paths unchanged, idempotent, store and steamapps skipped, libraries empty, same-name folders),GameStorageTestupdated. Not yet on a device.
2026-10-09 - feat/games-add-edit: add games from the Games tab, edit them like Steam's Properties
- Steam ⚙ settings: the Games section is gone (it was a feature, not a setting). Games folders added before keep listing their games.
- + beside "Games N": a compact in-app picker (storage step, then the storage root; every
folder A-Z, unreadable ones greyed;
.exeonly; width fixed at opening; red X; B goes up). Picking an exe adds the game (name fromGameIdentifier, art, Custom chip, Steam shortcut via the listing and the running client, Start in = exe folder) and selects it; an exe in a listed game switches its target; the same exe again selects it; a path the session cannot see is refused. Internal storage anywhere now maps to/root/Storage. - "Add all games in this folder": each subfolder with its best exe (
GameExePicker, depth 5, "?" when unsure), listed ones skipped, then a summary (added, already there, rows that open the editor with back to the summary). - ✎ editor on a Custom game: Name, Target (ranked, "?", Other exe…), Start in, Launch options, Artwork (Cover, Background, Logo, Icon with thumbnails and sources; chooser with the game folder, Steam and SteamGridDB; Pick image…; Reset to automatic), Remove (two presses, files kept). Every edit goes through the same shortcut on its stored appid (listing + live DevTools); the writer keeps Steam-side edits and records them, and the app takes them over once.
- Setup: Artwork switch and SteamGridDB API key (user key sealed with the Keystore, wins over
BuildConfig.SGDB_API_KEYfrom theSGDB_API_KEYsecret). - Brought over from
feat/added-games-remove(not merged):GameExePicker,GameIdentifier+PeVersionInfo, the art and SteamGridDB work, the build secret, the + and the picker. - Docs:
docs/development/added-games.md. Tests:AddedExesTest,AddedGameEditsTest,AddedGamesCandidatesTest,AddedGameArtLookupTest,GameIdentifierTest,PickListingTest,AddedGamesRemoveTest,test_steam_shortcuts.py. Not yet on a device.
2026-10-09 - feat/store-credentials-keystore: store sign-ins sealed with the Android Keystore
- What:
filesDir/stores/<store>/credentials.json(GOG, Epic, Amazon: tokens, ids, names, expiry) is now an AES-256-GCM envelope ({"v":1,"alg":"AES/GCM","iv","ct"}) under a non-exportable AndroidKeyStore key (StrongBox if present, else TEE; no user authentication).StoreAccountsis the one layer;CredentialCipherholds the envelope and the key. - Upgrade: a plain file is sealed at the first start (verified, then replaced); users stay
signed in. Log:
stores: credentials encrypted <store>. - Fallbacks: no Keystore - stays plain, logged once, retried next start. A file that can never
open (bad tag, key invalidated or missing, not an envelope) - deleted, signed out (sign-in card).
Any other Keystore or I/O failure - file kept, store stays signed in but unavailable (actions show
<Store> sign-in could not be read: try again.), logged once, retried on the next access. Never a crash. - Session: the guest sees the envelope only; it never read the files (the Epic code comes over the request channel, written beside the launcher and deleted by it).
- Tests:
StoreAccountsCipherTest(round trip, sealed on disk, migration of all three stores, tampered file, lost key, key missing, garbage, no-Keystore fallback then sealed next start, transient Keystore / I/O failures keep the file and a later read succeeds, sign-out). Not yet on a device.
2026-10-08 - feat/private-logs: every log in app-private storage, shared as one scrubbed zip
- Where logs live:
files/logs/(LinuxRuntime.logDir), nothing underDownload/DroidDeckany more. Session folders (<day>-NN-<what>/),tools/(GuestCommand, Flatpak, Windows component installers) andstores/(the stores' engine log, one file a day) all sit there. The files directory is bound into every session at the same path, so the session script, gamescope and the guest tools write there as before. Thirty sessions kept; the Logs switch off still means a cache folder deleted at session end. Game save backups stay public inDownload/DroidDeck/Saves. - Moving the old ones: on the first start of this build, session folders,
tools/and loose*.logfiles move fromDownload/DroidDeck/intofiles/logs/(rename, else copy and delete; an item that fails stays). A marker records the pass;Saves/and anything else there is never touched, and the app does not readDownload/DroidDeck/again. - Share logs: one zip in
cache/share/, made on demand: the newest session folder (or the one the session screen shares), the last seven days ofstores/andtools/. Every text file goes throughLogRedactor.redactForShareon the way in - the existing rules (Steam tokens, JWTs, SteamIDs, account names, emails, own addresses, MACs, serials) plus URLs without query, fragment or userinfo and with token-bearing path segments blanked (GOG secure links),__token__,f_token,hdnts, OAuthcode=, and Authorization / Cookie headers - so a file written before a rule existed is clean in the zip. The zip is deleted at the next share or app start.SessionLogShareTestcovers it. Nothing was relaxed: the per-session scrub,scrubOlderand every existing rule stay as they were. - File manager: the Session logs shortcut opens
files/logs(the app reads its own storage). - On main after Stores (#476) and the KDE Plasma desktop (#473): the stores' log is already
files/logs/stores/stores-<date>.logand the zip carries its last seven days understores/. One set of rules scrubs both the stores' log and the zip (SecretScrub):StoreLog.redactLinekeeps URLs to their host and first plain segment, the zip pass keeps paths with token segments blanked, both blank token values and Authorization / Cookie headers; the two test suites pass on it. The Plasma desktop writes nothing outside the session folder (its output is the session'sdesktop.log), so there was nothing new to move. - Privacy scan fixes: the Steam login account (an email, 374 times) was still in older
folders' copied
steam/webhelper_js.txt(SteamUI: INFO: Login: OnLoginStateChange <account>): those folders carried a scrub marker from before the account rules, and the marker never expired. The marker is now named after the redactor's rules version (.scrubbed-r3), so every folder scrubbed under older rules goes through again at app start, steam/ and every subfolder included; folders moved out ofDownload/DroidDecklose every old marker (SessionArtifacts.unmarkMoved), so the same pass takes them whatever those markers said. New rules:OnLoginStateChange <anything>, andAccountName/account_name/username/loginfields (loginonly aslogin=, a quoted key or a VDF pair, so the "Login:" label stays). Thecode=rule no longer touches Proton's exception codes (code=c0000005,406d1388,80000003): a barecode=is blanked only right after?/&or on a line with OAuth keys (client_id,redirect_uri,state, tokens), and never when it is 8 hex digits; exchange / authorization codes are always blanked. Tests:LogRedactorTest(the login line with an email and with a plain name),SecretScrubTest,SessionLogShareZipTest(older and moved folders scrubbed on disk). - Device check (31 migrated folders): only 4 carried
.scrubbed-r3a minute in. Nothing was skipped - the pass was still running: one folder holds ~30 MB of Steam logs, ~30 s through the redactor, one folder after another (about 15 minutes for 31). The start pass now scrubs a few folders at a time (half the cores, at most 4, background priority), old markers go whether or not a folder's pass succeeds, a folder the process does not finish is taken up at the next start, and it logs one line:logs: scrubbed N folders under r3. Test: nine migrated folders carrying.scrubbed-2,.scrubbed-1or a stale.scrubbed-r3all end up scrubbed under r3 after one migration + start pass. - LAN addresses in the zip: private IPv4 addresses (10/8, 172.16/12, 192.168/16, 169.254/16;
e.g. the router in
LinuxNetworkLink: resolver: ...) become<lan-address>in the zip pass; public server addresses and dotted version numbers stay (LogRedactorTest).
2026-10-08 - feat/stores: GOG, Epic Games and Amazon Games in the launcher (in progress)
A new Stores section - the three storefronts' libraries and public catalogs, one download
queue for them, and every install registered in the Steam client exactly like an added folder.
Ported from Bannerlator's store clients (auth, API, library sync, download managers); its
Activities and XML are not ported, the UI is Compose, after the approved preview. The native
download engine is the Rust side's work (libdroiddeckstores.so, loaded lazily; the app runs
without it and Setup says "Stores engine not built").
- Where it lives. Rail item
storesunder Games, gated by Setup › Stores › "Show Stores in the rail" (SessionPrefs.gameStoresEnabled, off by default); a count badge while anything is downloading. The page is full width: four chips (GOG · Epic · Amazon · Downloads, a signed-in dot per store) and a cog at the row's end whose popup holds the section's own settings - which tab a store opens on (storesOpenTab, Library by default; applied on chip tap, never while the user is switching tabs by hand) and the speed tier (gameStoresSpeedTier, Balanced / Fast / Max = 16 / 32 / 96 requests in flight). Setup repeats the rows under the gate. A signed-out store shows its sign-in card first. Every install is added to Steam - the user's call: "Add to Steam shouldn't be optional, automatic" - so the switch an earlier build had (gameStoresAddToSteam, the sidecar'saddToSteam, the page's "Add to Steam" button) is gone; an older sidecar's field is read past and ignored, and the game page shows a passive "In Steam" chip. The Games hero's eyebrow for a never-played store game reads "Games storage" (the chip names the store), and "Custom game" for a hand-added folder; Steam titles keep their library label. - A store game is an added game. The install lands at
<Games storage>/Games/<Store>/<title>(the SD library's folder when one is chosen, else the runtime's own/root/Games/Stores) with a.droiddeck-store.jsonsidecar (store, id, title, exe, launcher, args, env, version, addToSteam, art).AddedGames.scannow walks those folders too andscanGamereads the sidecar for the exe and the name, so the game goes down the existing path:session/added-games.json→droiddeck-steam-shortcutsat the client's next start. The listing is rewritten at install time, and with a client running the shortcut is added live over the client's DevTools port (SteamClient.Apps.AddShortcut+ our tag + the compat tool;SteamLiveShortcuts), best-effort. Uninstall removes the folder, the sidecar and the shortcut. - Launch. A shortcut can only name an exe and the listing carries no launch options, so a game
that needs arguments or environment gets a
.droiddeck-launch.batbeside it (Proton's steam.exe shim hands it to Wine's cmd): Epic's identity arguments, Amazon's FuelPump variables. Epic's per-launch exchange code is minted by the app right before a launch into.droiddeck-epic-code, which the script reads once and deletes; with no code the game starts in its offline identity mode. The EOS overlay is never provisioned. - Games tab badges.
Library.SteamGame.source(steam / gog / epic / amazon / added, from the sidecar) with a coloured chip on every row and in the hero eyebrow;LibraryCachekeeps it. - Downloads.
DownloadQueueruns 1-3 at a time, stages Manifest → Download → Verify → Install, pause = stop and keep the files (every engine skips verified files on the rerun), cancel deletes;StoreDownloadServicekeeps the process alive meanwhile; the page shows speed, ETA, the tier, downloads-at-a-time and the engine log. - Tests:
StoreGameSidecarTest,AddedGamesStoreScanTest(JVM),tools/tests/test_store_shortcuts.py(the writer with a launcher .bat as Exe, the appid the app computes, removal). - Files:
stores/*(Store, StoreGameSidecar, StoreInstallRoot, StoreInstalls, StoreLaunch, StoreAccounts, StoresState, StoresNative, SteamLiveShortcuts, download/*),ui/StoresPage.kt,ui/StoresDetail.kt,ui/StoresDownloads.kt,ui/StoreChips.kt,ui/StoresWidgets.kt;frontend/AddedGames.kt,frontend/Library.kt,frontend/LibraryCache.kt,ui/FrontEndRail.kt,ui/FrontEndContent.kt,ui/FrontEndSetup.kt,ui/FrontEndGames.kt,ui/FrontEndScreen.kt,MainActivity.kt,session/SessionPrefs.kt,AndroidManifest.xml(the download service),res/values/strings.xml. - The three stores (
stores/gog,stores/epic,stores/amazon), ported from Bannerlator's clients with their storage replaced: GOG's OAuth page (Galaxy identity, a browser identity for the social-login hop), library fromembed.gog.com+api.gog.com, shelves fromcatalog.gog.com, gen2 chunks / gen1 ranges / plain installer; Epic's web login (the code off the redirect's JSON page), library service + catalog, the store's GraphQL and the free-games feed, ChunksV4 manifests with install tags and a delta pass, the manifest's launch exe; Amazon's PKCE device sign-in, entitlements,manifest.proto(the app's xz library), SHA-256 per file,fuel.json/ exe scoring, FuelPump variables; Amazon has no public catalog, so its Store tab is cut from the account's own games. Each manager hands the byte-fetch loop to the native engine whenlibdroiddeckstores.sois there (GogNative/EpicNative/AmazonNative, JNI.md's names and signatures) and runs its Java pool otherwise. - Left out on purpose: GOG DLC installs and GOG redistributable installs (Steam seeds the shared redistributables in the prefix as for any shortcut), Epic's Denuvo ownership token and overlay, Amazon's SDK DLL deployment into the prefix (the prefix does not exist before the first launch), cloud saves, and the stores' social tabs - none of them in the preview.
- Card art (device feedback on
61a9a48, GOG › Library washed out to white): the GOG library had drawn the product'simages.background- the store page's fade-out backdrop, white once its alpha is gone. The library now takesapi.gog.com/v2/games/<id>_links.galaxyBackgroundImage(the Galaxy client's dark 16:9 library art) andboxArtImage(the vertical cover), cached with the sync (cache key bumped so old caches re-fetch); the catalog'sproductIdfilter was tried and ignored by the service, so it is one call per owned game. Epic cards preferDieselGameBox/DieselGameBoxTallover the offer images; Amazon prefersbackgroundUrl2and never the logo. Cards draw over a dark surface with a fade into the card at the foot. On installStoreArtplacescover.jpg,hero.jpgandheader.jpgin the game folder, re-encoded as opaque JPEGs over a dark ground, under the namesAddedGameArtlooks for - so the Games tab and the client's grid (through the listing) get real art for store games. - Device feedback on
9f5a2f3(GOG › DOOM I Enhanced page): (1) the pad lost focus when a card opened its page (the card left composition; the next press landed on the rail). The page now follows the Flathub store's pattern: a focus move is recorded on each change andfocusWithinFrameslands it - the game page's main action (or its back link), the card the page was opened from on the way back, the first card after a tab or chip change (firstTile); the page is afocusGroup, so Left is the way to the rail. (2) GOG's account endpoints return template keys (product_description_2015545325<br>product_feature_…) for some products; the v2 record has the same keys for them.cleanStoreTextdrops those keys, strips tags, decodes entities; the library tries the product's lead, then v2overview, then v2description, and shows nothing when all are templates. Epic's and Amazon's text goes through the same cleaner. - Speed shown = install rate. Device evidence (Epic › Metalstorm, 1.68 GB on Max to the SD
card): the engine's windows burst at 0.4–20 MB/s with 0–3 of 20 requests in flight while the card
writes at 26 MB/s - the fetchers wait on the write side, so the engine's
speedBpsjumped while the bar advanced at the write rate.DownloadQueuenow measures the shown speed itself from the byte deltas (an exponential average over 3 s, sampled at most every 250 ms) and derives the ETA from it, for all three stores; the engine's own figure stays in its log lines, and the detail line under the bar has it stripped. The tier and downloads-at-a-time controls sit on one compact row, on the Downloads page and in the cog's popup. - Install location (user decision): the default is the app's internal storage for all three
stores, whatever the session's Game storage says. With a card in the device, Install asks
"Install to" first (internal / the card, free space per target, pad-focusable, last pick as the
default only). A card that is not the Steam library gets a bind of its own in the session
(
/mnt/droiddeck-stores/<volume uuid>; the library's own bind covers a library card, so installs already registered there keep their appid); every root is scanned and uninstall works for either. The Downloads row shows where a game is going; the game page names the place. Epic installs on a card keep their in-flight chunks in the app's cache: the engine'schunkCacheDir(fetch and the newnativeAssemble, which drops each chunk after its last use) and the manager's own loops read the same path; internal installs pass""as before. Cleaned on cancel-with-delete and uninstall. - Half-installed Epic game shown as Custom (device, Metalstorm on the SD card). The folder had
2.85 of 4.4 GB of files, a 0-byte file where the assembly stopped (19:15:10, four minutes after it
began), 9.1 GB of
.chunks, and no sidecar. The run stopped inside the assembly loop - before the.chunksremoval and the sidecar, both of which only ran at the very end; the events log does not reach back far enough to say whether the process died or the loop threw. Either way three things made it silent: the sidecar was only written last, the queue lives in memory (a process death drops the row), andAddedGames.scanlisted any sidecar-less folder under a store root as a Custom game. Now: the sidecar is written at the start in stateinstallingand finished at the end; unfinished folders (installing, or no sidecar) are never scanned as games; the Stores card reads Resume install, which reuses the folder and its.chunks; launcher/sidecar write failures fail the download, art and Steam registration are best-effort. The 9.1 GB cache is whole 1 MiB chunk windows shared with files outside this device's install tags - the free-space check now counts cache and files; a successful run removes the cache (Kotlin loop andnativeAssembleboth). - The cog popup is capped at 440 dp, like the Install dialog.
- Store cards are art + one line (maintainer feedback): no source chip (the tab says the store), no "In library" chip, no action button. The title sits left; right-aligned is a check when installed, Resume, the download percentage, the install size, or Free / price / discount. A thin bar over the art while downloading. Install, Resume, Uninstall, Get and Buy live on the game page; A on a card opens it.
- No log panel: the Downloads page's engine log is gone. Store lines go to logcat and to
app-private daily files (
filesDir/logs/stores/stores-<date>.log, seven days, ~2 MB rolling to.1), pruned as the app starts; the session's Share logs zip includes them understores/. - Store log lines are redacted at the one choke point (
StoresState.logLine, which the engines' lines reach too): every URL is cut to scheme, host and a plain first path segment (a GOG secure link's token sits in the path, so it goes), no query, fragment or userinfo; token-like values (token,__token__,f_token,hdnts, access/refresh/id tokens,code=) and Authorization / Cookie headers are blanked outside URLs too.StoreLogTestcovers a GOG secure link and an Epic Akamai URL. The engine's own logcat lines (tagEpicNativeetc.) are written on the native side. - GOG downloads use every CDN host: the secure link lists several hosts; only the first was
taken (
used=1/1 servers). All are parsed now and kept as one set, refreshed together on expiry; the Java pool takes them in turn. The progress bars glide to each new value over 250 ms. With the engine's0849a7cthe native start takes the whole set (cdnBases; the engine spreads its window across the hosts) and itsonBytes- every 250 ms, including files still in flight - drives the bar and the speed, so a large file no longer moves the bar in one jump. - Cloud saves device-proven (AYANEO Pocket FIT,
6a175f1, ELDERBORN on GOG): the launch loggedcloud gog 1732383191 down result=ok files=5 bytes=237499 reason=downloaded(template found) and the in-game slot shows the cloud save (Catacombs: Tutorial part 1, 00:08:40). - Store cards a quarter smaller: 112 dp least width (was 150) for shelf cards and grid cells; the grid fits as many columns as the width allows and shares the rest, so a density setting changes the column count; titles stay at 11 sp, art 16:9. Downloads rows unchanged.
- Cache first, refreshed in the background: a store's library shows from its saved copy at once
and is refreshed at most every six hours (or from the account row's Refresh), the result merged
so unchanged cards keep their objects - only changed cards recompose, the grid's scroll and the
pad's focus stay. Shelves saved on an earlier run count as fresh for three hours. Sizes are kept
per game and version in
filesDir/stores/<store>/sizes.json(format-versioned) and looked up again only for a new version. An installed card shows its size on disk with a small green check after it. - Every owned card shows its size at the right of its title strip, in the title's type: GOG's from
its catalog; Epic's (build manifest, the files this device installs) and Amazon's (download
manifest) looked up once a card is on screen or close, three at a time in the background, kept in
the store's prefs (
size_<id>). Nothing shows until it is known; a marker (installed, Resume, a download's percent) keeps the slot. - Two tabs, Store and Library (the All count mixed owned games with whatever shelves had loaded:
GOG Library 33 / All 120): Library's dropdown filters All (every owned game, the default, the
tab's count) or Installed, and the tab reads the filter ("Library (33)", "Installed (1)"); the old
All view is gone. Amazon has Library alone. Search follows the filter; the shoulders cycle Store
and Library. The dropdown opens under the Library tab, left edges aligned, as wide as its
entries (at least the tab, at most 260 dp) -
AnchoredMenu(compact = true). - The Stores chip row paints nothing (maintainer: a black box behind the chips): it sits over the page's own background; the content scrolling under it is clipped at its edge and faded there by the content's alpha once scrolled, never by a colour.
- Pre-PR device checks pass (AYANEO Pocket FIT): GOG install, launch and cloud saves down and up
(the session-end upload included); Epic with EOS sign-in; Amazon (Dread Templar); an SD-card install
(DOOM I Enhanced) launched from
/mnt/droiddeck-sd. - Downloads page, icons and one Clear (maintainer): the row actions are round icon buttons - pause, resume, play, retry, and a cancel that asks with a red delete and an undo - their labels as descriptions. Downloads under way sit on top; finished ones below a line whose X clears them all (rows only; a failed download's kept files stay for its game page's Clear), installed ones as a compact row with Play.
- Saves menu without explanations: Import / Export rows (Steam, Custom, GOG and Epic alike) lose their descriptive subtitles; only live values remain (last sync, "…", conflict count, the save folder and its size). The three strings are gone from every locale.
- One place for a store game's saves: a GOG or Epic game's hero button reads "Cloud saves" and opens its saves view, which now holds the Cloud saves switch, the sync status, Upload / Download and the conflict actions ("No cloud saves" when the store keeps none). The switch is gone from the cards; GOG has no card left, Epic's keeps sign-in, offline and Resolve Epic sign-in. Steam and Custom games keep "Manage saves". The view is laid out in full from the first frame - the switch (focused), Upload and Download with "…" until the cloud check is back - and only subtitles change after; a conflict turns the two actions into Keep local / Keep cloud in place.
- Background downloads: while a download runs the service holds a Wi-Fi lock and a partial wake
lock (timed, renewed with the progress, released when nothing runs). The notification shows a
real progress bar and " · 42% · 1.2 GB/2.5 GB · 12.3 MB/s · 3 min" (the stage while
installing), with Pause / Resume and a two-step Cancel (Delete / Keep, ten seconds); a paused
download leaves a notice to resume from; " installed" opens it in Games, " download
failed" opens Downloads. Notifications are asked for once on Android 13+. The Android 15 dataSync
time limit (and its
onTimeout) applies only to apps targeting 35+: this app targets 28 and compiles against 34, so there is nothing to handle yet. - Cloud uploads do not depend on the exit hook: a dirty mark per game launch; uploaded on the
first of exit request, session end, or the next app start (
trigger=exit|session-end|recovery). - Cloud saves, device round 1 (ELDERBORN,
b323a0b): nothing ran - a GOG game without arguments gets no launcher .bat, Steam runs its exe, anddroiddeck-store-launchonly recognised the .bat. It now recognises the exe too (searching the folders above it for the sidecar), and every path logs a result line. A first launch has no prefix yet: the download is deferred until Proton makes it, without holding the launch. Uploads are now safe: none before a download has set this device's baseline, a file changed on both sides is a conflict left for Keep cloud / Keep local, and the cloud copy is backed up before an upload replaces it (CloudPlan, tests for each rule). - GOG and Epic cloud saves (ported from Bannerlator's managers, device-proven there in August):
down before every launch where the cloud copy is newer (the compat tool waits up to 15 s), up
after the game exits (the compat tool waits for Proton for such a game, then asks without
waiting), newest wins per file with an MD5 check, a backup of what a download overwrites (last
three). The save folder comes from GOG's remote-config template or Epic's CloudSaveFolder,
expanded inside the game's prefix (
CloudSavePaths, Bannerlator's cases as tests). A "Cloud saves" switch on the GOG / Epic card, and cloud rows in Manage saves (last sync, Upload, Download, "No cloud saves"). Not yet device-tested. - Epic sign-in device-proven without the overlay (AYANEO Pocket FIT,
bf81104, Metalstorm): EOS's corrective-action browser flow →droiddeck xdg-open: https://www.epicgames.com→ Chromeepicgames.com/id/authorize?user_code=…→ approved →[eos] got logged in, product user id created, the game in its hangar at 92 fps. The EOS overlay crashed the game on every overlay-on launch, so it is removed (user decision): no overlay switch, no download, noOverlayPath; a launch removes a pointer an earlier build wrote, and the app deletes an earlier 656 MB download once. The work is parked onpark/epic-eos-overlay(atbf81104). Kept: the browser hand-off, the Epic card (sign-in, offline, Resolve Epic sign-in) andBL_DEBUG_BROWSER. - A game launched from the Games tab with no session running sat at "Launching" (device,
2026-10-09-04-steam, and 2026-10-08-05 before it). The session put
steam://rungameid/<id>on the client's command line; the client took it seconds before its interface was up (focus went to 769 at 04:29:07, the launch at 04:29:09) and its launch stopped atLaunchApp waiting for user response to ShowInterstitialswith nothing to answer - the next launch from inside Steam went straight through. Not store-specific: the 2026-10-08 cold launch of another shortcut stopped the same way.droiddeck-sessionnow holds such a URL back and hands it over through the samesteam-gamerequest an in-session launch uses, once gamescope's focused app is the interface (769) and four seconds have passed, or after two minutes regardless. Therc=139at the end of both sessions is the client crashing in its own shutdown, after "Shutdown", unrelated to the launch. The shortcuts writer keeps a compat tool the user chose (plan_mappingskips an entry that is not one it set itself), and the live add sets one only for a new shortcut. - Why EOS's browser flow never reached Android: the hand-off script was never in the APK. The
build copies only
usr/local/bin/droiddeck-*,steam-compatibilityandusr/bin/**of the overlay into the assets, sousr/local/lib/droiddeck/browser/xdg-openwas skipped, the session staged nothing, the wrapper did not put it on the PATH, and Wine's winebrowser ran the runtime's own/usr/bin/xdg-open, which has no browser to open in a Steam session. GE-Proton 11-7's winebrowser (Metalstorm's tool) is Wine's own - registryBrowserslist, thenxdg-openby name - with no steam://openurl route. The script is nowusr/local/bin/droiddeck-open-url: named as Wine's browser in the prefix ("Browsers"="/usr/local/bin/droiddeck-open-url,xdg-open", an absolute path that does not depend on Wine's PATH), linked asxdg-openfirst on the game's PATH, and reading the session channel from/tmp/droiddeck-browser/launch-dirwhen Wine does not passBL_LAUNCH_DIRon. Whether EOS calls ShellExecute at all in its browser flow (rather than handing a verification URI and code to the game to show) is what the next run'sdroiddeck xdg-open:line, orBL_DEBUG_BROWSER=1in droiddeck-env (WINEDEBUG=+shell,+winebrowser), will show. - Resolve Epic sign-in (the Epic card): opens Epic's account page behind its sign-in in Android's browser, where a pending corrective action (privacy policy, EULA) is presented.
- Epic overlay off by default (device A/B, Metalstorm: overlay pointer set → exits while
loading, twice; off → signs in to its Starform account and runs at 118 fps).
EpicOptions.overlaydefaults to false; the sidecar'sepicblock carries"v": 2, and an older block (the old default wrote"overlay": trueunasked) reads as off - in the app and indroiddeck-store-launchalike. The 656 MB overlay is fetched only for a game that turns it on (the card's switch, or a launch of such a game), no longer after every library sync. - The card's switches are saved where both launch paths read them: each toggle reads the
sidecar fresh from disk, writes it and reads it back (
StoreGameSidecar.updateEpic), and the card shows what was read back; the app logsepic options <id> eos= offline= overlay=. - Registry lines that a running wineserver would drop:
droiddeck-store-launchwaits up to 8 s for a wineserver still holding the prefix to exit before appending touser.reg(it rewrites the file from memory when it exits, which is howOverlayPathvanished), reads the value back, and logsoverlay-set,overlay-off,overlay-not-writtenoroverlay-still-set, plusserver-runningwhen one never left. The helper's docstring is raw (no SyntaxWarning). - Browser hand-off diagnostics: the game-PATH
xdg-openlogs every call asscheme://hostonly and acceptssteam://openurl/<address>. On the device it was never called while EOS waited on its browser flow, so how EOS opens the browser under this Proton is still to be seen in the next log. - The exchange code no longer reaches the game's environment (device: Metalstorm's Player.log
dumped
DD_AUTH: … -AUTH_PASSWORD=<code>). The launcher read the code into a variable the game inherited. It now clears the variable on the same line that starts the game - cmd has expanded it there already - so the code is on the command line only; the variable has a plain name. An old launcher is rewritten before the next Epic launch (StoreLaunch.refreshLauncher, from both the app and the Steam-launch request). - A store game page shows no description that is only its title again (Metalstorm's).
- Epic launch card (Games tab › Launch settings, Epic games only): "Epic sign-in (EOS)" on,
"Launch offline" off, "Epic overlay" on, stored in the game's sidecar (
"epic": {"eos", "offline", "overlay"}, kept across updates) so both launch paths honour them alike - the app'sStoreLaunch.epicCodeanddroiddeck-store-launchfor a launch from the Steam client: no code when sign-in is off or offline, the overlay pointer written or removed per the switch. The card reads e.g. "EOS · Overlay". Bannerlator's forced ownership token (-epicovt) is not ported, so there is no switch for it. - EOS overlay (device: Metalstorm's EOS grant answered
corrective_action_required, the overlay was not configured, and EOS fell back to a browser flow that never finished). Epic's own overlay component ("EpicOnlineServicesOverlay", the mechanism from Legendary'slfs/eos.py, credits inEpicOverlay.kt) is downloaded once, with the Epic account's token, to/root/.local/share/droiddeck/epic-overlayin the runtime - after an Epic library sync, or at the first launch that finds it missing.droiddeck-store-launchthen writes the one pointer,HKCU\Software\Epic Games\EOSOverlayPath= that folder through Z:, into the game's prefix (STEAM_COMPAT_DATA_PATH/pfx/user.reg, appended as DirectAudio's key is; a prefix Proton has not created yet gets it on the next launch). Logged asepic overlay installed=<build> prefix=<appid>. A game's web links now open on Android: Wine's browser is set to xdg-open, and on a game's PATHdroiddeck-open-urlhands http(s) addresses to the app, which opens them with Android's browser - so a device-code sign-in can be finished on the phone. - Epic sign-in from Steam's own Play button (device, Metalstorm "Guest Account"): the game was
started from the Steam client, so the app never minted its exchange code - the code was only
written when a launch began in the app. Now the compat tool (
droiddeck-protonanddroiddeck-proton-wrap,steam-compatibility) runsdroiddeck-store-launchon every real launch: it recognises the store launcher among Steam's arguments, and for an Epic game asks the app through<session>/stores/req|respand waits up to 5 s; the app (StoreLaunchRequests→StoreLaunch.epicCode) refreshes the token, mints the code and writes it into the game's own folder - the code never crosses the channel or the log. No answer: the game starts with its offline identity. A code file older than five minutes is dropped first. Every attempt logsepic launch id=<app> code=yes|no reason=<ok|signed-out|sign-in-expired|exchange-failed|timeout|…>; a sign-in that ran out dims the Epic chip and shows the sign-in card. The Games tab and Stores still prepare a code before they launch; the compat tool's request is the one every path shares. - Amazon has Installed and Library only: with no public catalog its Store tab repeated the library (Trending = Your library) and All equalled Library. The open-on Store choice falls back to Library for Amazon. Amazon's library API carries no install size, so a size shows only once a game has been installed (the manifest gives it then); no placeholder otherwise.
- Shelves are titles only (no "Just landed" / "On sale this week" lines); the chip row sits on the page's own ground with room under it, and what scrolls passes beneath it clipped, with a short fade where it meets the row.
- Show mature content (Setup › Stores and the cog; off by default): the storefront's shelves
and search leave out titles the store itself rates or tags as adult; Library, Installed and owned
games always show, and a title without rating data stays. GOG:
ratings[].ageRating17+ (ESRB M 17, PEGI / USK / GOG 18) or the tagsmature,nsfw,sexual-content,nudity- the catalog API has no server-side filter for either, so it is client-side. Epic: its public store GraphQL carries no age rating on an offer (no age-gating or rating field on CatalogOffer or StoreConfig; introspection is off), so only an adult content tag would mark one, and none of the probed titles have one - Epic's storefront is effectively unfiltered. Amazon's Store tab is the user's own games. - UI slow while downloading (device): engines report per chunk or file, and each report
published a new queue that the whole front end read (the rail badge was computed from it in the
root composition). Now progress-only changes reach the UI at most four times a second (stage and
state changes at once), each row and card reads only its own entry, the list replaces itself only
when rows come, go or change state, the badge count changes only when it changes, and the
notification redraws at most once a second. Download threads run at background priority (the
queue's runner and every Java pool), the tier leaves two cores free (
processWorkerscapped at cores - 2, at least 2), and the stores log is written in half-second batches. The native engine's own threads need the same priority on its side. - Installed tab empty under "Installed (2)" (device): the count came from the installs on disk,
the grid from the library filtered by id. Both now come from
installedCards: each install joined to its library item by id (or title), else a card from the sidecar's title and art. - GOG rows (device, DOOM + DOOM II): bytes read "0 B" instead of blank; GOG and Amazon report
their size on disk from the manifest; their Install stage counts its finishing steps (record and
launcher, art, Steam) instead of sitting empty - GOG writes files in place while downloading, so
there is no file move to count. A stage a store skips reads complete once a later one starts.
The tier's window does reach GOG's engine (Max = 96 as its ceiling); the plan line now says so
(
gog: plan … window=96 … tier=max). The per-host spread is the engine's and was not changed. - Stages named for what they do; both sizes; Cancel deletes. Epic's check of the files already
there is part of Manifest ("Checking 120/515"), never a 100% Verify before anything is fetched.
Install has its own write rate and ETA from the bytes the assembler writes, and files n/N. The
row shows the download (compressed) as "1.6 GB / 2.5 GB" and the size on disk beside the
location; Epic remembers the disk size for the card and page. Cancel asks twice ("Delete
download?") and removes everything the install wrote - folder, sidecar,
.chunks, scratch,.gog_chunks- except a finished install being repaired, which keeps itself; the row says Cancelled for three seconds, then goes, and the game reads Install. Pause keeps everything. A failed row reads "Failed · 2.1 GB kept" (the reason is in the log) with Resume and Clear (Clear deletes like Cancel); the game page offers Resume install and Clear for an unfinished install. Library sync is a thin bar and "25/55", no sentence. - Per-stage progress (device: Epic DOOMBLADE, 13 GB, ~3 min of assembly with a still bar). The entry carries the active stage's own count (amount + files) and which stages are passed; the Downloads row draws one segment per stage (Manifest, Download, Verify, Install), and its line reads e.g. "Installing 42% · 120/515 files" (speed and ETA only during Download). Fed by Epic's verify pass, its native and fallback assembly, and GOG's chunk-cache removal; Amazon writes while it downloads and has no long stage after it. Leaving Download clears the measured speed. Cards and the game page follow the active stage's percent.
- The art is the card (maintainer feedback): a store card is its art edge to edge, sharp above and, under the one line, a blurred and darkened copy of the same picture (blur on API 31+, a stronger scrim below it). A Downloads row is the game's wide art cropped to the row with a dark wash from the left, the copy in white there and the actions on the right; no thumbnail.
- Install dialog: one plain line "Install :", then one row per place (icon, name, free space), Cancel small in the title line; 440 dp wide.
- No flavor text (standing rule from the maintainer): Setup › Stores and the cog popup keep only control labels; the download-controls caption, the Downloads footnote, the sign-in blurbs and footnote, the engine note and the Install-to caption are gone; empty states read "No games", "Nothing installed", "No downloads", "No catalog". A Custom game's hero has no eyebrow (its chip says it).
- Logos reverted (user decision, no third-party marks): back to the text source chips and the coloured dots.
- Device-proven on the AYANEO Pocket FIT, 2026-10-08 (GOG): sign-in, the library (33 owned), DOOM I Enhanced installed through the GOG engine to the SD Games root, shown in Games with the GOG chip and real art, registered as a non-Steam game in the Steam client (grid art present), and launched from the Games tab through the shortcut and droiddeck-proton to the main menu at 143 fps.
- Status: CI green (build, JVM and python tests). Epic and Amazon are still unproven on device - their sign-in pages, an install, and for Epic the launcher .bat with the exchange code are the next things to prove on hardware.
2026-10-08 - feat/directaudio-from-release: DirectAudio from its own release, picked by interface, and the client gets the real engine
Three things were wrong with audio at once, and one tidy-up @xXJSONDeruloXx asked for.
- Proton-CachyOS games never got DirectAudio. Both CachyOS Protons we offer carry Wine's newer
system-thread audio interface (
dlls/mmdevapi/unixlib.hdiffers from Valve's); the one driver we shipped was built for the classic table, and the wrapper only asked "is this Wine 11?", which CachyOS passes. Now two driver sets ship (linux-wine11for Valve Proton 11 / Experimental / GE-Proton,linux-wine11-systhreadfor Proton-CachyOS) andsteam-compatibilitypicks by what the Proton's ownwinepulse.soimports (PsCreateSystemThreadonly on the system-thread table) - never by the version string. No set for the interface, or nowinepulse.so: DirectAudio stays off and Proton's audio runs. Confirmed on the FIT: Experimental 20260924 and GE-Proton11-7 classic, cachyos-11.0-20261005-slr system-thread. - CachyOS also overrode the registry (device log:
init_driver Loading driver list L"pulse,alsa"): its launcher setsWINE_AUDIO_DRIVER=pulse,alsa(setdefault) and its mmdevapi reads that variable before the registry; Valve's and GE's mmdevapi ignore it. The wrapper now exportsWINE_AUDIO_DRIVER=directaudio,pulseas well as writing the registry. On CachyOS that also means DirectAudio engages on a game's first launch, no registry value needed. - Some games ran on PulseAudio while the log said DirectAudio. Two prefixes on the FIT had the
driver linked into
system32but a pristineuser.regwith noAudiovalue: Steam runs the compat tool several times per launch, and a wineserver from an earlier verb still holding the registry saves its copy over the line we appended. The wrapper now says so when a server is live. Two stronger fixes were tried on the device and withdrawn the same day: writing the value through the live server with a barewine reg add, and having Proton create a missing prefix first (proton run wineboot) - the second Proton run inside Steam's launch sequence got the launch aborted (KeyboardInterruptin Proton, games bounced back to their page, a half-made prefix). On Valve/GE a first launch therefore still takes DirectAudio from the second run. - The Steam client's own sound was "DirectAudio" in name only. PulseAudio → sink → relay →
AAudio is one hop more than a plain AAudio sink; the only gain was sharing the games' stream.
The daemon runs on the Android side, so DirectAudio's engine can run in it:
module-directaudio-native-sink(from the DirectAudio release) - the game driver's adaptive buffer, two-burst floor, primed ring with fades, route-change reopen, watchdog and limiter, one step from Android. It is now the only route: the Steam client audio choice is gone from the menu (the relay route was one hop more for no gain, and the old "Classic" sink is the automatic fallback). If the native sink cannot open its stream the daemon keeps running (.nofail) andPulseAudioComponent.ensureClientSinkloads the plain AAudio sink instead, saying so inaudio.log. The relay helper now starts only for games and the microphone. - Binaries out of tree (@xXJSONDeruloXx): the driver sets, the relay helper and both sinks come
from the pinned release in
tools/directaudio/release.env(directaudio-linux-v1.1.0, four zips, sha256-checked) throughtools/directaudio/fetch.sh, in CI andbuild_local.sh, like gamescope. Deleted:app/src/main/assets/directaudio/(now generated, ignored),tools/directaudio-relay/,tools/aaudio-sink/module-directaudio-sink.c+da_relay_proto.h.tools/aaudio-sinkkeeps onlymodule-aaudio-sink(the fallback). The release carries MaxsTechReview's #338 fixes, credited there. - Audio bundle stamp bumped so an installed app re-unpacks the modules.
- Device-proven on the FIT, four cases: the client's sink opened at 12 ms / 4 ms bursts with
0 underruns after 10 s (
client sink: DirectAudio (module-directaudio-native-sink)); FEZ oncachyos-11.0-20261005-slrtook the system-thread set - Proton logLoading driver list L"directaudio,pulse"→Selecting driver L"directaudio", relayhello from FEZ.exe, 12 ms; FEZ on Proton Experimental 20260924 onlinux-wine11, relay hello, 12 ms (no regression); Curse of the Dead Gods on CachyOS from a cold prefix - DirectAudio on its first launch, one early xrun and the buffer grew 12 → 16 ms. Open: a Thor (20 ms bursts). An added game (Noita, a Winlator-era folder never launched here before) bounced on launch with and without this branch's wrapper steps, before Proton wrote a log - its own issue.
2026-09-29 - feat/controller-input: the pad the way SteamOS has it
Device-tested on the AYN Thor (Katamari under Proton Experimental ARM64).
- Why: the client read the pad as an Xbox 360 controller, and games read the same node
wearing Steam Input's virtual identity (28de:11ff), because the client's own virtual pad needs
/dev/uinputand never appeared. Steam Input's layouts therefore never reached a game, and the QAM was a timed Guide+A chord. InputPlumber itself cannot run in the sandbox (root, uinput, uhid, udev, D-Bus), so what it provides is emulated in the interposer instead. /dev/uinputstand-in (FAKE_EVDEV_UINPUT=1, libfakeinput): a gamepad the client makes becomes/dev/input/event16+, backed by a ring of the app's own format, with the client's name, ids and bits; its writes are that ring's events. Other devices (virtual keyboard and mouse) are accepted and dropped.inputudev.cdescribes these nodes to Wine's HID bus from a.ueventfile beside each one, and while one exists describes the app's pads as the Xbox 360 pad they are, so Wine reads only Steam Input's output. Verified: the client makes "Microsoft X-Box 360 pad 0", re-makes it at game launch, and the game binds it (Controller 0 uses xinput : true).- Steam Deck controller (
FAKE_EVDEV_DECK=1, Steam sessions):/dev/hidraw16, found through a sysfs treeSteamDeckPad.ktbinds in (usb_device → interface 2 → hid 28de:1205 → hidraw), streams the Deck's 64-byte state report every 4 ms from ring 0 and answers feature reports as InputPlumber does. systemd 261's libudev refuses a syspath not on sysfs, sofstatfs/statfsreport sysfs for that tree. The app's evdev nodes are withdrawn while it is on, and only thesteamprocess sees the Deck. QAM is a real button (snapshot bit 11). Verified: the client lists a Steam Deck Controller (V1 HID protocol) and runs its handshake (0x83, 0xAE, 0x81/0x87 lizard off, 0x8F). - Setting: Steam page → Touch & controls → Controller: Steam Deck controller (default) or
Xbox 360 controller (the pad of earlier versions, QAM by Guide+A; games still get Steam Input's
virtual pad). Debug switches:
droiddeck-no-uinput(back to the 28de:11ff disguise),droiddeck-no-deck-pad(forces the Xbox 360 pad). - Gyro:
PadMotion.ktfeeds the handheld's own gyro and accelerometer (4 ms sampling, while the session is on screen) into an IMU block after ring 0's events, turned to the screen and then to the Deck's axes and units; the Deck report carries them. Verified: Steam's Gyro Calibration page moves with the Thor (sh5001 IMU); at rest the accelerometer reads 1 g. - Deck grips + trackpads on the second screen (
DeckControlsPanel.kt, offered while the pad is a Deck controller): tabs for the four grips (2x2), either trackpad with a click bar, and both trackpads over the grips in a row; a second finger on a trackpad clicks it. True black for OLED. Grips, pad positions, touch, click and pressure go through the same block after ring 0's events as the gyro (DeckControls.kt) into the Deck report. - Not yet: a Bluetooth pad's own IMU (DualSense); the game's rumble on the virtual pad goes straight to the vibrator rather than back through the client.
- Prior art checked: WinNative and Bannerlator never emulated uinput or hidraw; Bannerlator's
-steamdeckmode stopped at "the virtual pad never arrives", which is this missing uinput.
2026-09-25 - main 8cdedbe: melonDS out of the box, touch in Big Picture, log privacy, Decky
State: main = 8cdedbe (PR #22 merge). Device-tested on the AYANEO Pocket FIT (repacked test builds)
unless marked. gamescope now staged from release gamescope-3.16.29-p3.
- PR #18 →
9369dc3(feat/melonds-preset) - melonDS and the session:- melonDS preset (
bannerlator-pad-defaults): the top screen as big as the panel allows with the bottom screen alone beside it, centred (Horizontal + Emphasize top; Hybrid repeats the top screen), sharp pixels; full screen from the front end (-f) at the panel's own resolution; the guide button toggles full screen, leaving to a 1x-wide 16:9 window whose menus gamescope stretches to a usable size; DS/DSi BIOS, firmware and NAND found inROMs/nds/{bios,firmware}(BIOS by checksum). Existing installs moved once. - Desktop:
QT_QPA_PLATFORM="wayland;xcb"(the melonDS AppImage ships only xcb and exited at once); a labwc window rule opens melonDS at 800x600, centred. - HUD follows the game window, not gamescope's 1x1 cursor or Qt menus (read 0 fps at 60).
- Cursor: hides on pad / on-screen-control input (Bannerlator's 1.2 s rule), and shows the program's own
shape - labwc's resize arrows, I-beams, a hide (Bannerlator's
wl_pointer.set_cursorport; new here: labwc's GPU cursor is a UBWC dma-buf, read back throughvkp_image_readback). - Steam desktop switching: "Steam Desktop UI" opens the desktop with Steam's desktop client in it (under
gamescope the client picks Big Picture regardless); the drawer's DESKTOP button does it from Big
Picture; relaunches restart the singleTop activity in place (
startActivityfrom it was swallowed). Big Picture's own Power → Switch to Desktop still hangs: it waits on SteamOS Manager before itssteamos-session-selectfallback (stub included; a session bus did not help - tried and reverted).
- melonDS preset (
- PR #19 →
ce0da01(fix/log-privacy) - no credentials or identities in session logs: the device's public addresses (the client's IPv6 check logs "external address" ~20x a session),network.txtaddresses as their kind and the MAC masked, every Steam AccountName/PersonaName (plain account names leaked in the UI log's login lines), and every text file scrubbed at session end and in the share-logs zip. Verified: 0 hits across 83 session folders. - PR #16 →
5270574(Decky Loader, Kurt) - merged with the Steam CEF debugging marker tied to the supervisor toggle (it was created at install; on Android any app can reach 127.0.0.1:8080 - proven from another uid). Decky's own updater still points at the official project (no ARM64 build there). - PR #20 →
036317f(Kurt) - local builds carry the CI payload (NDK proot, patched gamescope). - PR #22 →
8cdedbe(feat/steam-touch) - touch in Big Picture: gamescope0110bindswl_touchin the nested backend and feeds wlserver's touch path (Big Picture sets Passthrough: tap and swipe rows as on a Deck);0111makes the nested pointer warp in Passthrough (touchpad mode: hover and tap-to-click work); the compositor's pointer fallback no longer jams on a lost finger-up. - In design, not built: the drawer's page dots as QAM-style icons (Display / Controls / Settings), the
selected one "stepping forward" in a line; mock
droiddeck-drawer-tabs-preview.html.
2026-09-24 (afternoon) - main 94f9935: PS1, controller focus, own signing key, CI hardening
State: main = 94f9935 (PR #41 merge), main build run 36036219740 - the first one signed with
DroidDeck's own key (droiddeck-signed-standard). No tag, no release since 0.1.6. Device-tested on
the AYANEO Pocket FIT unless marked. main is now protected (below).
- PR #34 →
63d7051, PR #38 →c7998a8(feat/duckstation,feat/duckstation-play) - PS1 and the front end:- DuckStation like ARMSX2: Player 1 on the controller, BIOS from
ROMs/psx/bios,psxas the game list, Vulkan, guide → pause menu,-batch -bigpicture -fullscreenfrom the rail. Crash Bandicoot and Tekken 3 at 60 fps from the desktop and from the front end. - One tap to play: DuckStation's and ARMSX2's first-time setup is answered for the user
(
SetupWizardIncomplete = false); ARMSX2 names its BIOS dump itself ([Filenames] BIOS, a US dump first, re-checked before every launch) - PCSX2 needs a named file, DuckStation finds one in its folder. DuckStation no longer warns it cannot inhibit a screensaver (noorg.freedesktop.ScreenSaverhere). - One tile per disc: a
.cue/.gdi/.m3uhides the files it names (Tekken 3 showed 4×, one per(Track N).bin);bios/firmwarefolders are skipped (the BIOS showed as a game). - Controller focus: the rail item is outlined on open (an app starts in touch mode, where clickables
refuse focus - the app now asks for keyboard mode first); Right enters a page on its main button,
Left from the page's edge returns to the rail, Right again returns to the tile or button last used
(explicit tracking -
FocusRequester.saveFocusedChildrestored into the grid, not the tile); a page opened from a tile takes focus on its main button (focus used to vanish with the tile and the next press landed on Steam); Down from a page's buttons goes to the first tile.
- DuckStation like ARMSX2: Player 1 on the controller, BIOS from
- PR #41 →
94f9935(feat/release-signing) - signing, file access, hardening before going public:- Own signing key (RSA 4096, made on device by
tools/release/make-release-key.sh, digestb241ea7d…5d3f0inkeystore/release-signer.sha256). Every build before it - 0.1.6 included - was signed with the public AOSP testkey, so anyone could sign an "update" that installs over DroidDeck. - Hand-over (APK Signature Scheme v3 rotation): v1/v2 by the testkey, v3 by our key with a lineage
testkey → ours, testkey rollback off,
--rotation-min-sdk-version 28. Proven on the FIT with a throwaway key and package: a 0.1.6-style install updates and keeps its data; an apk signed with the testkey alone is refused; so is one with an attacker's own lineage from the testkey; an apk signed with our key alone (what MT Manager / APK Tool M make) installs over a hand-over install and fresh, not over a testkey one. - CI:
sign.yml(reusable, a matrix job per package) runs after every non-pull-request build (environmentsigning-mainon main,signing-branchelsewhere; the key is an environment secret).release.yml(dispatch, version) signs the exact Build APK artifact of a main commit as four packages:com.droiddeck.launcher,com.tencent.ig,com.antutu.benchmark.full,com.ludashi.benchmark(tools/release/variants.txt; the manifest rename also covers.documentsand the.hometask affinity). - Documents provider (
files/AppFilesProvider.kt): the app's data folder in Android's file picker ("Open from" → DroidDeck, per-package title), confined to the data folder (a../id is refused, links out of it are hidden), and deleting never follows a link. - Hardening: pull requests build with
pull_request, notpull_request_target(a pull request could write the caches main's signed builds restore); a build fails if a private key file is committed;.gitignorecovers key files; main is protected - pull request + passingbuild+ one approval, no force-push or deletion, the owner can override for his own pull requests. History scanned before going public: no secrets. - Max and Kurt get the key (jks, p12, pk8 + x509.pem, lineage) with a password and three plain guides (MT Manager / APK Tool M signing, the hand-over, how pull requests / test builds / releases work).
- Own signing key (RSA 4096, made on device by
- Also merged today (Kurt): #20 Back shortcuts (session menu, Steam QAM), #24 build identity on the launcher, #33 per-launch Android display choice, #35 CI build-cleanliness snapshot, #37 the split-D logo, #40 faster held-stick navigation in the session drawer; (The412Banner) #32 stretch games to fill the screen, Quake-engine titles windowed at the session's size.
2026-09-24 - main 08a6769: GPU desktop, emulators like PS3, ARMSX2, game art
State: main = 08a6769 (PR #29 merge), main build run 35983098806, staged as Download/DroidDeck-main-08a6769(-Genshin).apk. No tag, no release since 0.1.6. Device-tested on the AYANEO Pocket FIT (Adreno 750) unless marked.
- PR #25 →
f6f7470(feat/desktop-gpu) - the desktop on the GPU:- Patched wlroots (
tools/wlroots, releasewlroots-0.20.2-p1, staged at/usr/local/lib/droiddeck-wlroots): a Vulkan DMA-BUF allocator for the KGSL stand-in node, which is not a DRM device - stock wlroots failed atdrmModeCreateLease→ "unable to create allocator"; linux-dmabuf skips its GEM import check on such a node. labwc's vulkan and gles2 renderers start; vulkan is the default. - Compositor: a toplevel's initial commit is answered with a configure (labwc on vulkan waited for ever - wlroots moves its toplevel to a private queue after that commit);
xdg_positioner/xdg_popup/wl_outputrequests no longer abort the app (NULL implementations; a gamescope beside labwc took the app down); without a Wine desktop a new window takes the keys. - Fallback: labwc that exits or does not answer its socket within 20 s → pixman, remembered per renderer + wlroots md5 + driver (
~/.droiddeck-renderer-failed, cleared by choosing a renderer). On pixman, Game/Emulator menu entries run throughdroiddeck-gpu(their own gamescope on wayland-0). - Emulators: the pad is a plain Xbox 360 controller outside Steam (SDL ignores Steam's virtual gamepad outside Steam);
bannerlator-pad-defaultsseeds Player 1 per emulator (source-verified formats) and never overwrites a user's profile; big-core pinning (program_cores, game cores when chosen); rail programs use the desktop's Linux driver (one shader cache); RPCS3 moved once toAsync Recompiler (multi-threaded)(no 6650-variant interpreter precompile each boot). - PC keyboard (drawer: Keyboard [Hardware] [Android]): Esc, F1-F12, editing block, modifiers (sticky), arrows; real evdev key presses; 47% of the screen.
- Proven: Vulkan and gles2 desktops, Firefox on Vulkan, RPCS3 God of War II HD in a window and from the rail (pad bound, no precompile, cores 2-7), pixman fallback path, Steam session.
- Patched wlroots (
- PR #26 →
6980428(feat/armsx2) - PS2 is ARMSX2 (PCSX2 fork with ARM64 recompilers, GPL-3.0). Upstream PCSX2 only haspcsx2/arm64/RecStubs.cpp: NFS Underground 2 ran at 17 fps with one core at 100%; ARMSX2 runs it at 59.9 fps / 100%, all recompilers + fastmem on under proot. PCSX2 left the front end and the catalog. - PR #29 →
08a6769(feat/armsx2-polish):- ARMSX2 from the rail:
-batch -bigpicture -fullscreen; guide → pause menu → Close Game returns to the app. First run: PCSX2 settings + memory cards copied once; a new player gets ARMSX2's setup with the BIOS folder inROMs/ps2/bios(orps2/firmware),ps2as game list, Vulkan, controller on Player 1. Users bring their own BIOS and games. - Catalog
pages- a file per memory page size; ARMSX2 4K + 16K. - On-screen controls: Auto for rail games and Steam when no controller, off on the desktop; setting in the drawer everywhere; drawer headed by the emulator for rail games.
- Front end: Back/B steps back a level (emulator → Desktop, game → emulator) instead of leaving the app; games found up to 3 folders deep (
ps2/games/<game>/); box art (libretro-thumbnails by name exact/loose, xlenore/ps2-covers by PS2 serial, GameTDB by GC/Wii ID, PS3 discs' ownICON0.PNG), kept infiles/covers; wide art shown whole over a blurred copy; titles without dump tags.
- ARMSX2 from the rail:
- winlator-contents (the catalog): ARMSX2
nightly-20260923-92aa2174damirrored (armsx2.AppImage4K,armsx2-16k.AppImage), PCSX2 dropped fromdesktop.json(PRs #12, #13); the mirror workflow lists only the file being added (a re-run re-fetches and clobbers every listed file, and DuckStation's upstream link moves). - Not yet device-played: DuckStation, Dolphin, Cemu, melonDS, PPSSPP, RetroArch (profiles + covers written from their sources). Known gap: a keyboard's Esc does not open ARMSX2's pause menu in a rail game (guide does).
2026-09-23 (evening) - renamed SteamDeck → DroidDeck
- App name, app ID and code package: DroidDeck,
com.droiddeck.launcher(wascom.steamdeck.launcher), sources underapp/src/main/java/com/droiddeck/launcher, the 68 JNI functions renamed with them. A new app ID means DroidDeck installs beside SteamDeck, not over it: the runtime, the Steam sign-in and the settings start fresh. - Downloads: session logs go to
Download/DroidDeck/; the switch files aredroiddeck-env,-osc,-driver,-tu-debug,-no-pad,-pad-log,-no-hud,-wlr-renderer. The oldsteamdeck-*names are not read. - Inside the runtime:
droiddeck-desktop, Firefox'sdroiddeck.js,~/.droiddeck-desktop-debug,~/.cache/droiddeck, the shortcuts tagdroiddeck-app,X-DroidDeck-AppId, thedroiddeck-rumblesocket. "Desktop installed" now tests forusr/bin/labwc(it tested for the launcher, which the app stages itself). - Kept on purpose - Valve's Steam Deck: the
-steamdeckflag,steamdeck_publicbeta/steamdeck_stable, "Steam Deck mode",steamdeck-packages.steamos.cloud. And names inside the hosted desktop package:steamdeck.png,steamdeck-steam(.desktop),steamdeck-bigpicture.desktop(renaming them means re-hosting it). - CI artifact
droiddeck-apk; a stray committed__pycache__/*.pycremoved.
2026-09-23 (afternoon) - main 90c7574: first-run onboarding, Desktop & apps page, 4:3 display
State: main = 90c7574 (PR #14 merge), main build run 35895701784. No tag, no release; next version is still open. Nothing below is device-tested except the audio default (Kurt, AYN Thor).
- PR #11 →
f79dc2d(fix/directaudio-default): the Steam client's audio is back on the 0.1.5 classic sink by default (module-aaudio-classic-sink.so, byte-identical to 0.1.5's arm64 module); DirectAudio is a choice in the Steam cog ("Steam client audio", prefclientDirectAudio, default off). Games keep DirectAudio on by default; mic on by default, RECORD_AUDIO asked once. Old armhf sink out of the bundle; the bundle re-unpacks when its contents change (stamp = BUNDLE_STAMP + CRC32 of pulseaudio.tzst). Relay logs a heartbeat line; Setup › Session logs › Share latest logs. Kurt on the Thor: classic "works perfectly". - PR #12 →
0ddb0be(feat/play-installs-runtime):586c9c2Play and Desktop UI are enabled with no runtime; the session's loading screen downloads, checks and unpacks it ("downloading the Linux runtime · 332 of 791 MB"), then starts the session. A failure ends on the loading screen with the reason.f437957Desktop does the same: the runtime if missing, then thedesktoppackage fromdesktop.json(426 MB), then LXQt. Play, Desktop UI and Desktop all give the non-Adreno warning before any download (MainActivity.startSession).cbd3bc9Desktop & apps is a page in the pane (pageKeyapps), not a dialog: a rail-style dropdown per tier with an installed/total pill (Native ARM64 open), per-package Install/Remove, and the installing package's step line and bar in its own row. Leaving the page does not stop an install.
- PR #14 →
90c7574(fix/exact-panel-shape), from a user report on a 4:3 handheld (RP Nova) - "the panel's shape" still gave 16:9:- Cause:
SessionActivitysized the display atmaxOf(panel aspect, 16:9)(the foldable guard), so on a 4:3 panel both choices were 16:9 with bars. 9c47095Shape › Exactly this panel (4:3, 3:2…) - the panel's own aspect, no floor. The other two unchanged; foldables still default to 16:9. In the cog and the in-session menu (SessionPrefs.shapeChoices).ad2f9f8Resolution › Custom… per mode (customRes.<mode>, 320×240-3840×2160, evened) with 4:3 / 16:10 / 16:9 presets. It replaces the cap and the shape (Shape greys out); picking a cap clears it; the device report lists it. Not in the in-session menu.- Note: the branch was cut from
cbd3bc9, not0ddb0be- same tree.
- Cause:
- Staged:
SteamDeck-play-installs-runtime-586c9c2.apk(21460acd…),…-f437957.apk(174e74a6…),SteamDeck-apps-page-cbd3bc9.apk(c2cfbebe…),SteamDeck-custom-res-ad2f9f8.apk(d02ee5ce…, = main's content). HTML preview of the onboarding flow:/sdcard/Download/SteamDeck-onboarding-preview.html. - CI note: a branch push does not build (
build.yml= main + PRs); dispatch it withgh workflow run build.yml --ref <branch>. A newer dispatch on the same branch cancels the older run. - Open: Kurt's PR #13 "Add Steam second-screen controls" (
steamdeck-second-screen) - not reviewed; now needs to merge onto90c7574. - Next: (1) fresh-install pass: uninstall, install, press Desktop first (runtime + desktop, ~1.2 GB), then Play with no download; (2) the Desktop & apps page on device (dropdowns, in-row bar, Back mid-install); (3) the Nova reporter tests Exactly this panel / Custom 960×720; (4) PR #13.
2026-09-23 - branch feat/armada-and-lineage
- Steam Deck mode:
-steamdeckonly; SteamOS helper stubs staged from the apk under/usr/binand/usr/bin/steamos-polkit-helpers(the missingsteamos-updatethere was the "Update Error" dialog;jupiter-dock-updater --checkanswers 7 so no dock firmware row); Deck mode defaults thesteamdeck_publicbetachannel (onpublicbetaevery start reinstalled the client and lost the launch URL); client branch row in the Steam cog. - QAM battery:
session/BatteryComponent.ktwrites BAT0/BAT1 from BatteryManager, bound over/sys/class/power_supply. Cloud saves checked on device: in sync, nothing was broken. - Added games: any number of Games folders, each bound under
/root/Games; one shortcut per subfolder in the client'sshortcuts.vdfunder the ARM64 Proton; exe choice per game; art from the folder, else Steam's store (capsule, header, hero, logo) copied into the client's grid. - gamescope: the runtime's 3.16.29 rebuilt in CI (
tools/gamescope, releasegamescope-3.16.29-p1) with Armada's ARM64 client fixes, a realtime-queue switch and the gamepad cursor fix; staged over/usr/local/bin. Game windows forced fullscreen in Steam mode (a game came back in the top-left corner after the Steam menu). - Audio: own PulseAudio sinks in
tools/aaudio-sink, built into the bundle by CI. With DirectAudio on, the client's sound goes through the relay's shared ring (module-directaudio-sink, sink named DirectAudio); off,module-aaudio-sink. Inside proot AAudio only gave 20 ms bursts, the relay gets 4 ms. - Winlator code rewritten or removed (SessionPart, HostEnvironment, HostProcess, PadState, cpp/framegen); rumble now served by
session/RumbleComponent.kt. - Front end shelf laid out whole so the d-pad reaches every tile.
- Hosted runtime and desktop packages untouched.
0.1.5 RELEASED 2026-09-23 03:17 - known-good point
- Tag
0.1.5=d2f4a84(bump commit, versionCode 6), notesdocs/releases/0.1.5.mdon main after it. Private release 0.1.5 and publicSteamDeck-0.1.5on winlator-contents, both Latest, assetSteamDeck-0.1.5.apk21,093,959 bytes, sha25685b42954…, run 35813465422. Staged as/sdcard/Download/SteamDeck-0.1.5.apk. - What went in since 0.1.4 (58 commits): the motion front end, settings as pages with anchored menus, the session drawer, three themes (Paper default) and the new icon, run-as-a-game + libpci fix (117 fps Big Picture on a Fold), client-interface switches, FEX presets, 720p default, TZ, non-Adreno gate, 16 KB app libs, two on-screen sticks + bar-aware layout, folded categories at launch, Kurt's #3 (Back key) / #4 (icon) / #5 (local build helpers), pull-request CI + contributions ledger.
- Untested at release: sticks/bar layout, FEX presets, Deck mode, desktop → Steam hand-off, the
perf:line. Rollback:git reset --hard 8e58e8e(0.1.4) +SteamDeck-0.1.4.apk. - After the cut: the public release on winlator-contents got the private title, and that repo's README a SteamDeck section (latest release,
linuxfs.json,desktop.json) - refresh both at every cut. Verified on the FIT's client binary: there is no-steampalflag; the Deck flags are-steamdeck/-steamos3(Deck mode on the Performance page),-gamepadui,-steamos. - Next, in order: (1) device pass of 0.1.5 - sticks in a game and the bar layout on the Fold, the
perf:line and 720p in a session log, a FEX preset, the desktop → Steam hand-off; (2) decide the stick click gesture (double tap or long press); (3) Kurt's draft PR #1 (Decky installer) builds when he marks it ready - it compiles against main, minus two stray.kotlinfiles; (4) rebuild the six PulseAudio prebuilts 16 KB-aligned; (5) optional FIFO/mailbox switch on the compositor's Vulkan present path; (6) the rename away from Valve marks is still open (icon is now the "A").
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). Oneperf: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. - Builds: r1
7ddfb57(motion only), r2546c063(+ run-as-a-game), r3d1ced87(+ client 720p), r4fb2a3d4(+ desktop 720p) - all CI-green, none device-tested. Staged asSteamDeck-motion-r1..r4.apk; r4 has everything. - r5
1ba803b+ TZ in the guest; r639272cfSetup folds, "Linux desktop environment", help link above the grid (r4 seen running on the FIT); r7af3f377settings 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 tomain(af3f377) and main's auto-build staged asSteamDeck-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) andxXJSONDeruloXx(push, invited 2026-09-22) on the private repo. - CI for pull requests (2026-09-23 early):
Build APKruns onpull_request_targetagainst main (a conflicting pull request never firespull_request), checks the pull request out at its head with full history, merges main itself, and leaves one comment kept current: mergeable + built (apk on the run), or the conflicting files and hunks, or the compiler's errors from the kept Gradle log. Drafts wait. Fork pull requests may run without approval (repo setting). Proven with a throwaway pull request (#2, closed) both ways.Contributions ledgerrewrites the README's contributors table on open/merge/close via.github/scripts/contributions.sh, committed to main by the Actions bot.
Checkpoint 2026-09-22 22:xx - known-good point for the front end
main@36cde7d, APKSteamDeck-r51.apkstaged (shaadb5acf5…, run 35761437514) = frontend-r6 + docs. Same versionCode 5 / 0.1.4 inside; 0.1.5 not cut yet.- Proven on the Pocket FIT this evening: the front end renders and navigates (rail, art,
square emulator icons, focus outline); RPCS3 lists Tomb Raider (ISO in a subfolder) and God of
War II HD (its HDD); God of War II HD boots the game under gamescope at ~40 fps
(
session-20260922-133044:run: rpcs3.AppImage --no-gui …NPUA80491/USRDIR/EBOOT.BIN, 399 frames on screen in 10 s). - Not yet proven: desktop→Steam hand-off after the three fixes (r47–r50); FlatOut from the rail (Steam session with rungameid); Running tile; volume keys; automatic SD game storage (Install drive drop-down); Steam desktop-UI session; Tomb Raider ISO boot.
- Open decision: rename the app away from Valve's marks before it spreads (shortlist offered:
Linuxlator / Pocketscope / Portascope); rename = label, icon text,
applicationId(fresh install + runtime re-download),Download/SteamDeck/log folder, repo, release-tag pattern. - Rollback:
git checkout 36cde7d(or 0.1.4 tag8e58e8efor the last release), reinstallSteamDeck-r51.apk/SteamDeck-0.1.4.apkfrom Downloads. - Loose ends:
ui/MainScreen.ktkeeps ConfirmDialog/CreditsDialog/EmulatorHelpDialog but itsMainScreen/tiles are dead; worktree~/steamdeck-frontendstill exists (branch merged).
Current state (2026-09-22, evening)
main@ the front end merge (feat/frontendfast-forwarded): the launcher main screen - Steam ▸ games, Desktop ▸ emulators ▸ games, Running tile, focus outline. First emulator game proven from the app: God of War II HD in RPCS3 under gamescope at ~40 fps on the Pocket FIT.- Since 0.1.4 on main, unreleased: volume keys to Android; emulators under gamescope; ROMs chip
- ? explainer; sysmem copy + ENOSYS hint; automatic game storage (a card = a Steam library);
the client's games on the desktop with a
steamshim; desktop→Steam hand-off (three causes fixed from FIT logs: Surface detach race, the replaced session's exit ending the new one, shared log folder); the front end. Next release = 0.1.5 once the hand-off is seen working.
- ? explainer; sysmem copy + ENOSYS hint; automatic game storage (a card = a Steam library);
the client's games on the desktop with a
State at 0.1.4 (2026-09-22)
- Latest release: 0.1.4 - private: https://github.com/The412Banner/SteamDeck/releases/tag/0.1.4
· public: https://github.com/The412Banner/winlator-contents/releases/tag/SteamDeck-0.1.4
main@8e58e8e, CI run 35682844106, versionCode 5, APK sha2564b53ce6a…(20,957,823 B). - Runtime:
linuxfs-r9on winlator-contents (790 MB), the only runtime release left; desktop packages fromsteamdeck-desktop-r1. The app rewrites its own scripts (bannerlator-*), the driver,bannerlator-netmanagerand the DirectAudio pieces into the installed runtime at every launch, so a fix in those reaches an installed runtime without a re-download. - Shipped feature set: the native ARM64 Steam client under gamescope on the in-app Wayland compositor; a LXQt-on-labwc desktop with Firefox and a shelf of emulators; Proton tools (GE / CachyOS) as downloads; graphics drivers importable per mode; DirectAudio + microphone; client/game core masks + four session switches; NetworkManager stand-in (Max's); overlay restored; ROMs folder + Storage bound into the session's home; Bannerlator's File Manager; a cog per launch mode (resolution, shape, HDR10, drivers, touch, OSC/audio, renderer); per-session log folders that survive a crash; leftover-process sweep; frame generation (Win-FG, LSFG).
- Verification level: every release is CI-green and staged to the maintainer's device. Proven on hardware (Pocket FIT / Fold): sign-in, store, install + launch (FlatOut 144 Hz), frame generation, controllers + OSC, sound, desktop + Firefox, folding mid-session, driver import, the log folder, the ANR fix, the network page. Not yet proven: everything 0.1.4 added (ROMs folder inside an emulator, File Manager, resolution cap, HDR10, crash sweep, orphan sweep, the drawer's Steam menu), DirectAudio voice, the core masks' effect, offline start, the soft keyboard, any emulator with a game, Adreno 710.
- Open field reports (Thor Pro, 8 Gen 2): trackpad-mode tap does not click (code sends the
click; cause not found by reading); crash after 1–2 min in Ballionaire / Geometry Wars (no log
survived - 0.1.4's sweep will leave
crash.log); a Fold's 10–20 fps client menus (Chromium → ANGLE → Zink on an experimental A8xx Turnip); Fold crash loop (xalia ENOSYS storm - switches shipped, untested); FlatOut shrinking after Resume (gamescope forcing the swapchain extent).
Release pipeline (how every version is cut)
- Work on
main, pushed →Build APKrunsassembleRelease(AOSP test key, v1+v2+v3 re-signed by CI with zipalign + apksigner) → artifactsteamdeck-apk. Dev builds keep the last release's versionCode/versionName; a release bumps both inapp/build.gradlein its own commit. - Verify:
gh run view <id> --json conclusion(never trust a run listing's first row - aworkflow_dispatchrun gets cancelled by concurrency in favour of the push-triggered one), download the artifact, sha256 it. - Stage:
cpto/sdcard/Download/SteamDeck-rN.apk(dev) orSteamDeck-X.Y.Z.apk(release). Neverpm install- the maintainer installs. - Private release
X.Y.Ztargeting the full 40-char sha (a short sha is refused), notes from the draft in the session scratchpad,--latest. Public releaseSteamDeck-X.Y.Zon winlator-contents with the README-style notes; one public release per version. - README ledger, this log, memory.
Timeline
- 2026-09-19 - 0.1 groundwork. Lifted out of Bannerlator's gamescope runtime on the user's
ask: one screen, one button. r2 reached Big Picture sign-in on the Pocket FIT. Package renamed to
com.steamdeck.launcher(r4), foreground service, test key, on-screen controls. - 2026-09-20 - 0.1. The Linux Steam client and a desktop; runtime r6→r9 in a day (Proton self-selection, proot shipped, refresh rate, video, first run reaches a game). Public explainer page for non-Linux users.
- 2026-09-21 - 0.1.1. Catch-up with Bannerlator: driver selection (two lists, per mode), overlay restored + GameHub force-stop, DirectAudio + microphone, core masks, four session switches as toggles, per-session log folders with credentials scrubbed, Adreno 710 path via Banners-Turnip r3.
- 2026-09-22 - 0.1.2. The teardown ANR (log collection on the main thread) fixed; two compositor log lines (frame-size change, window rename); README ledger.
- 2026-09-22 - 0.1.3. Max's NetworkManager stand-in ported (the client's network page); shim audit vs WinNative = zero functional drift.
- 2026-09-22 (later) - after 0.1.4, on main. Volume keys; emulators under gamescope (the
desktop cannot: labwc on pixman offers no dma-buf); ROMs chip; sysmem warning + ENOSYS hint;
automatic game storage; the client's games on the desktop (
steamshim hands off to a gamescope session); the hand-off's three faults found in FIT logs and fixed; the front end merged (branchfeat/frontend, r1–r6); God of War II HD booted in RPCS3 from the rail. - 2026-09-22 - 0.1.4. ROMs folder + Storage in the session's home; Bannerlator's File Manager
ported whole; a cog per mode (railed settings window) with resolution cap, shape, HDR10 gated on
the panel, drivers, touch, OSC/audio/renderer; two-column main menu; crash-safe log folders +
Session logs toggle; OrphanReaper; drawer Steam menu. Public releases reorganised: one per
version, old
Steamdeckrelease and runtimes r1–r8 deleted.
Architecture (where things live)
MainActivity/ui/MainScreen.kt- the main screen;ui/ModeSettingsDialog.ktthe cogs;ui/*Dialog.ktthe rest.SessionActivity- the Surface, input (touch, touchpad, pads, keyboard), the drawer, the HDR decision, the compositor start (wayland/CompositorHost,wayland/WaylandCompositor).session/SessionService- proot + gamescope/labwc + PulseAudio + DirectAudio relay + network link; binds (/root/Storage,/root/ROMs); env for the guest; teardown;OrphanReaper;SessionArtifacts+CrashHandler+SessionPathsfor the log folders;SessionPrefs.files/- Bannerlator's File Manager (FileManagerScreen.ktand its helpers), the picker activity andInAppFilePickerintent API.gpu/- Turnip (bionic, for the compositor) and Linux Turnip (glibc, for the runtime) managers.tools/linuxfs/overlay/usr/local/bin/bannerlator-*- the guest scripts, staged into the runtime bySessionFilesat every launch (CI packages onlybannerlator-*names).app/src/main/cpp/waylandcomp/- the compositor (shared lineage with Bannerlator).
Lessons learned (don't repeat these)
- Nothing in teardown may do bulk filesystem work on the main thread (0.1.1's ANR).
- A CI closure check on a multi-line variable must flatten it first (
libaaudio.sorefusal). - A runtime script must be named
bannerlator-*or CI never packages it. gh release create --targetneeds the full sha.- A proot bind is invisible to a program's "Computer" list; put what users need under home.
- The compositor decides its driver and its HDR gate once per app process - anything that changes them applies after the app is fully closed, and the UI must say so.
- Bannerlator's File Manager ports mechanically (
port_fm.pyanchors) once the container hooks are cut; do not hand-edit 2,500 lines. - Deleting a release asset on GitHub can drop a sibling asset - re-list and restore.
- A resource without an implementation makes libwayland abort on its first request - every interface the compositor hands out needs one, even as no-ops (positioner, popup, output).
- The KGSL stand-in answers no DRM ioctl:
drmIsMaster()reads that as "master"; checkdrmGetVersion()before treating a GPU fd as DRM. - Verify an emulator's config values from its source: RPCS3's GUI says "Async Shader Recompiler", the config value is "Async Recompiler (multi-threaded)".
- Upstream PCSX2 has no ARM64 recompiler; a correct ARM64 build of it is still interpreter-slow.
- An Android app starts in touch mode, where Compose clickables refuse focus:
requestFocus()on open does nothing until the app asks for keyboard input mode. grep -q(orhead) at the end of a pipe exits early; underpipefailthat fails the whole line at random. Capture the output first, then search it.- A reusable workflow gets no more permission than its caller gives, and sees environment secrets only
with
secrets: inherit. A workflow naming an environment that does not exist creates it unprotected. pull_request_targetshares main's caches with the pull request's code - never with signed builds.- apksigner prints signers as "Signer #1", "Signer (minSdkVersion=…)" or "V3.0 Signer:" by version.
Backlog / next
- Play-test the other emulators one by one on the FIT: Dolphin, Cemu, melonDS, PPSSPP, RetroArch (pad profiles and covers exist; nothing played yet). DuckStation done 2026-09-24.
- Kurt's PR #39 (Non-Launcher flavor) breaks signing as written: it replaces the
droiddeck-apkartifact withdroiddeck-home-apk/droiddeck-non-launcher-apk, whichsign.ymlandrelease.ymlread, and it conflicts with today'sbuild.yml. Adapt before merging: sign both editions (a release would then be editions × packages). - After the repo is public: add a required reviewer to the
releaseenvironment (not allowed on a private repo on the Pro plan). - First release with the new key (0.1.7): run
release.yml, publish; 0.1.6 users move over by the hand-over. - Keyboard Esc in a rail game does not reach ARMSX2's pause menu (guide does).
- 0.1.7 pre-release when ready (everything above is on main only).
- Pin a newer ARMSX2 nightly only after testing it (it changes daily).
- Device-prove 0.1.4 on the FIT (list above).
- Thor Pro: trackpad tap; the 1–2 min in-game crash once a
crash.logarrives. - Xfce as a second desktop shell (Max's branch runs XFCE 4.20 on labwc) - a catalog package + a shell choice in the Desktop cog; comfort, not performance.
- Quick Access Menu for non-Deck pads - Back double-press (500 ms) now routes to the existing Guide+A chord; the in-session menu, Steam settings, and Setup › Session can swap the single- and double-Back actions. Device confirmation is still needed.
- FlatOut shrink: try
vk_wsi_force_swapchain_to_current_extent=falseviasteamdeck-env. - Max's stricter
winnative-directaudioguards;winnative-epic-launch(a feature). - Rename before anything truly public: "SteamDeck" is Valve's mark. (Done 2026-09-23: DroidDeck.)