Compare commits

..
Author SHA1 Message Date
xXJsonDeruloXx c26fa0981c wip: multi-controller session and pad bridge changes 2026-10-14 12:16:24 -04:00
Kurt Himebauch ec24b3a472 feat: the built-in pad can be a DualShock 4 or DualSense, its touchpad fed by the trackpads (#559) 2026-10-11 04:21:31 +00:00
Kurt Himebauch 6455b1dade feat: the built-in pad's controller type switches from the session menu while Steam runs (#558) 2026-10-11 00:12:53 -04:00
Kurt Himebauch 542ad3e060 feat: the built-in pad can be a Steam Controller to Steam (#545) 2026-10-11 03:24:22 +00:00
Kurt Himebauch 910fb47692 fix: the Steam Controller's hidraw node reaches the input shim again (#557) 2026-10-10 23:20:09 -04:00
Kurt Himebauch 3dd14dd625 refactor: Steam always runs in Deck mode on the Deck beta branch (#546) 2026-10-10 22:15:42 -04:00
The412Banner d1b0ac4df1 Windows components: picked automatically by each game's source (#554)
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).
2026-10-10 21:15:56 -04:00
MaxsTechReview 0e225d0c6e FEX: preload the fault report only where x86 loaders look (#551)
Preload the FEX fault report only where x86 loaders look

droiddeck-fex put the x86 libfaultreport.so in LD_PRELOAD, which FEX's own
arm64 loader reads too - and FEXServer's, and every child FEX's - so each
x86 start printed "ERROR: ld.so: object '.../libfaultreport.so' from
LD_PRELOAD cannot be preloaded", and an i386 child added "wrong ELF class".
It is now listed in the x86 runtime's own /etc/ld.so.preload, which only
the loaders inside FEX read, as /usr/$LIB/droiddeck/libfaultreport.so.

faultreport.c builds for i386 too, so 32-bit programs get the report as
well instead of an error.
2026-10-10 19:12:51 -04:00
MaxsTechReview d9cd0d2cc8 Desktop: run programs, scripts and AppImages; Switch to Desktop without Deck mode (#549)
* Run programs, scripts and AppImages from the desktop

Windows programs open with the Proton Steam has chosen, and a Steam game's own
launch program is handed to Steam. x86-64 Linux programs and game scripts run
under FEX, and an exec the kernel refuses goes to droiddeck-open through
libblsession's binfmt.c. Programs on noexec shared storage pass KIO's checks; a
folder of Linux programs there, and sharun AppImages, are added by the app once
and run from the copy. Executable scripts run without KIO's prompt, mpv plays
on the software desktop, FEXServer's socket moves under XDG_RUNTIME_DIR, and
Proton runtimes no longer show up as games.

* Switch to Desktop from Big Picture without Deck mode

Big Picture's Switch to Desktop asks the client's SteamOS manager, which the
client only has when started as SteamOS (Deck mode). Without it the request
failed and the screen stayed on "Switching to Desktop..." for good. The live
compat helper now sees the request on its way to the client and hands it to
steamos-session-select, which opens the KDE desktop as Deck mode already did.

* Run x86 programs from shared storage where they lie

FEX asks for the guest's PROT_EXEC when it maps a program or library, which
the kernel refuses on noexec shared storage; FEX never runs those pages
itself, so libblsession maps them without it in FEX's processes. An x86
game, its libraries and its start script now run in place instead of being
copied in; only native code on noexec storage is still copied.

A Steam library's games are never copied, and a Linux game's launch program
opened from the desktop goes to Steam like a Windows one.
2026-10-10 19:12:20 -04:00
MaxsTechReview eadebc46d5 Performance: idle front end, proot stops, launch-time passes and log collection (#529)
* Front end: draw the capsule wall without clipping it every frame

The tilted wall clipped each capsule to a rounded rect and recomposed its columns on every drift
step, and a rotated rounded clip is rasterised in software on the render threads: the idle main
page held about 1.7 cores. The art is now loaded at the capsule's size with its corners already
rounded, each column draws its capsules itself in one layer moved by the drift, and the drift
advances at most every 15 ms. The idle page takes about a third of a core.

* libfakeinput: let calls on descriptors it does not serve skip its lock

Every read, write, ioctl, close and poll of every process in a Proton session went through the
controller lock and the three map lookups, Wine's wineserver pipes and the game's files alike.
A bitmap of the descriptors the maps hold, set before one is handed out and cleared when it is
closed, lets everything else go straight to the kernel.

* Pad motion: ask the display for its rotation four times a second

The rotation was looked up for every sensor reading, every 2 ms, and cost more than the rest of
the reading; it is now refreshed every 250 ms, and the readings no longer allocate pairs.

* Session: keep the library passes out of a game's launch

The redist seeder stamped each prefix with the checksum of the whole set of names, which differs
between the session's pass and the game launch's (the game's own names are added), so every
launch re-appended the markers to every prefix in the library while the game loaded. Each
prefix's marker now lists the names it was given and only the missing ones are added.

The beat's seed and compatibility passes, and the Lossless Scaling ownership probe (which loads
steamclient.so), also wait while a wineserver runs: a launch rewrites the manifest and made them
due at exactly that moment. The game's own launch seeds its prefix regardless.

* Agent: keep the focus watcher's X connection open while nothing changes

The connect timeout stayed on the socket, so with no property change the read timed out, and the
watcher dropped the connection, reconnected and took a fresh snapshot every few seconds.

* Game environment: start Cyberpunk 2077 without REDlauncher

The prelauncher opens REDlauncher, a Qt WebEngine window that hands over to the game half a
minute later and then stays up beside it, well over a core busy for as long as the game runs; its
own --launcher-skip started the game twice. The game's executable is started directly instead.

* netif: answer Wine's network driver from a reading of the link

Mortal Kombat X has Wine's network driver enumerate every interface, address and route table
hundreds of times a second. Each enumeration read /etc/droiddeck-net several times, asked the
kernel for its interface dump again (a netlink socket bound under proot, refused each time) and
looked the real functions up with dlsym. The link file is now read at most once a second, a
refused dump is not asked for again within that second, and the real functions are looked up
once.

* Fast path: create memfds without a stop in proot

proot looks at a memfd's name only to refuse the few that are then executed or locked; every other
memfd_create is now issued from the trampoline. The session's /proc/net and status copies made
one per read, a stop each. Without memfd in the kernel the call still goes to proot's ashmem.

* Logs: run each redaction rule only on a line that could match it

A session's end copies some 70 MB of the Steam client's logs and puts every line through all
twenty-odd rules, most of a minute of a core on the phone after every session. Each rule now runs
only on a line holding what it cannot match without: its keyword, the five separators of a MAC,
the four dashes of a GUID, a long enough hex or token run, the learned account or address text.
Lines with non-ASCII text still meet every case-insensitive rule. Over 895,000 lines of real
client logs, with the device's accounts and addresses learned, the output is identical and the
pass takes a twentieth of the time.
2026-10-10 19:00:33 -04:00
SG3xHERO 96d7c66535 Add Steam Controller support over Bluetooth LE (#404) 2026-10-10 20:53:07 +00:00
The412Banner 90690d1c5c Read the package catalogs from Droid-Deck/DroidDeck-Components (#544)
The Windows components catalog, the desktop and emulator packages, the Linux runtime and the
Steam seed now live on the organisation's components repository, mirrored and built there by
CI; the app reads the catalogs from its main and the files from its releases. The personal
winlator-contents mirror keeps serving older app versions and Bannerlator.
2026-10-10 16:52:33 -04:00
The412Banner abbcb8e3ad Games: old Games folders become entries, not Steam libraries (#542)
Games folders saved before the settings section went away were still
registered as Steam libraries on Automatic, with no way left to remove
them. Once, at app start and before any scan, each game such a folder
listed becomes an entry of its own, keeping everything keyed by its folder
(chosen exe, shortcut appid, name, Start in, launch options, art,
removal); store installs and steamapps are skipped. The folders stay bound
at their old guest paths, so no shortcut changes, and the saved roots are
cleared. A Games folder stays a library only when Steam installed a game
into it; the others leave the library list, and the runtime forgets them.
2026-10-10 16:19:17 -04:00
David Kubeša fe60b3c279 Add global manual landscape orientation setting (#441) 2026-10-10 16:07:12 -04:00
Kurt Himebauch c7455b74ff Merge rules (#540) 2026-10-10 19:13:01 +00:00
Ashish Ajin c875ceda4d Hide the touchpad cursor when switching to direct touch (#424) 2026-10-10 15:02:38 -04:00
The412Banner 666abd134f Windows components: a recommended component is listed once, not again under All (#537)
The full list left out only the 'installed for this game' rows, never the recommendations, so
every recommended component appeared twice (DirectX 9 on Final Fantasy Resonance, both rows
switched on).
2026-10-10 14:54:28 -04:00
Muaz Kadan c115916962 Read the phone's motion sensors only while Steam asks for them (#534)
perf: only read the phone's motion sensors when Steam wants them

The gyro and accelerometer ran at 4 ms for the whole session, even in games without gyro, costing about 6% of a core. Steam already tells the Deck pad when it wants motion (setting 48), so libfakeinput now passes that on and PadMotion follows it, with a short delay to ride out Steam's brief toggles.
2026-10-10 14:44:05 -04:00
0579e688ba fix: preserve keyboard input on composite devices (#435)
* fix: preserve keyboard input on composite devices

* Fix composite input merge noteDevice call

---------

Co-authored-by: Jammo <[email protected]>
Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-10 14:40:53 -04:00
Muaz Kadan bc50de3b55 Fix CPU temperature showing 0°C in the performance overlay (#516)
Since the proot speed-up in #494, the overlay couldn't find the CPU sensor and showed 0°C. It shows the real temperature again.
2026-10-10 14:39:30 -04:00
ArthurandJames Yeung 6489232598 Fix NativeAOT GC region range defaults (#385)
Use the bare hex value accepted by older NativeAOT runtimes and CoreCLR. Cover the default and shared and per-game overrides.

Co-authored-by: James Yeung <[email protected]>
2026-10-10 14:38:47 -04:00
0x142857andxXJsonDeruloXx c23c436502 Controller: optional Select button that opens the Steam menu (#384)
* Controller: optional Select button that opens the Steam menu

* Controller: translate the Select-opens-Steam-menu option

* Controller: clearer wording for the Select-opens-Steam-menu option

* docs: evidence for the Select-opens-Steam-menu option

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-10 14:38:07 -04:00
abduznik fad52ec125 Terminal: release createSubprocess's JNI local references, and the cwd chars against cwd (#527)
Fix JNI local reference leak in createSubprocess

The argv and envp loops took one local reference per element from
GetObjectArrayElement and never released it, so the references piled up
until the native call returned. ART aborts the process at 512 local
references ("local reference table overflow"), and both loops share the
table, so a long argument list plus a large environment could hit it.

Delete each element's local reference once its chars are copied and
released, after ReleaseStringUTFChars since the chars depend on the string.

Also release the cwd chars against cwd rather than cmd; the call passed
the wrong jstring.

Adds a host-side test that runs termux.c against a mock JNIEnv that counts
local references and checks release pairing. It needs a jni.h (JNI_INCLUDE,
JAVA_HOME or the NDK) and skips without one.

Fixes #507
2026-10-10 14:29:51 -04:00
Nicolás Gómez Solano d22e458d64 fix(audio): avoid InputStream.readAllBytes() for Android 12 compatibility (#531) 2026-10-10 14:29:46 -04:00
The412Banner 1b7cb4179d Windows components: leave out WebView2, which only Microsoft's installer can lay out (#532)
Its files sit in a container no extractor opens and may not be hosted, so a recording cannot
source them; installing it means running the installer in the prefix, which no runtime here
does. With it gone the waiting section is empty.
2026-10-10 14:03:04 -04:00
MaxsTechReviewandxXJsonDeruloXx c1c22dfd37 Launch the executable a Store package declares (#400)
Store and GDK folders name their game in MicrosoftGame.config or
appxmanifest.xml. Picking the top-level Unreal bootstrap instead failed
with "CreateProcess() returned 2" when the Store had renamed the
shipping exe.

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-10 13:46:03 -04:00
kmnkv1 0043cefab5 Rumble: don't restart the vibrator on every force-feedback packet (#514)
fix: stop restarting the vibrator on every force-feedback packet
2026-10-10 13:34:19 -04:00
kaosjammoandJammo 41f68ef1a1 Fix launcher fullscreen around display cutouts (#432)
Co-authored-by: Jammo <[email protected]>
2026-10-10 13:33:59 -04:00
The412Banner 9743ac3abc Windows components: leave out the OpenAL setup program, a twin of the DLL copy that installs here (#528) 2026-10-10 13:32:12 -04:00
SG3xHERO ca6cfcd193 Unfolded deck controls (#407) 2026-10-10 17:24:23 +00:00
Willie Yao d41424170a Controller editor: snap controls into line while dragging (#490) 2026-10-10 15:11:50 +00:00
kmnkv1 f4678935c4 Odin 3: skip the low-latency Wi-Fi lock, whose WLAN firmware hang reboots the device (#512) 2026-10-10 11:06:29 -04:00
Kurt Himebauch 416eb41d8d feat(desktop): add desktop uninstall and legacy LXQt cleanup (#493) 2026-10-10 10:59:32 -04:00
Kurt Himebauch 6e0d456dd7 fix(amazon): sync library with nile's headers and registered serial (#500) 2026-10-10 10:58:18 -04:00
The412Banner 8ac555664a Windows components: Access 2000 runtime installs here (an MSI inside a 7-Zip-openable wrapper) (#505)
art2kmin.exe is Setup.exe + Data1.msi in a wrapper 7-Zip opens, so the MSI engine lays it out
like any other package; it was on the setup-program list by assumption.
2026-10-10 01:31:23 -04:00
The412Banner 4bf4e53ecf Build: verify the runtime inputs only after the session scripts are staged (#504)
With parallel task execution, verifyRuntimeInputs and stageSessionScripts could run at the
same time; main's build at 3d50e6b1 hashed droiddeck-netmanager mid-copy and failed with
'Missing or changed runtime input'. The check now runs after the staging tasks.
2026-10-10 00:38:23 -04:00
The412Banner 3d50e6b1ae Windows components: hide Vulkan RT and the IE8 update; honest captions for the waiting section (#502)
* Windows components: hide Vulkan RT, whose loader every Proton here ships

vulkan-1.dll is in lib/wine/x86_64-windows and i386-windows of Proton Experimental (ARM64),
GE-Proton and proton-cachyos alike, so the catalog's VulkanRT entry would only shadow it. It
joins the list the page leaves out.

* Windows components: the waiting section says what is really missing (a recording), not 'coming next'

* Windows components: leave out the IE8 security update, which patches a browser no prefix has

* Windows components: the waiting caption without an apostrophe the resource compiler rejects
2026-10-10 00:24:14 -04:00
The412Banner 846b20f78a CI: re-pin the Debian image and stop the publish workflow replacing it (#501)
The publish workflow's push trigger fired again on the branch's rebase and overwrote the
release asset with a freshly compressed copy, so main's first build failed the checksum and
fell back to the registry pull. The env file now pins the asset as published; the workflow is
dispatch-only, defaults to a new tag, and refuses to replace an existing asset unless asked.
2026-10-09 23:59:58 -04:00
The412Banner a229d196bb CI: host the Debian image the preloads build in on a release of this repo (#497)
* CI: host the Debian image the preloads build in on a release of this repo

The preloads step pulled debian:bullseye from a public mirror on every build whose preload
cache missed; the mirrors rate-limit the shared runner addresses and failed two builds in an
hour tonight. Debian 11 is frozen upstream, so the image is published once (workflow
ci-debian-image.yml, release ci-debian-bullseye-r1, pinned in tools/linuxfs/debian-image.env)
and the build downloads that file; only if that fails does it pull, with retries.

* CI: pin the published Debian image (ci-debian-bullseye-r1, 34 MB)

* CI: a new Debian image rebuilds the preloads (env file joins the cache key)
2026-10-09 23:21:11 -04:00
The412Banner 61bfc04704 Windows components: hide the CJK fonts and PowerShell, which every Proton here ships (#496)
Proton Experimental (ARM64), GE-Proton and proton-cachyos all carry Malgun, MS Gothic, YaHei
and SimSun in share/fonts and Wine's PowerShell wrapper, so the catalog's cjkfonts and
powershell entries would only shadow what is already there. They join gecko and mono in the
list the page leaves out.
2026-10-09 23:13:53 -04:00
The412Banner 9b90386e73 Windows components: install from a recording of the installer's result (#495)
* Windows components: install from a recording of the installer's result

A catalog entry may name a 'snapshot': what its own setup program leaves in a prefix,
recorded once in Wine by winlator-contents' tools/snapshot/record.py (the files it places and
the registry values it writes, the self-registering DLLs' CLSID entries included). With one,
the setup program never runs here: the files come from the archive beside the recording, or
out of the vendor's installer by the paths the recording maps (small generated ones inline),
and the registry values go to registry.json for droiddeck-wincomponents to write at launch.
The steps a recording makes redundant are skipped; DLL overrides still apply. A component
with a recording counts as installable; its prerequisites must still be.

* droiddeck-msi-install --unpack: open archives nested inside an installer

QuickTime's cabinets and an NSIS pack's payloads sit inside the unpacked installer; they are
opened in place into '<name>.nested' folders, two levels deep, the same layout the component
recordings map their file sources to, so a recording's paths resolve on the device (QuickTime:
593 of 593).
2026-10-09 22:56:18 -04:00
Kurt Himebauchandmaxjivi05 ddc4c18faf Steam's touch controller as on-screen controls (beta) (#431)
* feat: Steam's touch controller as on-screen controls (beta)

* fix: Steam touch controls appear once the session makes the device; tidier default layout

* fix: Steam touch layout decoding, saved layouts and new sessions

* docs: Steam's touch controls

* feat: Steam's touch icons, labels and colours on the touch controls; pick and save them like Steam

* feat: Steam touch icon picker as in-session panels; layout colour; docs

* Give each physical controller its own player slot

Every controller fed the one exported pad, so two pads played as one.
The first controller keeps slot 0 (the Deck in a Steam session); further
ones get slots 1-3 as separate Xbox 360 pads, created on first input and
removed on disconnect. In Deck mode only the Steam client sees those
pads and hands them to games through Steam Input. Rumble follows the
slot it names.

* feat: Steam Link's touch menu, per-game input modes, action set editing, add/copy/paste, oval controls, paste control

* feat: Steam touch controls drawn as Steam Link draws them; Steam Link's touch menu and layout tray

* feat: Steam touch controls in DroidDeck's style, D-mark guide button, shake to hide

* refactor: Steam touch controls become an On-screen controls choice instead of a second dropdown

---------

Co-authored-by: maxjivi05 <[email protected]>
2026-10-09 21:12:46 -04:00
MaxsTechReview fb8361b197 Expanded Fastpaths for proot and increased performance up to 57% (#494)
* perf: fast path resolves links, /proc and glue as proot does, and answers uname, isatty and passwd

The fast path handed back to proot whatever crossed a symlink, /proc or a directory proot
only shows as glue, which is most of what Steam and Wine ask. It now walks a path the way
proot's canonicalize() does, so those calls stay in-process:

- links are followed in guest terms and their text detranslated as detranslate_path() does,
  including readlink into a buffer the kernel cut short and /proc's exe, cwd and fd links;
- a binding proot dropped at start is dropped here too, glue above a binding is ENOENT, and a
  last component proot cannot stat is left to the kernel;
- pipe and socket dirfds are ENOTDIR, rmdir("dir/") removes dir, and "." and ".." go to proot;
- a blocking accept() with a connection already waiting no longer goes to proot.

Also answered: uname() and gethostname() (proot's utsname never changes), isatty() (glibc
2.43 asks with TCGETS2, which proot rewrites) and getpwnam/getpwuid when nsswitch.conf
sends passwd to files first and the entry is there (Xwayland asks for every client).

equiv.py, equiv2.py and equiv3.py match line for line with and without the fast path
(222, 507 and 924 lines). Per call on the RedMagic: realpath 240 us -> 1.0 us,
realpath /proc/self/fd/N 408 -> 9.9, getpwuid 41.5 -> 4.6, uname 28 -> 0.006.

* perf: LD_AUDIT module skips library lookups proot would fail

ld.so looks each library up along the game's search path, and Proton puts the game's
directory, Steam's runtime and video directories and its own library directories ahead of
/usr/lib: 72% of ld.so's opens in an Among Us launch failed, each a round trip to proot.

libblaudit.so answers la_objsearch() for a candidate proot would certainly fail with ENOENT
(its directory walked as canonicalize() walks it, the name or a directory on the way missing).
It has no libc and maps the fast path's trampoline itself. Each lookup's first candidate is
still opened, since a skipped one fails with ld.so's errno as it stands, and the default
directories are never skipped, so dlerror() reads the same. droiddeck-proton and the extra
wrapper add it to LD_AUDIT when the fast path is on.

* perf: droiddeck-proton checks a tool's components with one sha256sum

Each program the launcher starts pays a dozen traced library lookups under the game's search
path before it does anything. One sha256sum over every listed file replaces one per file;
the check itself is unchanged.

* perf: build a locale archive for C.UTF-8

Every program loads the locale LANG names before main(). glibc ships C.UTF-8 as a directory
it looks for once per category under two spellings, before reading gconv's module lists:
about 30 opens, each a stop in proot, in every process of the session. droiddeck-session
builds /usr/lib/locale/locale-archive (and gconv's cache) once per glibc package, so that is
one open.

localedef names a finished archive by hard link, which Android refuses; with
DROIDDECK_LINK_RENAME=1 libblsession turns a refused link into a rename that will not replace
an existing name.

* perf: library beat starts its helpers only when what they read changed

Every 15 s the session ran droiddeck-seed-redists (some forty programs under proot) and, when
a library directory had changed, steam-compatibility: a burst of thousands of proot stops each
quarter minute, game loads included. The seeder now runs when a library changed or a prefix
(or Proton's template for them) is unmarked, the compat pass when a manifest, depot, tool or
config.vdf changed, both checked from bash without starting anything, and each every five
minutes regardless. The seeder reads its stamp and the time without a subshell each.

* perf: fakeinput opens pad.log on its first line

Every process the library is loaded in opened the session's pad.log at start, a stop in proot
on shared storage, and most never write to it. The file is now opened when there is a line to
write.

* docs: the fast path as it ships, the audit module and measured launch times

* fix: a game the app launches after an idle spell comes to the front

The Steam client brings a game it launched forward only while it counts someone as present,
which it reads from gamescope's input counter. A game the app launched after four or more
minutes without a touch started, mapped its window, and stayed behind the library page until
the next input (device: Palworld, Stardew Valley, Fallout: New Vegas, Mortal Kombat X; the same
on main). The session now watches a requested launch, and when the game's window is focusable
but the interface keeps focus for two polls, counts one input on gamescope's counter, as a key
would. Nothing changes for a launch the client already brings forward.
2026-10-09 20:55:18 -04:00
The412Banner a5fd64f9f2 Games: revamped add games feature (#492)
Adding and managing your own (non-Steam) games moves out of the Steam
settings into the Games tab, and works like Steam's own non-Steam
Properties.

- A + beside "Games N" opens a compact in-app picker: the storage first,
  then that storage's root, every folder, .exe files only. Picking an exe
  adds the game with its name, art, Custom badge and Steam shortcut.
- "Add all games in this folder" adds every game folder inside at once.
  A summary opens immediately and fills in as each game is found, with a
  "?" where the exe was a close call and a red X to remove a game.
- An edit button on each Custom game opens its editor: Name, Target
  (ranked exes, Other exe…), Start in, Launch options, the four pieces of
  art with thumbnails, and Remove. Edits go through the same Steam
  shortcut, so the appid never changes and nothing duplicates.
- Art chooser per piece: images in the game folder, Steam's official art,
  SteamGridDB alternatives, your own image, or back to automatic.
- Finding the exe: up to five folders deep, every exe scored (name match,
  Unreal -Shipping builds, bin folders and size up; launchers, crash
  reporters, installers, redistributables and depth down).
- Real titles from the game's files (Steam manifest, GOG info, the exe's
  version info), keeping the folder's spacing when both spell the same
  title, underscores read as spaces.
- Art: the exact Steam appid when the game names one, strict name
  matches otherwise, Steam's hashed asset paths for newer games,
  SteamGridDB as a fallback (built-in key from the SGDB_API_KEY secret,
  optional own key in Setup, sealed with the Keystore), and a cover made
  from the exe's icon when nothing else has one.
- Removing a game is instant and keeps its files; adding it again brings
  it back. Steam's shortcut list is rewritten once, in the background.
- The Games section is gone from Steam settings; folders added before
  keep listing their games. Artwork switch and SteamGridDB key are in
  Setup.
2026-10-09 19:50:03 -04:00
Kurt Himebauch e0a7ce4f88 fix(desktop): address KDE feedback and keep local builds in sync with CI (#488) 2026-10-09 21:35:37 +00:00
Kurt Himebauch b213ae77b4 feat(stores): bring the launcher's motion to the stores pages (#482)
* refactor(ui): share End session's step-out as StepOut

* feat(ui): let PageFlood carry a tile's art

* feat(stores): bring the launcher's motion to the stores pages

* fix(stores): lay out the pane's rows in a column

* fix(stores): a game page no longer leaves the pad's focus on a dead card and crashes the next press

* perf(stores): GOG remembers its non-games and the library cache loads off the main thread

* fix(ui): a step-out's backdrop never takes the pad's focus

* feat(stores): modes step out of the store chip, each store in its own colour, search in the header

* feat(stores): sign-in pages in the Steam tab's style and an are-you-sure for sign out

* style(stores): drop the sign-in page's store eyebrow, which the title already names

* style(stores): the sign-in page is the store's name and Sign in

* fix(ui): the focus ring follows a control until it stops and meets one still rising into place

* feat(stores): the store a pad's focus is on shows without A, and a press never lands on where it sent focus

* perf(stores): keep chip motion responsive while libraries change
2026-10-09 14:55:03 -04:00
The412Banner 6e5932cb68 Stores: seal the GOG, Epic and Amazon sign-in files with an Android Keystore key (#486)
The stores' sign-ins (files/stores/<store>/credentials.json: access and refresh
tokens, account id and name) were plain JSON. Other apps could not read them, but
every program in the Linux session runs as the app and could. They are now sealed
with AES-256-GCM under a non-exportable AndroidKeyStore key (StrongBox when the
device has one), so the session only ever sees ciphertext.

- Format: a versioned envelope {v, alg, iv, ct}, written atomically and readable
  only by the app. No new dependencies (javax.crypto and KeyStore).
- Migration: an existing plaintext file is sealed at the next start, checked by
  decrypting it, then the plaintext is removed; users stay signed in.
- No Keystore on the device: the file stays plain and the log says so once per
  start.
- A sealed file that can never be opened (tampered, key gone or invalidated,
  broken envelope) is removed and that store asks for sign-in again. A transient
  Keystore error keeps the file and retries.
2026-10-09 14:42:23 -04:00
The412Banner 38e3fa2a33 Logs: every log in app-private storage, shared as one scrubbed zip (#484)
Session folders, tool and installer logs and the stores' log now all live under the
app's private files/logs instead of Download/DroidDeck, and every existing "Share
logs" action bundles them into one zip that goes through a final scrub.

- Where: LinuxRuntime.logDir() = files/logs; sessions keep their dated folders and
  30-session pruning, tools/ and stores/ beside them. The "Logs" setting keeps its
  meaning. Game save backups stay in Download/DroidDeck/Saves.
- Migration: at first start the old session folders, tools/ and loose logs move from
  Download/DroidDeck into files/logs (Saves untouched), and every moved folder is
  scrubbed under the current rules.
- Scrubbing: one rule set (SecretScrub) for the stores' log and the zip. The scrub
  marker carries the rules version, so a rules change re-scrubs every folder at the
  next start (in the background, a few at a time). Steam login account names and
  emails, account-name fields, OAuth codes, tokens in URLs and private LAN
  addresses are removed; Windows exception codes and server addresses are kept.
- Share: one zip of the current session folder plus the last 7 days of stores/ and
  tools/, written to cache/share and handed to the share sheet.
2026-10-09 13:28:03 -04:00
The412Banner 8e1a676f19 Windows components: lay out installer .exe wrappers without running them (#480)
* Windows components: lay out installer .exe wrappers without running them

The components page could only take .msi packages apart; most catalog entries ship an
installer .exe, so VC++ runtimes, .NET Framework 4.0-4.8 and the .NET Core/5+ runtimes sat
under 'Need a Windows installer'. droiddeck-msi-install now opens those wrappers and lays
out the packages inside the same way:

- WiX bundles (Visual C++ 2012-2022, .NET Core/5+ desktop runtimes): the .wixburn section
  names the containers, each a cabinet found by its header (signing moves them); payloads
  get the names the bundle manifest lists, so each package finds its cab1.cab beside it.
  ARM64 packages are skipped, x86 and x64 both laid out.
- Self-extracting cabinets (Visual C++ 2005-2010): cabextract, external cabinet beside the
  package (vc_red.cab).
- Self-extracting 7-Zip archives (.NET Framework 4.0-4.8): 7-Zip, now bundled with the
  msitools (Arch Linux ARM 7zip 26.04: 7z + its 7z.so plugin, licences beside the others).
  The complete product (netfx_Full_*) wins over the 4.0 parts and the per-Windows copies.
- An .exe that is its own setup program (NSIS, InnoSetup) reports that it holds no package.
- --unpack opens an archive for the app: DirectX's redistributable for XACT's cabinet picks.

The app executes the remaining catalog steps offline: set_windows (HKCU\Software\Wine
Version), set_register_key, get_from_cab (through --unpack) and 'uninstall Wine Mono' (the
mscoree override beside it is what matters), and reads url/file_name the catalog nests under
'environment' on a few .NET entries. Setup-program installers stay in the waiting section
with a note that says what they are.

Verified on this branch's engine with msitools, cabextract and 7z: VC++ 2010 x64 (19 files,
61 values), VC++ 2022 x64 bundle (26 files, 66 values), .NET 4.0 (1053 files, 6056 values,
Framework64\v4.0.30319 with clr.dll and mscorlib.dll, NDP\v4 keys) and .NET 4.8 (1633
files, 522 GAC assemblies, 6495 values). Not yet run on a device.

* Windows components: the pressed row greys out and shows its own two-step progress bar

Pressing a switch now greys that row at once and runs a thin bar across it: the download
0..100, then the install 0..100, with a caption naming the file or the stage (a dependency's
name in front while that installs first). Turning a component off greys the row the same way
until the pick is saved. The page-wide bar at the bottom is gone; Done waits on the busy row.

droiddeck-msi-install reports how far along it is ('progress 37% placing files'): reading,
each cabinet, every 5% of files placed, the registry, done - and a wrapper maps each package
into its share, so a bundle's two packages run 10..55 and 55..100. The app parses the percent
and passes a typed Progress (component, stage, phase, percent) up to the editor.

* Windows components: the ones turned on sort to the top of the list

In 'All components' the picks for this game come first, in their usual alphabetical order
among themselves; turning one off returns it to its place. The row moves once the install has
finished and the pick is saved, not while its progress bar is running.

* Windows components: an 'Installed for this game' section under the recommendations

What is turned on for this game gets its own section between Recommended and All components,
alphabetical; the recommendations stay where they are (a recommended pick stays there). A
component in the section leaves All components, and goes back to its place when turned off.
LB/RB jump through the new section too.
2026-10-09 12:36:05 -04:00
The412Banner 7c946db440 Stores: GOG, Epic Games and Amazon Games libraries, installs and cloud saves (#476)
A Stores section (rail, under Games; Setup › Stores gate) with each store's sign-in,
library, storefront shelves and a download queue. Each store has two tabs, Store and
Library, and Library filters to All or Installed; libraries open from a saved copy
and refresh in the background, with install sizes on every card. Games install to internal storage,
or a card chosen at Install time, and are registered in the Steam client as non-Steam
games, so they launch through Steam and droiddeck-proton from either the front end or
the client.

- Downloads: GOG, Epic and Amazon engines in Rust (app/src/main/rust/stores, built by
  cargo-ndk, nothing compiled in the tree). One adaptive fetch core with a latency
  brake, stalled-host handoff and every CDN a store offers. Per-stage progress,
  pause/resume, two-step cancel that deletes, failed downloads kept for retry, a
  foreground service with Wi-Fi and wake locks and notification controls.
- Games tab: store source chips (Steam, GOG, Epic, Amazon, Custom) and store art.
- Epic: per-launch exchange code from the app and from Steam's Play button,
  "Epic sign-in (EOS)", "Launch offline", and a game's web links opened in Android's
  browser for EOS consent. The EOS overlay crashed games and is left out.
- Cloud saves (GOG, Epic): pulled before launch, pushed after the game or the session
  ends, no upload before a first download, conflicts never overwritten, backups both
  ways.
- Logs: every store line redacted; the engine log goes to daily files.
2026-10-09 11:08:17 -04:00
Kurt Himebauch eaf26c8a20 feat: replace the LXQt desktop with SteamOS-style KDE Plasma (#473)
* feat: replace the LXQt desktop with SteamOS-style KDE Plasma

* feat: leaving the KDE desktop logs out through Plasma and returns to gaming mode

* feat: DroidDeck logo wallpaper as the KDE desktop default

* feat: build the KDE wallpaper from the DroidDeck mark artwork

* ci: keep the built desktop package as a short-lived artifact for device testing

* fix: the desktop loading screen no longer promises Steam on the desktop

* docs: the output resize comments name the desktop's KWin

* fix: the desktop package verifies every package's Arch Linux ARM signature and never replaces a published tag

* fix: programs sent to the GPU from the desktop drop its software rendering overrides

* fix: an interrupted desktop install no longer counts as installed

* feat: pin the desktop package to its DroidDeck-Components release instead of the catalog
2026-10-09 10:39:50 -04:00
Magarto 0a96767657 Desktop: keep the session when the big cores cannot be pinned (#394) 2026-10-09 07:13:57 -04:00
SG3xHERO 32054e156e Add a Follow screen resolution for foldables (#403) 2026-10-09 07:13:06 -04:00
Kurt Himebauch fd6d6b7c46 fix: proot fast path hands opendir of a directory symlink to proot (#454) 2026-10-09 07:11:09 -04:00
Kurt Himebauch c101a451e3 perf: fast path answers Steam's pre-2.33 stat calls and stays off without the patched proot (#466) 2026-10-09 03:14:29 +00:00
Kurt Himebauch a76ba69217 fix: runtime removal never strands an undeletable leftover (#463) 2026-10-08 21:39:00 -04:00
The412BannerandThe412Banner d06caf9838 Desktop: Firefox skips shared-memory seals again (Android 16 tab crash) (#460)
Desktop: Firefox skips shared-memory seals again (Android 16 tab crash, #280 line lost in #252)

Co-authored-by: The412Banner <[email protected]>
2026-10-08 19:45:44 -04:00
Kurt Himebauch 134fa167fe feat: install Flatpak apps from a .flatpak file (#446) 2026-10-08 14:13:32 -04:00
The412BannerandThe412Banner da42a5d7a7 DirectAudio from its own release: driver set per Proton interface (CachyOS fix), the Steam client's own DirectAudio sink, binaries out of tree (#439)
DirectAudio from its own release: driver set per Proton audio interface (CachyOS fix), the Steam client's own DirectAudio sink, binaries out of tree

Games
- Two driver sets ship from the pinned DirectAudio release (tools/directaudio/release.env =
  directaudio-linux-v1.1.0, sha256-checked by tools/directaudio/fetch.sh in CI and
  build_local.sh): linux-wine11 for Valve Proton 11 / Experimental / GE-Proton, and
  linux-wine11-systhread for Proton-CachyOS, whose Wine carries the system-thread mmdevapi
  interface. steam-compatibility picks by whether the Proton's own winepulse.so imports
  PsCreateSystemThread, never by the version string (both say wine-11.0; the wrong set is
  silence). No matching set: DirectAudio off, Proton's audio runs, logged.
- Proton-CachyOS's launcher pre-sets WINE_AUDIO_DRIVER=pulse,alsa and its mmdevapi reads that
  before the registry (Valve's and GE's do not read it): the wrapper exports
  WINE_AUDIO_DRIVER=directaudio,pulse as well as writing the registry. On CachyOS this also
  makes DirectAudio engage on a game's first launch.
- A wineserver from an earlier Steam verb can save its in-memory registry over the appended
  Audio value; the wrapper says so when one is live. Writing through it (wine reg add) and
  creating a missing prefix first (proton run wineboot) were tried on the device and withdrawn:
  a second Proton/Wine run inside Steam's launch sequence got launches aborted.
- Tests: both sets, no set, no prefix (34 pass).

Steam client audio
- DirectAudio = module-directaudio-native-sink, the game driver's AAudio engine inside the
  PulseAudio daemon, one step from Android (adaptive buffer, two-burst floor, primed ring with
  fades, reopen on route change, watchdog, limiter). Default. Shared output = the previous relay
  route, kept. New pref clientAudioRoute; the old boolean is not read. If the native sink cannot
  open its stream the daemon keeps running (.nofail) and ensureClientSink loads the plain AAudio
  sink instead, logged to audio.log. Audio bundle stamp bumped.

Out of tree
- app/src/main/assets/directaudio/ is generated and ignored; tools/directaudio-relay/ and the
  vendored module-directaudio-sink.c + da_relay_proto.h are gone; tools/aaudio-sink keeps only
  module-aaudio-sink (the fallback). The release carries the relay and sink field fixes by
  MaxsTechReview (#338), credited there and in its AUTHORS.

Device-proven on an AYANEO Pocket FIT: client sink 12 ms / 4 ms bursts, 0 underruns; FEZ on
cachyos-11.0-20261005-slr on DirectAudio (took pulse before); FEZ on Proton Experimental
unchanged; Curse of the Dead Gods on CachyOS from a cold prefix, DirectAudio on first launch.

Co-authored-by: The412Banner <[email protected]>
2026-10-08 12:35:00 -04:00
MaxsTechReview b6c1d3c09c Land taps and the pointer on what is drawn under them (#389)
The compositor takes touch and pointer positions over its whole output and
removes the letterbox bars itself, but the session sent them already
measured from the picture's edge, so the bars were taken off twice. Any
session whose output is a different shape from the panel (16:9 on a 16:10
tablet or a foldable, a fixed resolution on a wide phone) had taps and the
touchpad pointer pulled away from the centre, by up to a bar's height at
the edges: on a 2400x1504 panel showing 1280x720, a tap near the top
reached the first row and one near the bottom landed 32 rows low. Stretch
to panel hid it because that output has no bars.

Direct touch, the touchpad and mouse pointer, and the second-screen pointer
now send positions over the whole view, so the compositor's own mapping
puts them on the pixel under the finger.
2026-10-08 11:52:45 -04:00
Kurt Himebauch a58b0ac3e0 chore: bump version to 0.3.2 (12) (#437) 2026-10-08 10:49:33 -04:00
Kurt Himebauch e1700ba982 docs: point 0.3.2 full changelog at the 0.3.2 tag (#438) 2026-10-08 10:49:29 -04:00
Kurt Himebauch 736ac7db74 Draft 0.3.2 release notes (#417)
* docs: draft 0.3.2 release notes

* docs: update 0.3.2 release notes

* docs: update external storage install note

* docs: add keep downloads running demo gif to 0.3.2 notes

* fix: restore colours in keep downloads running gif

* docs: add native steam sleep demo gif to 0.3.2 notes

* fix: drop glitched frames from native steam sleep gif

* docs: add windows components screenshot to 0.3.2 notes

* docs: use newer windows components screenshot in 0.3.2 notes
2026-10-08 10:42:16 -04:00
MaxsTechReview 6e2966be43 fix: keep Steam's directory listings on FUSE libraries (#410)
* fix: keep Steam's directory listings on FUSE libraries

Before it creates, moves or deletes a content file the Steam client lists
the whole destination directory about ten times to match names
case-insensitively. On Android's FUSE storage every listing goes through
the media provider, so reserving space grew quadratically with the number
of files: 33 minutes for Haunted Paws Demo (3,938 files) in a Games folder.

The client's listings under steamapps/ on FUSE are now kept while the
directory's stat is unchanged, for at most five seconds, and patched by
the client's own creates, renames and removals. Other changes fail the
stat check and the directory is read again. BL_NO_DIR_CACHE=1 turns it off.

Haunted Paws Demo into the internal shared Games folder: reserving space
33m08s -> 25-52s, committing 6m15s -> 19-30s, install 39m50s -> 1m12s-1m51s.
Internal storage is unchanged (5s reserve).

* fix: drop stale directory listings on coarse-timestamp volumes

NTFS reports whole-second directory times and a fixed size, so a change
made outside the client's own calls could leave a cached listing stale.
Listings now carry an inotify watch and are dropped on any event the
client's own patched changes don't account for. A forked child stops
caching, since it shares the parent's queue.

Covers rewinddir, telldir and seekdir on cached listings, and directories
opened by the fast-path preload's opendir, which goes through fdopendir.
2026-10-08 08:50:00 -04:00
Kurt Himebauch 679948e792 Include PR descriptions and queue catalog updates (#421)
* Include a short PR description in build announcements

* Queue catalog refreshes without cancelling publishing runs
2026-10-08 00:46:34 -04:00
Kurt Himebauch e0fc8362b8 Fix PR build report lookup (#420)
Use standalone jq to filter paginated PR comments
2026-10-08 00:32:11 -04:00
Kurt Himebauch 9d8ff9615f Separate PR build announcements (#418)
* Separate PR build announcements and retire closed builds

* Isolate watcher test clocks and clean up the agent server
2026-10-08 00:23:55 -04:00
Tak-attackandxXJsonDeruloXx 1304809240 Improvements to the Japanese Translation (#314)
Update strings.xml

Improvements to the Japanese Translation
I have rewritten the translation to make it sound more natural in Japanese.

Also, since “Loader” in “Decky Loader” is a proper noun, I believe it does not need to be translated.

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-08 04:11:52 +00:00
kmnkv1andxXJsonDeruloXx 343a516e1a Update Russian translation (#324)
Translate the 103 missing strings (screen effects, scaling, Wine sync
backends, FEX for AppImages, Steam repair, device info) and fix existing
ones: inverted meaning in mode_suspend_note, content_add_roms and
wadb_connect_address, double space in the update/install buttons,
outdated AppImage/Proton hints, "1 приложений"-style counts, gender
agreement with substituted names, lowercase fragments that are joined
into sentences, and inconsistent terms (Setup, Developer options, Stop
session, Button mapping).

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-08 04:06:56 +00:00
0x142857andxXJsonDeruloXx 7c08cb58ae Share font defaults across runtime sessions (#252)
* Support CJK text with the device's own fonts

The Linux runtime's rootfs ships DejaVu alone, which has no CJK
glyphs, so Chinese, Japanese and Korean text in Steam Big Picture and
on the LXQt desktop draws as tofu boxes (issues #118, #206, #249).

Bind the device's own CJK fonts - Android's world-readable Noto CJK
.ttc, or DroidSansFallback / Source Han - into the guest under
/usr/share/fonts/droiddeck-cjk, so nothing is added to the ~790 MB
runtime image and the coverage follows the device. A new
droiddeck-fonts script, run by droiddeck-session before any program
that queries fontconfig, links the default rules, writes local.conf
with DejaVu first and the CJK families after it, and rebuilds the
fontconfig cache once per set of font files. The desktop's own
fontconfig block moves there, so every mode gets it.

* Fix CJK and Thai: bind the device font directory, not one file at a time

The first cut bound each device font with its own proot -b host:guest. A
per-file binding into a plain directory is not applied here: the guest sees a
zero-byte placeholder where the font should be (stat size=0, fc-scan empty),
so Chinese stayed tofu even though the app had "bound" the font. A directory
binding is what the rest of the runtime relies on, so bind /system/fonts as a
whole under /usr/share/fonts/droiddeck-device. That also brings Thai, Arabic,
Devanagari and the emoji fonts, which the runtime did not have either.

Verified on the AYN Thor: the session's fontconfig probe reports
sans=DejaVu Sans, zh=Noto Sans CJK SC, th=Noto Serif Thai, and Steam Big
Picture in Simplified Chinese renders without tofu (screenshot under
docs/development/evidence/).

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-08 04:06:23 +00:00
7e81668de2 Polish Spanish translation (#234)
* i18n: complete Spanish

Spanish gets the strings added since it landed and the ones that are only a format or a brand name, so it has the same keys as English, in the same order. Each English term keeps one Spanish word across the file, and a few existing strings are reworded to read right with the values they are filled with.

* i18n: use compilación for build in Spanish

* i18n: translate new strings to Spanish

---------

Co-authored-by: Kurt Himebauch <[email protected]>
Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-08 04:05:09 +00:00
Kurt Himebauch d231564f8e feat: agent bridge wayland and gamescope coverage (#406)
* feat: run a guest agent in every session for commands, focus and the Steam client's DevTools

* feat: give the agent bridge game, input, UI and experiment commands behind a Setup toggle

* feat: droiddeckctl commands for the new bridge, a scenario runner and contract tests

* feat: run cleanup steps after every scenario run so a failure cannot leak into the next

* feat: restart a session that died between scenario runs with start --reuse

* fix: give droiddeckctl stop its own, longer wait for the session's logs

* fix: harden debugging tools access and output
2026-10-08 03:50:58 +00:00
Kurt Himebauch 37e89c5b4d harden logging (#414) 2026-10-08 02:10:06 +00:00
The412Banner e070bd72e0 fix: read droiddeck-env from ps (#413) 2026-10-07 21:38:33 -04:00
Kurt Himebauch 9310d19857 fix: backport upstream gamescope fix for games left behind Steam's loading screen (#401) 2026-10-07 15:28:11 -04:00
Kurt Himebauch 4454a6d39a fix: keep Updates channels and status side by side (#397) 2026-10-07 13:48:02 -04:00
MaxsTechReview 453f6f5180 Keep the shortcut settings changed in Steam (#393)
Steam saves a shortcut's Properties to shortcuts.vdf as soon as they
change and keeps its appid. The session rewrote every added game's entry
from the app's list before and after each client run, keeping only the
launch options, so a Target or Start In set in Steam, a new name or icon,
the hidden and overlay switches, the last played time and Steam's sort
name were all put back at the next exit.

droiddeck-steam-shortcuts now merges instead: an entry keeps every field
Steam has, and the app's exe is written only where the app changed it.
What the app last asked for and what Steam then had are recorded beside
shortcuts.vdf (.droiddeck-shortcuts.json). A Target changed in Steam to a
file inside the game's folder becomes the exe chosen in Steam > Settings >
Games, and a later pick there replaces it in Steam, so both show the same
exe. The appid now stays with the folder, so changing the exe no longer
turns the game into a new one with a new prefix, playtime and launch
options. A game whose folder is missing is set aside with its settings
and comes back with them. The exit-time merge runs before Steam's logs
are copied.
2026-10-07 09:32:15 -04:00
MaxsTechReview 6d0484941e Make Games folders Steam libraries and skip the zero-filled reservation (#388)
With Second library on Automatic, every writable Games folder is now a
Steam library: it gets the same private prefix and tool binds as the card,
the app hands the session the list (BL_STEAM_LIBRARIES), and
droiddeck-steam-library registers, relabels and forgets entries from it.
Steam offers them in the install dialog as "<folder> (<volume>)", and
Proton mapping, esync, the redist seeder, desktop entries and the compat
beat read every registered library instead of only the card. Games folder
libraries stop at the fast path's bind budget.

Steam's preallocation on Android's FUSE storage either failed outright
(NTFS answers EPERM, so installs were cancelled as "not enough disk
space") or zero-filled the whole download first (exFAT, minutes for a
small game). After fallocate is refused, the Steam client's ftruncate
extension of its own staging files is reported as done; the writes that
follow fill the file. A 76 MB install to a USB SSD now preallocates
instantly instead of taking about 3.5 minutes.

A library that is not there is left out of the list and forgotten until
it returns, imports whose folder is missing are set aside rather than
offered as broken installs, and the front end refreshes when storage is
mounted or removed.
2026-10-07 09:31:33 -04:00
The412Banner cf47910881 File manager: the Linux system's root under Storage (#392) 2026-10-07 08:12:02 -04:00
The412Banner 4f0f0cf5b1 File manager: the Linux system's root under Storage
A "Linux system" entry in the file manager's Storage list (the rail and the
drive menu) opens the root of the Linux system the sessions run in - the
app's files/linuxfs, the desktop's / - so its files can be browsed, copied
in and out without the desktop. Shown once the runtime is installed; Back
stops at that root like a drive's, and the drive label reads Linux system
anywhere under it. The label is translated in all nine languages.
2026-10-07 08:05:43 -04:00
Kurt Himebauch a78d5cb600 Let Steam draw at the session's full size above 1080p (#391) 2026-10-07 07:51:50 -04:00
Juan MartíandxXJsonDeruloXx 206c8a8846 Add Steam background download mode (#335)
* Add Steam background download mode

* Keep active Steam downloads running

* Make background downloads a toggle

* fix: keep background downloads alive between Steam's rate log lines

* Track all Steam content update types

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-07 10:36:49 +00:00
Kurt Himebauch bf17ce1151 fix: revert gamescope patch 0113 now that games run on their own Xwayland (#372) 2026-10-07 06:25:06 -04:00
MaxsTechReview ef38c975b9 Draw Steam's CJK and Thai text with the device's fonts (#374)
Steam ships no CJK font and the rootfs only has DejaVu, so a phone set to
Chinese, Japanese or Korean got Steam in that language with every character
drawn as a box. Bind Android's /system/fonts into the guest, index it again
whenever it changes, and prefer the matching Noto CJK face for each language.
2026-10-07 02:23:27 -04:00
The412Banner 61953c4f7b File manager: up/back from any start folder, plus a ".." up row (#377) 2026-10-07 00:05:06 -04:00
The412Banner 1f13285ab8 File manager: up/back work from any start folder, and a ".." row to go up
Opening the manager at a folder (a game's files, its Proton prefix, the
saves folder, or the picker's remembered folder) ran openDrive() on the
start dir, which pinned the up/back floor to that folder: the toolbar
arrow was greyed out and Back closed the screen. The start dir now opens
with loadDirectory(), keeping the volume-root floor computed for it, and
folders in the app's private storage (internal Steam library, prefixes)
floor at the app's files dir instead of themselves.

Adds a ".." (Up one level) row at the top of the list and grid whenever
the current folder is above its floor, sharing one goUp() with the
toolbar arrow. Covers both screens built on FileManagerScreen: the File
Manager and the in-app file/folder picker. Strings for all 9 locales.
2026-10-06 23:52:59 -04:00
MaxsTechReview 1bc23ac169 Language setting and translations: Spanish, Japanese, Korean, Chinese; Steam follows it (#187) (#284)
* l10n: move hard-coded UI text into string resources (#212)

About 650 user-facing strings were written in Kotlin and Java, so no
translation could reach them and the UI stayed half English in every
language. They now live in values/strings.xml.

- Dialogs, menus, status lines, toasts and errors shown to the user go
  through getString/stringResource.
- Code that matched on English text now uses IDs: installer and loading
  stages, Decky stages, storage labels and the Back button's order labels.
- Choice lists (upscalers, FPS limits, textures, themes, presets) carry
  @StringRes labels instead of English.
- Robolectric tests read the app's resources (includeAndroidResources).
- Six strings nothing used any more are dropped.

Existing translations are untouched; the new strings show in English
until they are translated.

* Layouts: fit longer text in other languages, and small phones

Found by screenshotting every page, tab and menu in each language on a
phone, a tablet, a 640dp phone and a foldable.

- Rail labels shrink to fit instead of being cut off (FitText).
- Tab rows tighten when they overflow and scroll the chosen tab into view.
- Menus fade over a down arrow when more entries are below, and open
  scrolled to the current choice.
- On a small phone the desktop card's button moves under its title,
  emulator tiles go two across, the GPU support verdict wraps and the
  Updates page stacks the channels under the status.

* l10n: translate every string into Spanish, Japanese, Korean and Chinese (#187)

Simplified Chinese, Traditional Chinese (Taiwan) and Traditional Chinese
(Hong Kong, reviewed for Hong Kong wording) are complete, as are Spanish,
Japanese and Korean: no missing keys and no placeholder mismatches.

* Language setting: follow the system, or a language picked in Setup; Steam uses it too (#187)

- Setup -> Launcher starts with a Language row: "System default (...)"
  and each language by its own name. With nothing picked the app follows
  the system language; a pick applies at once and stays until System
  default is picked again.
- Steam keeps its language in ~/.steam/registry.vdf under both
  Valve/Steam/language and Valve/Steamsteamglobal/language. Before each
  start of the client the guest helper droiddeck-steam-language writes
  both (and exportedsettings.json), and the client gets -language.
  SteamLanguage maps the app's language to Steam's names: english,
  spanish/latam, japanese, koreana, schinese, tchinese.

* l10n: translate the strings main added since, and Chinese review fixes

- Translate the 32 strings added on main since (Windows components,
  device details, Force SSBS, native Steam sleep) into all six languages.
- zh-CN: busy labels say what is in progress again (正在取消, 正在导出
  ...), as the other languages do.
- zh-CN: "set up" is 配置, as Setup is; 设置 stays for Settings.
- zh-TW: proton_compatible_note says 前綴 rather than "prefix".
2026-10-06 22:00:21 -04:00
MaxsTechReview 50f50b16e0 l10n: move hard-coded UI text into string resources (#212) (#282)
About 650 user-facing strings were written in Kotlin and Java, so no
translation could reach them and the UI stayed half English in every
language. They now live in values/strings.xml.

- Dialogs, menus, status lines, toasts and errors shown to the user go
  through getString/stringResource.
- Code that matched on English text now uses IDs: installer and loading
  stages, Decky stages, storage labels and the Back button's order labels.
- Choice lists (upscalers, FPS limits, textures, themes, presets) carry
  @StringRes labels instead of English.
- Robolectric tests read the app's resources (includeAndroidResources).
- Six strings nothing used any more are dropped.

Existing translations are untouched; the new strings show in English
until they are translated.
2026-10-06 21:59:31 -04:00
MaxsTechReview adc86fc7df Fix Audio Crackling on Pulse & DirectAudio (#338)
* Build the DirectAudio relay from source

The relay helper shipped as a prebuilt libdirectaudiorelay.so with no source in the
repo. Its source (The412Banner/directaudio @ 3548a4a, the commit the bundled driver
names in version.txt) now lives in tools/directaudio-relay, unmodified, and CI and
build_local.sh compile it into jniLibs the way proot is built. It shares
da_relay_proto.h with the PulseAudio sink, which is byte-identical to its own copy.

* DirectAudio relay: prime before playing, one consumer, safe buffer floors

- The output stream started before the client had its ring, so every connection
  opened with a run of short callbacks: six clicks per stream on a RedMagic, even idle.
  The callback now plays silence until the ring holds its target, and only a real
  underrun afterwards counts.
- An underrun ends the block with a short fade and waits for the ring to refill to
  target before resuming with a fade in, instead of zero-padding every short callback.
  Each callback spent starved grows the target, so one stall sizes it for the next.
  With a producer freezing 60 ms every 2 s: 14 underruns before, 1 after.
- During a reopen the outgoing and incoming streams could both drain the ring. Only
  the promoted stream reads it now; any other plays silence.
- The ring target starts at 40 ms instead of two bursts (8 ms) and keeps what it
  learned across reopens.
- The device buffer is at least two bursts; 12 ms was a single burst on devices that
  grant a 20 ms legacy stream.

* PulseAudio: the classic path plays through the native-rate AAudio sink

The classic path loaded the prebuilt 0.1.5 module, which opened AAudio at the daemon's
default 44.1 kHz: Android gave it a legacy stream (20 ms bursts) and resampled, after
PulseAudio had already resampled the 48 kHz clients down. Its data callback also waited
on the daemon's IO thread, and it started at two bursts and grew only after an audible
underrun. The app's own module-aaudio-sink, built by CI into the same bundle, now
carries that path:

- The stream opens at the device's rate (48 kHz) with the buffer at two bursts or
  30 ms, whichever is more, still growing a burst per underrun.
- A write that times out part way keeps the rest for the next write instead of
  dropping it.
- A stream reopened after a route change asks for the rate the first one got, so the
  sink keeps its format instead of unloading.

* Audio: DirectAudio sink at audio priority, deeper client buffers, no decay probing

- module-directaudio-sink's IO thread asks for nice -16 like module-aaudio-sink's, so
  the client's producer keeps up under game load.
- PULSE_LATENCY_MSEC=60 for the session: libpulse clients under FEX (the Steam
  client, winepulse when DirectAudio is off) keep enough queued to ride out a late
  wakeup.
- BANNER_AUDIO_DIRECT_DECAY=0: the driver's decay shrinks the device buffer every
  10 s of quiet until it underruns again, a click by design.

Both sit ahead of Downloads/droiddeck-env and per-game environment, which still win.

* Audio: drop the session-wide PULSE_LATENCY_MSEC

On an AYN Thor it took the Steam client's stream to 110 ms (60 ms tlength on top of the
sink's 40 ms) with no underruns to show for it, and the delay was audible. Clients pick
their own buffers again; the per-game option still sets it.

* PulseAudio sinks: wait in the poll loop, never in a blocking call

Clients' audio reaches a sink as messages its IO thread handles in pa_rtpoll_run. The AAudio
sink spent each iteration inside a blocking AAudioStream_write for up to a burst, and the
DirectAudio sink inside a futex wait for up to a burst, so a client's data sat unread and its
stream ran dry: a Chromium-shaped client (44.1 kHz float, 30 ms tlength, 10 ms minreq) had
304 underflows in 30 s on the AAudio sink, heard as crackling in the Steam client. Bursts of
20 ms (a legacy stream, an AYN Thor) made it worst.

- module-aaudio-sink writes without blocking, renders only what the device has room for, and
  comes back after half a burst. 0 underflows in 30 s, with and without 8 busy cores.
- module-directaudio-sink tops the ring up on the same half-burst timer instead of sleeping
  on the relay's futex.

* DirectAudio relay: vendor the source the shipped binary was built from

The import took 3548a4a, the commit the bundled driver names in version.txt, but the relay
binary that shipped came from e4727e9 on the same branch (feat/linux-relay-mic): its strings
match a build of that source exactly. 3548a4a has no --mic-fifo, so with the microphone on the
relay printed its usage and exited, and DirectAudio played nothing. This is e4727e9 as
published; the fixes made on top of 3548a4a follow in the next commit.

* DirectAudio relay: prime before playing, one consumer, safe buffer floors (on e4727e9)

The same changes as ca5d39b, carried onto the e4727e9 source: playback waits until the ring
holds its target, an underrun fades out and re-primes, starved callbacks grow the target, only
the promoted stream drains the ring, the ring target starts at 40 ms and survives reopens, and
the device buffer is at least two bursts.

* Steam client audio: DirectAudio by default

A fresh install plays the Steam client through the DirectAudio relay. Choosing Classic in the
Steam cog is stored and kept; only an install that never chose falls back to DirectAudio.
2026-10-06 17:10:00 -04:00
Juan Martí f3fd3a2307 feat: recognize Adreno 650 as supported (#326) 2026-10-06 16:02:54 -04:00
Kurt Himebauch 1e877fa602 fix: give each new session the current screen's output size (#357) 2026-10-06 19:17:52 +00:00
Kurt Himebauch 906009a33a fix: share session logs in about a second instead of re-scrubbing them (#352) 2026-10-06 14:51:43 -04:00
The412BannerandThe412Banner 08559a02f8 Windows components: install .msi packages (XNA, PhysX, GFWL, MSXML...) without running them (#347)
* Windows components: install .msi packages (XNA, PhysX, GFWL, MSXML...) without running them

droiddeck-msi-install reads a Windows Installer package's tables with msiinfo and unpacks its
cabinets with cabextract, then lays out what Wine's msiexec would have installed: files at their
64-bit Windows paths (Program Files (x86), syswow64...), .NET assemblies in the GAC, side-by-side
assemblies in winsxs, and the registry values in the view the component writes through. Conditions,
features and the folder-setting custom actions are evaluated as on Windows 10 64-bit; what needs
the package's own code (DXSETUP, self-registration) is listed in the component's notes.

droiddeck-wincomponents puts a component's drive_c tree into the prefix (keeping the prefix's own
spelling of each folder) and writes its registry values into system.reg/user.reg before launch,
following Wine's link keys (Wow6432Node\Classes, CurrentControlSet...). Turning it off puts every
value back and takes an added PATH entry out again.

The app installs these from the Windows components menu like the DLL ones: it downloads the .msi
into the runtime and runs the installer there (GuestCommand). msitools 0.106 and cabextract 1.11
come from Arch Linux ARM, pinned in tools/msitools/packages.txt and hosted on the msitools-0.106-r1
release. wine-gecko stays hidden (Proton ships it) and wine-mono keeps waiting (uninstall step).

* Windows components: open with the top switch focused, by touch or by pad

The dialog's window starts in touch mode, where a switch cannot take focus, and the first switch
was asked once, two frames after the list loaded, only when a pad opened it - racing the dialog's
own Done focus. Now it always puts the window in pad mode, holds Done while the list loads and
then asks the top switch every frame until it has focus.

* Windows components: give msitools its own libraries; show a failed install on its row

msiinfo and cabextract sit with their libraries in /usr/local/lib/droiddeck-msitools, which the
loader does not search: every .msi install stopped at "libmsi-1.0.so.0: cannot open shared
object file". droiddeck-msi-install now runs them with that folder on LD_LIBRARY_PATH.

A failed install also says so under the component itself ("Could not install: ..."), not
only at the foot of the dialog, past the list.

---------

Co-authored-by: The412Banner <[email protected]>
2026-10-06 14:14:28 -04:00
Kurt Himebauch 1d93626453 feat: remove Force game windows fullscreen now that games run on their own Xwayland (#349) 2026-10-06 18:04:22 +00:00
Kurt Himebauch 9d0989374b feat: run Steam sessions on two Xwayland servers as SteamOS does (#344) 2026-10-06 17:56:13 +00:00
The412BannerandThe412Banner 3278afaa1a Fast path: hold every bind, so added games start in their own folder (#343)
* Fast path: hold every bind, so added games start in their own folder

libblfastpath kept the first 64 binds and dropped the rest without a word. A session has 87 (two
Games folders are the last two), so /root/Games/<folder>/... was mapped into the rootfs - the
empty mount point - instead of the bound storage. Wine resolves a new process's working directory
by those paths, found no drive for it and started every added game in C:\windows ("could not
find DOS drive for current working directory"): DiRT 3 did not mind, FEZ looks for
Content\Essentials.pak there and quits.

The library now holds 256, and turns itself off for the process when a bind does not fit or the
list runs past that, leaving the call to proot, which never maps it wrong. The app keeps the fast
path off for a session with more binds than that.

Verified on an AYANEO Pocket FIT: FEZ (added game) reaches its title screen; the warning is gone.

* Fast path: say in the session log when it stays off

---------

Co-authored-by: The412Banner <[email protected]>
2026-10-06 13:48:01 -04:00
Kurt Himebauch bf221ffbbd fix: resolution presets keep the panel shape at 720p, 900p and 1080p (#337) 2026-10-06 13:19:46 -04:00
Kurt Himebauch c636eea355 fix: hold flood edges at the page edge once reached (#330) 2026-10-06 12:07:24 -04:00
Kurt Himebauch ffaa906cd1 fix: catch KGSL GEM_CLOSE at ioctl instead of exporting drmIoctl (#305)
* fix: avoid reusing retired KGSL GEM handles

* fix: defer closing GEM_CLOSE'd KGSL dma-buf descriptors

* Revert "fix: defer closing GEM_CLOSE'd KGSL dma-buf descriptors"

This reverts commit 191bf4b6f2.

* test: interpose drmIoctl without releasing GEM_CLOSE handles

* fix: catch KGSL GEM_CLOSE at ioctl instead of exporting drmIoctl
2026-10-06 11:52:16 -04:00
The412BannerandThe412Banner d61af961c1 Windows components for Steam and added games; no more redist installer prompts (#287)
* Put a game's Windows components into its Proton prefix at launch

droiddeck-wincomponents copies the DLLs of the components picked for a game
into compatdata/<appid>/pfx before Proton starts and hands their overrides to
WINEDLLOVERRIDES, so the registry is never touched. It re-checks every launch
(an older Proton deletes the files it once placed), takes a component's files
out again when it is turned off, and forgets Proton's config_info then so
Proton puts its own builtins back.

* Windows components for added games: picker, packages and their catalog

Settings > Games lists a Windows components row under each added game. Its
dialog switches components on per game; the first switch-on for any game
downloads the package (a catalog of its own beside desktop.json, installed
with the desktop package installer to /opt/droiddeck/wincomponents/<id>).
The picks are kept in the app and published to the runtime at every session
start for droiddeck-wincomponents to apply.

tools/wincomponents/package.py builds the packages: Bannerlator's eight
wincomponents archives re-rooted, and OpenAL from OpenAL Soft's release
(soft_oal.dll placed as OpenAL32.dll, 32- and 64-bit).

* Windows components: list the installed ones when the catalog cannot be reached

Offline the dialog only looked up ids it already knew - the catalog's and
the game's picks - so a component downloaded earlier never showed.

* Windows components: Bannerlator's list with Recommended, on the game's launch settings

The picker moves from Settings to a Windows components card in an added
game's launch settings. It offers Bannerlator's whole catalog
(components.json on winlator-contents): what the game's folder points at
first (Bannerlator's DependencyDetector, mapped to the DLL-copy twin where
there is one, oalinst -> oalinst_dll), then every component that installs
here, then the ones that need a Windows installer, greyed until DroidDeck can
run one.

A component installs by running its catalog steps into the runtime once:
archives downloaded (one download per archive for a batch), DLLs found by
name as Bannerlator does and copied to /opt/droiddeck/wincomponents/<name>,
overrides and bundled components written to component.json. The launch
script now takes catalog names (xaudio2.7), expands bundles (directmusic),
places files one level down (drivers/gm.dls) and keeps an override's own
mode (dsound=n). Drops the hand-made packages and their script.

* Windows components: Steam games too, and a pad that can drive the list

The card shows on a Steam title's launch settings as well, keyed by its
appid - the compatdata folder droiddeck-game-env reads - and Recommended
scans the game's install folder.

With a pad: Done takes focus while the catalog loads and the first
component once it is there (the list was not drawn yet when the dialog
asked for focus, so nothing had it); switches stay enabled during a
download and a press then is ignored (a disabled switch dropped the
pad's focus); LB/RB jump between Recommended, All, the installers and
Done.

* Redist markers from games' own install scripts; VC++ runtime as installed

The client still started a game's own _CommonRedist installer - its
script sits in the game's folder, often with another version in the
value name, and only Steamworks Shared's were read. On ARM64 Proton the
x64 VC++ 2015-2022 bundle then stops with "Microsoft Visual C++
2015-2022 Redistributable (arm64) is required" (reported on an AYN Odin 2
Portal Max).

- Read installscript*.vdf from every installed game in each library as
  well; the list is cached until an appmanifest changes, since this runs
  every quarter minute.
- Record the VC++ 2015-2022 runtime as installed where its installers
  look (VisualStudio\14.0\VC\Runtimes\{x86,x64,arm64}, both registry
  views), so any bundle - the client's or a game's own - finds it and
  exits; Proton's msvcp140/vcruntime140 serve the game.
- Join the Wow6432Node path instead of sub(): mawk and gawk read the
  replacement's backslashes differently (gawk, the runtime's, was right).

* Mark a game's install-script processes as run just before the client evaluates them

An Odin 3 bundle (0.3.1, Proton Experimental ARM64) shows the client
running its install-script evaluator on the first launch of three new
titles (409710, 2161700, 3404260) while their Steamworks Shared
redistributables (VC++ 2010/2012/2019/2022, DirectX) were still
downloading - 2161700 at 22:03:22, the shared app finished at 22:03:29 -
so the periodic seeder had nothing to mark yet and the installers ran.

The client writes the exact steps to legacycompat/evaluatorscript_<id>.vdf
and starts iscriptevaluator.exe through our launcher. droiddeck-game-env
now hands that script to droiddeck-seed-redists first, which marks every
process in it in the template and the prefixes before the evaluator
reads them.

The seeder's reader no longer assumes a depth: a block with a HasRunKey
is a process named by the key it opened under, wherever it sits.

* Windows components: keep a file that was not Proton's, put it back when turned off

A component's DLL replaced whatever had that name in the prefix, and
turning it off deleted ours, so a file a real installer (or the user) had
put there was gone for good. Before replacing a file that is neither
Proton's own (a symlink into Proton, or a Wine builtin/placeholder DLL)
nor an earlier copy of ours, keep it under
compatdata/<id>/.droiddeck-wincomponents-backup and move it back when the
component is turned off. The launch note says how many went back.

* Windows components: recommend what Steam installs, say what the game already has

- Steam titles: the appmanifest's SharedDepots (Steamworks Shared, app
  228980) list the VC++ 2005-2022 and DirectX redists Steam installs with
  the game; they live outside the game folder, so the folder scan never
  saw them. They join Recommended as "Steam installs VC++ 2010 with this
  game" (SteamRedists).
- Bannerlator's PrefixInstalledDetector, on compatdata/<id>/pfx:
  OpenAL32, Mono, PhysX and Games for Windows Live that are already there
  from elsewhere show "Already in this game". Files droiddeck-wincomponents
  placed itself do not count.
- The game page passes the prefix and, for Steam titles, the appid.

* Windows components: friendly names

The list showed catalog keys (oalinst_dll, vcredist2010_dll, dotnet452,
d3dcompiler_47). Bannerlator's catalog has no display name and some of
its descriptions do not fit the entry, so WinComponentNames gives one:
families by pattern (Visual C++ <year>, .NET Framework x.y[.z], XAudio,
D3DCompiler, MSXML, Wine Mono, DirectMusic parts), the rest from a
table, the key itself for anything new. The key stays in the row's
detail for logs and bug reports; both lists sort by the name, and the
game page's card names the picks the same way.

---------

Co-authored-by: The412Banner <[email protected]>
2026-10-06 10:46:27 -04:00
The412BannerandThe412Banner bc01b77752 Force SSBS for Proton games (libssbs.so), switch under FEX preset (#328)
* Force SSBS for a game's Wine: libssbs.so, opt in per game

Wine's arm64 signal code resumes threads from a Windows CONTEXT whose
Cpsr never carries PSTATE.SSBS, so every signal return leaves the
thread running with speculative store bypass disabled - slower on cores
such as Oryon. Bannerlator fixes it in its own Wine builds
(force_ssbs.diff, Sonicadvance1) and with an LD_PRELOAD adapter for
layers it cannot rebuild. DroidDeck's Protons (Valve, GE, CachyOS ARM64)
come prebuilt from Steam but run the same aarch64 Wine signal path, so
the adapter applies here: tools/linuxfs/ssbs/ssbs_adapter.c, the same
source built for glibc (+ sysv_signal), staged to
/usr/local/lib/libssbs.so.

droiddeck-game-env preloads it when the game's (or the shared)
environment sets DROIDDECK_FORCE_SSBS=1, and only into an ARM64 Proton.
Off unless asked for: the gain is unproven on Snapdragon 8 Gen 3.

For testing: SSBS_ADAPTER_OBSERVE=1 counts without changing anything
(the baseline) and SSBS_ADAPTER_DEBUG=1 logs the fixes; both also count
signals that ARRIVE while a thread runs without SSBS, which shows
whether anything besides signal returns clears it. Checked on an ARM64
Linux kernel: a handler that clears SSBS left 4 of 5 later signals
arriving with it clear; with the adapter, 0 of 5.

* Force SSBS: a switch under the FEX preset in Steam settings

Steam settings > Games gets "Force SSBS" right under the FEX preset, off
unless turned on. Like the preset it goes into the game environment's
defaults (DROIDDECK_FORCE_SSBS=1), published at once and read by
droiddeck-game-env on the next game launch, so a game's own environment
can still set it to 0.

Measured on the device with the adapter's counters (AYANEO Pocket FIT,
DiRT 3 on GE-Proton11-7-aarch64): observing only, 8192 of 8277 signals
found the thread running without SSBS; with the adapter, none did - it
keeps the bit set everywhere Wine clears it, as a rebuilt ntdll would.
The speed-up itself is reported on Oryon cores and was not measurable on
the FIT's 8 Gen 3, hence off by default.

---------

Co-authored-by: The412Banner <[email protected]>
2026-10-06 09:27:15 -04:00
UnbelievableFlavour a1b01a04a8 Added support for Adreno 6xx devices (#320) 2026-10-06 09:08:04 -04:00
Kurt Himebauch b44235c495 Add Native Steam background sleep and automatic wake (#291) 2026-10-06 00:15:39 -04:00
Kurt Himebauch b80d5db176 ci: require isolated version bumps (#303) 2026-10-06 03:26:14 +00:00
Kurt Himebauch d3c0d258b7 Return frontend-launched sessions to Android Home (#300) 2026-10-05 21:39:40 -04:00
xXJsonDeruloXx 9dcdbf245a Merge remote-tracking branch 'origin/main' into frontend-quit-return 2026-10-05 21:34:49 -04:00
xXJsonDeruloXx 369e218afb Return frontend-launched sessions to Android Home 2026-10-05 21:34:36 -04:00
Magarto 119d24bf6b Keep the 16:9 stretch (#63) working in Steam Deck mode (#133) 2026-10-05 21:28:31 -04:00
The412BannerandThe412Banner 2a905510d5 Device details on the GPU card; Adreno model and SoC name when the kernel leaves them out (#295)
* Device details on the GPU card, and the Adreno model when the kernel leaves it out

The Components page called an AYANEO Pocket FIT (Adreno 750) "Adreno"
and warned it was "Below tested hardware (Adreno 725 and newer)": its
kernel names the GPU "Adreno33v2", GpuInfo wanted three digits, and the
SoC model is blank. When KGSL's name has no model, take it from the
phone's Vulkan driver (once this process has asked it) or from the
platform code name (pineapple -> 750, kalama -> 740, sun -> 830, ...).

The GPU card now opens with a tap or A onto the device's details,
gathered once off the main thread (DeviceInfo): SoC (maker, model,
platform, soc0 id), GPU (model and where it came from, the kernel's
name, clock, load, temperature, support), CPU clusters, memory, system,
the phone's own Vulkan driver - device, API, driver version and build,
conformance, limits, the extensions DXVK and VKD3D lean on, Android's
certified version and level - and the drivers DroidDeck uses. A button
copies it all for a bug report; each section takes pad focus so the
d-pad walks the list.

Vulkan is asked through a new small native library (deviceinfo,
vkinfo.c) that opens the system loader with dlopen and keeps nothing
open.

* Device details: the chip's model and name, CPU clusters, GPU clock range

Ideas from ClusterTune (GPL-2.0, its approach, not its code), which
tunes clocks on AYN, Retroid and other handhelds:
- The SoC model is read where vendors put it - ro.soc.model,
  ro.vendor.qti.soc_model, Android's SOC_MODEL, ro.fota.platform (unless
  it is a branch name like "MSM_13.0") - since handhelds often leave
  Android's blank (Pocket FIT: SG8350P).
- SocNames names it: model codes we are sure of (QCS8550 -> Snapdragon
  8 Gen 2, SM8750 -> Snapdragon 8 Elite, ...), else the platform as a
  family (pineapple -> "Snapdragon 8 Gen 3 family"). The GPU card's
  subtitle shows it: "Snapdragon 8 Gen 3 family (SG8350P)".
- CPU clusters come from the cpufreq policies (the cores each covers,
  min-max speed) rather than grouping cores by top speed.
- GPU clock range from KGSL's step list, or a devfreq GPU node on
  non-Adreno chips (Mali), not only the top clock.

* Device details: drop Qualcomm's own "Driver Build:" label from the driver build line

---------

Co-authored-by: The412Banner <[email protected]>
2026-10-05 20:32:51 -04:00
Kurt Himebauch da29919b5a Fix legacy Steam sleep and missed wake handshakes (#289)
Fix legacy Steam sleep and recover missed wake handshakes
2026-10-05 16:49:55 -04:00
輕鈴andKurt Himebauch 05608ac4d4 Refine Simplified Chinese localization (#232)
Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-05 15:58:59 -04:00
starwishHiandKurt Himebauch 135b84a501 Add French and Russian translate, add fr,ru,jp,ko in locales_config.xml And overlap #234 and #259 (#286)
* Create values-zh-rCN

values language of Simplified Chinese

* Delete app/src/main/res/values-zh-rCN

* Add Korean,Japanese and Tranitional Chinese (zh-TW,zh-HK,zh-MO) translations

* Delete app/src/main/res/values/values-ko/strings.xml

* Create strings.xml

Add korean translate

* Add French translate

* Add Russian translate

* Fix French translate error

* Add the fr,ko,ja,ru in locales_config.xml
Fix French bulid error

* Overlap #234 and #259

---------

Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-05 15:52:39 -04:00
MaxsTechReview e7cbba37d1 sign-apk: rename the agent provider's authority for the other packages (#285) 2026-10-05 14:41:38 -04:00
Kurt Himebauch d9bb5e15b0 Use plain-ID .steam files for frontend exports (#277) 2026-10-05 13:48:18 -04:00
The412BannerandThe412Banner 46cdc32cf1 Firefox: skip shared-memory seals (tabs crash on Android 16) (#280)
On Android 16 phones (Galaxy S25 SM-S931B, SM-S948B, RedMagic NP05J)
every Firefox and LibreWolf tab dies at once: Firefox seals the size of
the shared memory it hands a tab, adding the seal fails there, Firefox
only warns, and the tab refuses the unsealed memory ("IPDL protocol
error: Shared memory PlatformHandle is not safe to map") and exits on
signal 11. The window opens; no page ever loads.

MOZ_SHM_NO_SEALS=1 is Firefox's own switch to skip the seals
(ipc/glue/SharedMemoryPlatform_posix.cpp). Set it beside the sandbox
switches in all three places a browser starts from: the desktop session,
AppImages, and Flatpak sandboxes.

Co-authored-by: The412Banner <[email protected]>
2026-10-05 13:37:08 -04:00
Lobanokivan11 387572e733 merge bridge to main (second time) (#279) 2026-10-05 17:09:24 +00:00
The412BannerandThe412Banner 9f46c7a50b Force game windows fullscreen: off by default (#276)
A game whose resolution differs from the screen's (DiRT 3 at 1280x720)
fights --force-windows-fullscreen: every forced resize makes it rebuild
its swapchain, and the picture flickers between its own size and the
screen's - often from the first menu, before the player can reach the
game's resolution setting. It also drove the Xwayland dma-buf leak fixed
separately.

The switch stays in Settings > Games and the session drawer, now off
unless turned on; its hint says when to turn it on (a game coming back
small or in a corner after switching apps, e.g. FlatOut) and when to
leave it off (flicker). Users who already set it keep their choice. The
session script's fallback when the app passes nothing is off as well.

Co-authored-by: The412Banner <[email protected]>
2026-10-05 12:44:45 -04:00
The412BannerandThe412Banner 0607bb0b3a Fix Xwayland dma-buf leak: close Zink's emulated GEM handles on KGSL (#275)
Close Zink's emulated GEM handles on KGSL (Xwayland dma-buf leak)

The session preload hands out GEM handles on the KGSL render node by
duplicating the dma-buf descriptor, and released them only through
drmCloseBufferHandle. Zink gives handles back with
drmIoctl(DRM_IOCTL_GEM_CLOSE) instead, which KGSL refuses, so every
duplicated descriptor stayed open and kept its buffer alive.

Xwayland asks for a handle for each buffer it imports through gbm. With
"Force game windows fullscreen" fighting a game's own resolution
(DiRT 3 at 1280x720), the game recreates its swapchain many times a
second and Xwayland piled up ~2,000 old swapchain images (6+ GB) in
seconds until Android killed the session. A preload trace on the device
put 2,090 of 2,118 open dma-buf descriptors in Xwayland on
drmPrimeFDToHandle called from Zink.

Handle GEM_CLOSE on the KGSL node in drmIoctl, and check the three DRM
symbols are exported in CI.

Co-authored-by: The412Banner <[email protected]>
2026-10-05 12:44:16 -04:00
Kurt Himebauch b89d87cbde Add app UI scale presets to Setup (#272) 2026-10-05 11:25:51 -04:00
Kurt Himebauch 06e06b9673 Fix MangoHud double-counting battery power and remaining life (#270) 2026-10-05 15:07:40 +00:00
Kurt Himebauch b896e7a3b9 Compact Steam, Desktop, and Setup settings headers (#268) 2026-10-05 10:34:31 -04:00
Kurt Himebauch c91daa9147 Keep the game undimmed while the session menu is open (#267)
Remove background dimming from the session menu
2026-10-05 09:59:35 -04:00
Kurt Himebauch 31ea194f3d Prepare 0.3.1 release (#263)
* docs: draft 0.3.1 release notes

* chore: bump version to 0.3.1 (11)
2026-10-05 07:46:36 -04:00
MaxsTechReview 472bb28c88 Fix controller rings on linked data dirs, route rumble to the controller, capture the mouse for pointer locks (#242)
- #166: export the fake-input ring paths with the spelling the session tree is bound under
  (Context.getFilesDir(), /data/user/0/...) instead of the canonical path. Where /data/user/0
  resolves to /data/data (ColorOS 16) the guest has nothing bound at the canonical prefix, so
  libfakeinput could not open the rings and every pad, physical and on-screen, was dead. The ring
  path logic moves to FakeInputRingPaths so it can be unit-tested without the native library.
- #219: play a game's rumble on the physical controller that last drove the pad, when Android
  exposes its motors (strong and weak separately on Android 12+), and fall back to the device's
  vibrator for the on-screen pad or a controller without rumble. PadBridge records the active
  controller; the on-screen pad taking over moves rumble back to the phone.
- #153: when a program takes a pointer lock (a mouse-look game through gamescope), capture a
  physical mouse and send its raw deltas as relative motion, so the camera no longer stops where
  Android's pointer meets the screen edge. Capture is released for the drawer, the PC keyboard,
  picture in picture, focus loss and pause, and when the lock ends.
2026-10-05 07:33:33 -04:00
Kurt Himebauch eaa4a7a047 Clarify display settings and organize in-session controls (#247)
* Clarify display sizing, game fit and scaling controls

* Replace aspect and resolution controls with one dimension menu

* Organize Steam session settings into tabs

* Keep session resolution out of the live controls panel

* Group live session controls and add a conditional second-screen tab

* Refine session sliders, tab icons and contextual controls

* Simplify screen effects with presets and collapsed advanced controls

* Give in-session tab icons more spacing

* Center the loading cancel button glyph
2026-10-04 23:32:30 -04:00
MaxsTechReview 4fa467c800 Fetch the Steam client again when it never shows up (#225)
A client stopped part way through updating itself can be left unable to
draw anything. Its files and our install stamp are still on disk, so the
installer never looked at it again and every session sat on "Waiting for
Steam" with nothing retried.

The session now adds a line to package/droiddeck-unconfirmed each time it
starts the client, and the app removes the file on the client's first
frame. After two starts in a row with nothing shown, the installer:
- drops Steam's update manifests, the stamp, .crash and steam.pid
- deletes package zips whose sha1 no longer matches their name
- downloads the client again over the old one

steamapps, config and userdata are left alone. An automatic repair runs
once per failure, until a start is seen again.

The Steam page's Client settings get a "Repair Steam client" item that
queues the same repair for the next start. The request is only cleared
once the download completes, so an interrupted repair is retried.
2026-10-04 21:33:16 -04:00
Kurt Himebauch beb96e6f0c docs: record Thor Steam SD allocation findings (#240) 2026-10-04 20:15:24 -04:00
Kurt Himebauch 26cb9e2f03 Add Decky plugin ZIP import (#79)
* Add Decky plugin ZIP import

* Match Decky plugin identity and loader settings on ZIP import

* Use the shared focus ring for the Decky toggle
2026-10-04 18:09:46 +00:00
MaxsTechReview 6dc74523c3 Start every title with a Valve pick on the Compatible Proton (#224)
The client runs in Steam Deck mode, where a title's "Default" is not the
"0" mapping but Valve's own pick for it: the Steam Deck compatibility
recommended_runtime (proton-stable, a pinned Proton, or native for the
Steam Linux Runtime) or an app_mappings entry in the SteamPlay manifests.
A title the session had not mapped yet - not installed, or installed
while the client was running on a build without the live helper - showed
"Default (Proton Stable)" and launched with a Proton that cannot run
here.

Every playable Windows title in the appinfo cache with such a pick is now
mapped to the default tool up front, before the client starts and in the
live helper, the same way installed titles are. A runnable tool the user
picks for one of them is kept, and they follow a change of the default.
2026-10-04 13:44:10 -04:00
MaxsTechReview 3858a69867 Start fresh installs on droiddeck-esync (#223)
In the Valheim and PEAK runs esync used the least CPU per frame. fsync
stays available, and esync still hands a Proton build without a pack to
droiddeck-fsync until its pack is published. A sync someone already picked
is kept.
2026-10-04 12:55:32 -04:00
MaxsTechReview 49a5f4aa94 ESync, FSync, NTSync + OpenNow + FEX for Desktop Apps (#197)
* Make the Compatible Proton ARM64 the default and label compatible builds

The CompatToolMapping "0" default always points at DroidDeck's Proton
ARM64 tool, and per-game picks of adopted GE/CachyOS builds are kept,
including when the shortcuts writer remaps.

The tool is named after the Proton build it runs, Proton Experimental
ARM64 (Compatible), or Proton 11.0 ARM64 (Compatible) once that newer build
is installed. Adopted builds carry the same Compatible mark, read from a
string resource the app stages into the runtime, and the Proton page
explains which builds have the prefix fixes.

The registrar is renamed steam-compatibility and the tool
droiddeck-proton-arm64. Existing installs migrate on the next session: the
old tool directory is removed, every mapping that named it moves to the new
tool, and the retired bannerlator-steam-compat script is removed from the
runtime. CI and local packaging include the renamed script.

* Drop the duplicate R import left by merging main

* Add userspace ntsync for Proton on Android

Android kernels have no /dev/ntsync and its app policy refuses futex_waitv,
so every Proton 11 build here fell back to wineserver for every wait.
libblsession now serves /dev/ntsync itself: wineserver opens a memfd region,
each sync object is a timerfd that carries its slot, and the kernel ntsync
ioctls are answered in shared memory with futexes, following the kernel
driver's wake order. Valve's Proton Experimental ARM64, GE-Proton and
proton-cachyos use it unchanged.

Locks record their owner so a killed process cannot wedge the others,
signals are held off inside each critical section, dead processes and
exiting threads are swept, and object lifetimes follow close, dup2, dup3,
close_range and received descriptors.

Fast sync is on by default under Performance > Session fixes, and
PROTON_NO_NTSYNC=1 turns it off per game.

* Add droiddeck-esync as the default sync for Proton on Android

Without a kernel ntsync driver, Proton 11 builds here fell back to
wineserver for every wait. droiddeck-esync brings eventfd and shared-memory
sync to the exact Proton build a game runs on: a pack holds that build's
ntdll.so and wineserver with the patch series in tools/droiddeck-esync
applied, and the guest runs Proton from a shadow tree that links the stock
tool and swaps in those two files. A wineserver from a pack still prefers
/dev/ntsync when a kernel driver or droiddeck-ntsync is there.

Packs are built in Valve's SDK image from each release's own wine source
(Proton Experimental ARM64 tags, GE-Proton and proton-cachyos releases); the
control build must reproduce the release binaries byte for byte, and a
signed index lists the packs. The APK bundles the packs pinned in
release.env, the app fetches newer ones, and Steam's compatibility tool
picks the pack for each Proton build, so new Proton releases get packs
without an app update.

The ntdll side reads, writes and polls its eventfds with raw system calls,
so the guest's LD_PRELOAD wrappers stay out of the sync path. The server
abandons a mutex once a thread killed by TerminateThread has really exited
and when its process dies, and a mutex's owner and recursion count change
in one atomic step, so a killed owner cannot touch the next owner's count.

droiddeck-esync is on by default and PROTON_NO_ESYNC=1 turns it off per
game; droiddeck-ntsync, the userspace ntsync, is now an opt-in toggle.

* Watch Valve's Proton releases and build droiddeck-esync packs for them

A scheduled workflow reads the Proton ARM64 builds Steam ships (anonymous
steamcmd app info) and the upstream source tags, compares them with the
previous watch state and starts the pack build for any build that has no
pack yet. The pack build applies the patch series with --fuzz=0 and runs a
smoke test of each new pack against the esstress suite before it is
published.

* Add droiddeck-fsync, keep Steam and the app on one Proton, fix DirectAudio

droiddeck-fsync: libblsession answers futex_waitv(2), which the app
sandbox refuses, in userspace, so Proton's own fsync runs on the stock
Proton files with no pack. Waiters register the words they wait on in a
table beside the fsync shared memory and sleep on a futex of their own;
FUTEX_WAKE on the fsync memory wakes the registered waiters. Slots carry
one owner word that the sweeper and the owner claim with a CAS, and the
mapping table is rebuilt under a lock held across the real munmap, so a
reused address or a recycled slot cannot lose a waiter. It stands in for
a Proton build without a pack (droiddeck-ntsync where fsync is off), takes
over for games with esync off, and the "droiddeck-fsync first" toggle
(BL_FSYNC_FIRST, also per game) puts it ahead of the packs. The games
Proton keeps off fsync and esync never get it. A launch into a prefix
whose wineserver is still running keeps that server's sync choice.

One Proton for Steam and the app: the Components page's Proton is the
default Steam uses, applied live through the client while Steam runs
(droiddeck-steam-compat) and at the next start otherwise; a choice made in
Steam's menu comes back to the app. Requests are ordered by a sequence that
never goes backwards, and the page no longer jumps back to the default
after a swap or restore.

DirectAudio: the wrapper wrote the driver key with its backslashes
stripped, so no game ever used DirectAudio. Wine 11 also loads a builtin
only from the prefix, so the driver is now linked into system32 and
syswow64 before "directaudio,pulse" is selected; mmdevapi falls back to
winepulse whenever DirectAudio cannot start.

* Pick esync, ntsync or fsync from the Steam settings' Games group

A three-way tab row above the FEX preset sets the sync Proton games use. It
writes the existing switches together (droiddeck-ntsync, droiddeck-fsync
first, droiddeck-esync), so the Performance toggles and the tabs always agree,
starts on esync and keeps the choice until it is changed again.

* Make droiddeck-fsync the default sync for Proton games

A fresh install, or one that never picked a sync, starts on fsync; a choice made
in the Steam settings' Games group still sticks. Games Proton keeps off fsync
still fall back to droiddeck-esync.

* Add wineserver to the Wine sync tabs and order them ntsync, fsync, esync, wineserver

wineserver turns every in-process sync off so games wait through wineserver;
picking any other tab turns sync back on. The tabs shrink to fit a narrow pane.

* Shorten the Wine sync description

* Answer openat2 and execveat for FEX in the session preload

FEX opens every file of its x86 rootfs with openat2(RESOLVE_IN_ROOT) and
only falls back to the host path on EXDEV. Under proot on Android openat2
answers ENOSYS, so each lookup of an x86 library silently went to the host
and missed. The exported syscall() now turns openat2 into openat once the
real call reports ENOSYS: RESOLVE_IN_ROOT paths are taken relative to the
root fd (FEX has already resolved the links inside the rootfs), and an
absolute path under RESOLVE_BENEATH fails with EXDEV as the kernel would.

execveat with an absolute path, or relative to AT_FDCWD, and without
AT_EMPTY_PATH or AT_SYMLINK_NOFOLLOW, is plain execve; it is sent as such
because proot refuses execveat with a directory fd, which FEX uses when it
re-executes itself for a child x86 program.

* Run x86 Linux games and programs through FEX

Steam: Linux-only titles that have no ARM64 build (from the client's
appinfo cache: type game, demo, application or beta) are mapped to a new
compatibility tool, droiddeck-fex ("Linux x86 (FEX)", linux to linux,
no required runtime). Valve's own "fex" tool needs pressure-vessel, which
cannot run under proot, so a mapping to it or to a Steam Linux Runtime is
replaced; any other pick the user made stays. Titles that also ship for
Windows stay on Proton. When such a title is installed and FEX or the
sniper runtime is missing, the client is asked to install them.

droiddeck-fex runs the program under the FEX that Steam installs (app
3127680), with Steam Linux Runtime 3.0 (sniper, app 1628350) as the x86
rootfs:
- the sniper platform is linked in place from its usr-mtree (relative
  links only, files/usr -> .), once per runtime version;
- the x86_64 and i386 builds of libblsession and libfakeinput go into the
  runtime's lib/<triplet>/droiddeck and its etc/ld.so.preload, mirroring
  the session's own list, so x86 programs get the pads and the session
  fixes (FEX passes their system calls straight to the kernel, so the
  ARM64 preloads cannot reach them);
- Config.json sets RootFS and the GL and Vulkan thunks, so x86 games draw
  with the device's Turnip and Mesa;
- the FEX preset and the game's own environment from the Steam settings
  apply, and ARM64 entries are dropped from LD_PRELOAD/LD_LIBRARY_PATH.
It also links ~/.steam/sdk64 and sdk32 to the client's x86 steamclient
when the client has one and nothing is there yet.

Desktop: x86_64 and i386 AppImages can be imported now; they unpack
through FEX (or unsquashfs) and record their architecture. Scripts,
AppImages and programs started in a run session use FEX automatically
when they are x86, and each added app has a "Use FEX" Auto/On/Off choice
in its edit dialog. The launchers' "still running" checks also look at
FEX processes, whose exe is FEX rather than the program. A GitHub release
without an ARM64 AppImage falls back to its x86_64 one. Decky's x86
loader starts through droiddeck-fex instead of the fex-mesa rootfs path,
which no runtime ships.

The x86 preloads are built against glibc 2.31 (Debian 11) so they load
in sniper: tools/linuxfs/build-x86-preloads.sh, run in CI and by
build_local.sh. fakeinput's stat hooks fall back to __xstat on a glibc
without a stat export, and mmap64 takes off64_t for the i386 build.

* Keep a FEX unixlib from outliving its DLL

A FEX package or original bundle without the aarch64-unix helpers left
the previous version's libarm64ecfex.so / libwow64fex.so in the Proton,
so the new DLLs loaded a unixlib built for another FEX. A swap now removes
the helpers the package does not carry; FEX then runs DLL-only, which it
already falls back to when Wine cannot load the unixlib.

* Fix the Wine sync choices an audit found

- wineserver tab: it passed Proton's environment through, so a build with
  esync built in (GE, cachyos) still ran esync and a kernel ntsync would
  still be used. It now sets PROTON_NO_ESYNC, PROTON_NO_FSYNC and
  PROTON_NO_NTSYNC (unless the game's own environment sets them).
- ntsync tab: a Proton whose wineserver has no /dev/ntsync client got
  wineserver-only sync although fsync would work. The hash cache records
  whether the wineserver has one; without it the launch goes to
  droiddeck-fsync, and no stand-in or pack turns droiddeck-ntsync on.
- A build with esync built in returned before the per-game fallbacks, so
  a game that turns esync off ran with no in-process sync. It now gets
  droiddeck-fsync, or droiddeck-ntsync where fsync is off too.
- When a pack's proton cannot start and the stock one is used, or the
  selection fails, the prefix's record now says stock, so a second
  launch into the same running prefix no longer picks the pack's ntdll.
- droiddeck-fsync: its mutex is held across fork and re-made in the
  child, a closed or replaced shm fd is forgotten (close, dup2, dup3,
  close_range, closefrom), and a waiter woken while it was leaving on a
  timeout or signal reports the wake instead of losing it.
- BL_SYNC_MIN_NOFILE applies per game to every verb, debug logs carry
  prefixes.json, the device report names the chosen sync instead of the
  three raw switches, and PATCHES.md and the proot notes describe the
  selector, the fsync default and droiddeck-fsync's futex_waitv.

* Name the runtime's scripts and files droiddeck, migrating old installs

The guest scripts are droiddeck-* now (droiddeck-session and the other 19),
as are the files the app and scripts generate (etc/droiddeck, the network
state files, the components dir, the Steam install stamp, the redist marker,
the extra-Proton wrapper and backups) and the compositor's own names
(libdroiddeckwayland, droiddeck_* sources, DROIDDECK_* switches, the
wl_output and seat names).

Installed runtimes carry on: LegacyNames moves etc/bannerlator and
share/bannerlator over and leaves relative links at the old paths before
every proot command, turns any old script into a link to its new name,
moves the generated files and points menu entries at the new scripts.
The session scripts and steam-compatibility move their markers on first
use, the components dir's desired.tsv is rewritten with it, and the
runtime counts as installed under either session script.

Kept: the banner_ahb_v1/banner_desktop_v1 wire names and BANNER_AUDIO_DIRECT
(read by prebuilt Turnip and Wine builds), Banners-Turnip and other
third-party names, BL_* switches, libblsession, and credits to Bannerlator.

* Unpack x86 AppImages without FEX, then ask Steam for FEX

Importing an x86 AppImage failed when FEX was not installed yet ("needs FEX
to unpack and no unsquashfs is installed"). AppImages now unpack with
uruntime (VHSgunzo, MIT, v0.8.1 aarch64, pinned by sha256 and shipped
unmodified with its licence), which reads SquashFS and DwarFS images of any
architecture natively. FEX's own --appimage-extract and unsquashfs stay as
fallbacks, and an ARM64 image whose own runtime cannot unpack it goes the
same way.

An x86 import with FEX missing records that FEX is wanted, so the next
Steam session requests FEX and the Steam Linux Runtime, and the program's
page says FEX is not set up yet and how to get it.

* Run Vulkan Qt Quick AppImages on Wayland

OpenNOW (Qt 6.8, Qt Quick on Vulkan) closed within two seconds in a run
mode. The session sends toolkits to gamescope's Xwayland, and Qt's xcb
backend only presents with Vulkan when Mesa's X11 WSI does, which needs
DRI3; Xwayland on KGSL has none, so Qt found no queue that can present and
aborted ("Failed to create RHI").

An AppImage that bundles Qt Quick, the Vulkan loader and Qt's Wayland
plugin now runs on gamescope's Wayland socket (the desktop's, outside
gamescope), where Turnip presents as it does for games, with Qt's xcb
backend as the fallback. SDL_VIDEODRIVER and XDG_SESSION_TYPE follow, so
helpers that pick their window system from them (OpenNOW's streamer) agree
with Qt. Everything else stays on X11; a qt file in the app's directory
(wayland or xcb) overrides the choice.

* Keep gamescope's WSI layer away from Qt apps on its Wayland socket

OpenNOW still closed at once: its FFmpeg (the Raspberry Pi fork, libavutil
61) creates its Vulkan queues internally synchronized whenever the driver
offers VK_KHR_internally_synchronized_queues, which DD-Turnip does. The
gamescope WSI layer registers every queue of a new device through
vkGetDeviceQueue, which only returns queues created without flags, so it
got VK_NULL_HANDLE and vkroots aborted on assert(obj) before Qt opened a
window.

Programs that draw on gamescope's Wayland socket do not need the layer,
which only carries X11 windows past Xwayland, so it is turned off for
them. OpenNOW bundles no Vulkan loader (it reaches Vulkan through Qt and
dlopen), so a binary that uses QVulkanInstance counts as a Vulkan app too,
and a Qt Quick app left on X11 says why in the session log.

* Keep the end of the session log, and say when FEX is missing

The session sends its output through head -c into session.log. head
writes through stdio, so it held up to 4 KB that proot's --kill-on-exit
threw away when the guest ended: every log stopped mid-line at a 4096-byte
boundary, losing exactly the lines that say why a program quit (an x86
AppImage's "x86 Linux programs need FEX" among them). dd writes each read
as it comes and keeps the same cap.

A run mode always ends with status 0 (gamescope does not pass its child's
on), so a program that could not start just closed. When the log says FEX
is missing, the ended screen now says so and how to get it.

The app reads FEX's readiness from status.json, which only a launch or an
import wrote, so it still said "not set up" after Steam installed FEX.
droiddeck-fex status rewrites it, and a Steam session ends with it while
FEX is wanted. FEX is also looked for at the tool's root and under files/,
and a tool folder without it lists what it holds.

* Let FEX and its runtime install runnable on an SD card library

A Steam library on shared storage loses the executable bits, so native
tools are staged in private directories bound over it; only Proton and
SteamLinuxRuntime folders were. FEX (3127680) installed there could never
run, droiddeck-fex saw no FEX and kept asking Steam for it. FEX and the
sniper runtime (1628350) now get private directories from their first
download, whatever their install folder is called.

* Keep fsync off in the pack smoke test

A runner whose kernel allows futex_waitv started stock Proton with fsync,
so the server-side baseline never said what the test waits for and every
pack was waved through as untestable; the esync run would have measured
fsync too.

* Judge a pack against stock's own esstress failures

Stock Proton fails esstress's dup wait all under server-side sync as well
(a Wine bug, seen on the phone too), and any stock failure made the test
call the runner unusable. A pack now fails only when it crashes, hangs or
fails a check stock passes.

* Ask Steam for FEX and its runtime one at a time

Both steam://install requests were started together, and the running client
took only the first: FEX installed, the sniper runtime FEX needs never did.
Each request now waits for the client to take it before the next.

* Bundle the first published droiddeck-esync packs in the apk

* Log how an x86 program under FEX ends

FEX keeps its own errors quiet unless asked, so a program it could not run
left nothing in the session log. Its log now goes to stderr, and the AppImage
launcher records the program's exit status or the signal that ended it.

* Show the log a failed AppImage program wrote

Programs like Eden stop themselves on a failed check and say why only in
their own log under the home folder. When the program ends badly, the end of
any log it wrote during the run is copied into the session log.

* Report where an x86 program under FEX hit an illegal instruction

A tiny x86_64 preload, built without libc, catches SIGILL and writes the
address, the instruction bytes and the mapping they belong to before the
program dies as it would have. droiddeck-fex preloads it into x86_64 programs.

* Load the fault reporter from the x86 runtime

Programs under FEX look libraries up in the x86 runtime, so the reporter is
copied there with the other preloads and named by its path inside it.

* Hand FEX its x86 runtime and thunks through the environment

Valve's FEX runs portable and reads its config beside itself, so the
Config.json droiddeck-fex writes was not seen: programs ran without the
runtime. The runtime, thunk libraries and thunk list now also go in FEX's
own environment variables, and each launch says what it was given.

* Use the sync pack built for the installed Proton release

Some Proton releases ship byte-identical ntdll.so and wineserver (Valve
11.0-2b/2c, CachyOS 0702/0703), so a pack built for a newer release
matched an older install exactly and the older install never fetched its
own pack. A pack from a newer release no longer counts as an exact match;
among matches the one built for the installed version wins, then rev.

The app ranks the pack a tool already uses the same way and only fetches
a pack that ranks higher, reads the release epoch from tools.tsv and
refetches when tools.tsv changes. A prefix whose server is still running
re-checks its recorded pack on waitforexitandrun and starts fresh when a
better one now applies.

* Keep published sync packs and revocations consistent

The publish step never replaces a pack that has an entry beside it; a
pack left without its entry by an interrupted run is replaced so the
build stops repeating. Discovery counts revocations the signed index does
not show yet, including superseded packs only the flavor releases list,
so the index is signed again. The release watch always builds every
flavor.

* Fetch the droiddeck-esync packs from DroidDeck-Components

The packs, the signed index and the apk bundle now live in the releases
of Droid-Deck/DroidDeck-Components, together with the pipeline that
builds them, so this repository keeps only the app and the guest code
that consumes them. The app's index and asset URLs point there,
release.env names the repository its bundle comes from, and the apk
builds drop bundled packs that the published index revokes.

* Stop the apk build when the published sync index cannot be read

* Ask Steam for one FEX dependency at a time

The client shows a confirmation dialog for each steam://install, and a
second request replaces the first dialog, so FEX was never installed
when the runtime was requested right after it. The next dependency is
now requested once the previous one has an app manifest.

* Use generic names in the AppImage test fixtures

* Give x86-64 apps under FEX the Vulkan thunk

Apps that bring their own glibc and Vulkan loader skipped FEX's thunk
overlay and loaded their bundled desktop Mesa drivers, which crash on the
phone's GPU during Vulkan setup. droiddeck-fex now adds an x86-64 audit
module when a program's bundled glibc and libstdc++ meet the thunk's
version needs. The module sends libvulkan.so.1 lookups to a nodelete stub
that needs FEX's thunk path. That keeps the thunk mapped when an app
unloads Vulkan and loads it again, since FEX keeps its first trampoline
links and the old addresses get reused.

The fault reporter now restores the default action before re-raising,
because FEX ignores SA_RESETHAND and the handler looped.
2026-10-04 12:43:57 -04:00
starwishHi 4cc9f2a3f3 Add Korean,Japanese and Traditional Chinese(Taiwan,Macau and Hongkong)translate (#216) 2026-10-04 12:13:13 -04:00
Kurt Himebauch b0d7fc01d8 Add opt-in Steam storage diagnostics (#158) 2026-10-04 08:19:48 -04:00
Magarto 9cc40246a3 Start .NET and Godot games under FEX without per-game setup (#168) 2026-10-04 08:19:11 -04:00
The412Banner 423cf11919 Effects page in the session menu: scaling modes, screen effects and texture filtering (#198) 2026-10-04 08:17:00 -04:00
Dantalian 33f7213599 l10n: add Simplified Chinese (#202) 2026-10-04 11:21:24 +00:00
Kurt Himebauch 6f1fb462ed Add controller and desktop quality-of-life options (#194) 2026-10-04 00:56:09 -04:00
Kurt Himebauch 336b8851f7 Add Touch Off and compact session menu shortcuts (#196)
Add touch off mode and compact session menu shortcuts
2026-10-04 04:37:56 +00:00
Kurt Himebauch 5da3cf673c Fix automatic Picture in Picture after session handoffs (#195)
Preserve session readiness for automatic picture in picture
2026-10-04 04:24:25 +00:00
Kurt Himebauch 9e54e094fb Restore 500 ms Back double-press window (#193) 2026-10-04 03:40:10 +00:00
Kurt Himebauch eb9ca4ab6b Remove redundant in-session Desktop button (#188) 2026-10-03 23:00:02 -04:00
MaxsTechReview e7b6876877 Add the droiddeck-esync pack pipeline (#189)
Builds esync packs for new Valve, GE and CachyOS Proton 11 releases on arm64
runners, smoke-tests them against stock with esstress, and on main publishes
each pack and a signed index as non-latest releases. A watcher checks Steam
and the GE/CachyOS releases every 20 minutes and starts a build for new ones.
The app side lands separately with feat/userspace-esync-ntsync.
2026-10-04 02:58:55 +00:00
Kurt Himebauch c535f6ef6e Empty library intro (#182) 2026-10-03 22:27:24 -04:00
Kurt Himebauch db2e1d7da2 Fix runtime removal and first-run Steam setup (#185)
* Fix runtime removal races and show removal progress

* Bootstrap Steam from the selected client channel

* Show runtime progress below its Setup row

* Always stage current session helpers and verify APK assets

* Initialize Steam UI scale once using the handheld default
2026-10-03 22:03:26 -04:00
Kurt Himebauch f5e204eb1d Add game file exports and sync for Android frontends (#179) 2026-10-03 21:29:01 -04:00
Magarto 705a5e7b2a Give protonfixes a readable /proc/sys/fs/file-max (#169) 2026-10-03 15:26:20 -04:00
Kurt Himebauch e60eae3646 Add standard session picture in picture (#156)
* Add session picture in picture controls

* Simplify PiP to standard Android controls

* Remove PiP testing document
2026-10-03 15:06:16 -04:00
Kurt Himebauch b04900b1b2 Merge pull request #172 from jmarti326/fix/steam-sleep-resume-black-screen 2026-10-03 14:28:24 -04:00
Kurt Himebauch 6357543b44 Merge branch 'main' into fix/steam-sleep-resume-black-screen 2026-10-03 14:18:44 -04:00
Kurt Himebauch 67fb338e5d Merge pull request #159 from Droid-Deck/preview-change-history
Show Preview update history
2026-10-03 13:36:00 -04:00
xXJsonDeruloXx ba140c4ab5 Merge remote-tracking branch 'origin/main' into preview-history-status-card 2026-10-03 13:30:13 -04:00
xXJsonDeruloXx f5fdbc7941 Move Preview history below the update action 2026-10-03 13:27:09 -04:00
Juan Marti Mercado 90ae85da4d Fix Steam sleep resume handshake 2026-10-03 12:08:40 -04:00
Kurt Himebauch f3600fa618 Merge pull request #157 from Droid-Deck/game-launcher-shortcuts 2026-10-03 12:04:11 -04:00
xXJsonDeruloXx 2d8befe1cb Add Preview update history 2026-10-03 00:06:09 -04:00
xXJsonDeruloXx f982ffcaf7 Add game launcher shortcuts 2026-10-02 23:49:26 -04:00
Kurt Himebauch 255c64551e Merge pull request #128 from magarto/upscaler-setting
Expose the compositor's upscaler (FSR 1 / GSR / NIS) as a session setting
2026-10-02 23:29:01 -04:00
Kurt Himebauch 746f28c1b2 Merge branch 'main' into upscaler-setting 2026-10-02 23:25:35 -04:00
Kurt Himebauch 2b481ba250 Merge pull request #155 from Droid-Deck/fix/steam-idle-wake
Fix Steam idle sleep and Android session wake
2026-10-02 21:44:13 -04:00
xXJsonDeruloXx 49622623de Merge remote-tracking branch 'origin/main' into fix/steam-idle-wake 2026-10-02 21:43:52 -04:00
Kurt Himebauch 69f99ade42 Merge pull request #146 from Droid-Deck/fix/deck-controller-discovery 2026-10-02 21:34:08 -04:00
xXJsonDeruloXx c236e9711f Restore Steam after idle sleep through session pause and login1 wake 2026-10-02 21:32:12 -04:00
Kurt Himebauch 4c2efb6688 Merge branch 'main' into fix/deck-controller-discovery 2026-10-02 20:51:17 -04:00
xXJsonDeruloXx 7eb2d1cf1a Trim Deck discovery to mount-ID compatibility fix 2026-10-02 20:35:28 -04:00
xXJsonDeruloXx b4ff83cf9f Recover Deck discovery mount IDs on older kernels 2026-10-02 19:08:01 -04:00
Kurt Himebauch 7b20b70c6b Merge pull request #151 from Droid-Deck/feat/android-clipboard 2026-10-02 17:47:25 -04:00
xXJsonDeruloXx 86207f9d0e Record visible Android copy and Steam paste test on Thor 2026-10-02 17:38:16 -04:00
xXJsonDeruloXx eda5ece382 Add Android clipboard text paste into Steam sessions 2026-10-02 17:29:41 -04:00
Kurt Himebauch 3404bbc128 Merge pull request #150 from Droid-Deck/feat/steam-native-network
Show Android Wi-Fi and cellular networks in Steam BPM
2026-10-02 16:16:46 -04:00
xXJsonDeruloXx b5a431aa3c Merge remote-tracking branch 'origin/main' into feat/steam-native-network 2026-10-02 16:11:04 -04:00
xXJsonDeruloXx 49249f6541 Add optional Wi-Fi discovery permission flow in Steam settings 2026-10-02 16:05:47 -04:00
xXJsonDeruloXx 5614621d6e Expose Android Wi-Fi names and nearby networks to Steam 2026-10-02 15:39:49 -04:00
xXJsonDeruloXx 0816562065 Report Android Wi-Fi and cellular connections to Steam 2026-10-02 15:05:03 -04:00
Kurt Himebauch 39618e6862 Merge pull request #147 from Droid-Deck/controller-overlay-toggle 2026-10-02 14:56:02 -04:00
xXJsonDeruloXx c66e4ba4b3 Release controls when touch is cancelled 2026-10-02 13:48:19 -04:00
xXJsonDeruloXx e4b033861f Add quick toggle for on-screen controls 2026-10-02 13:28:13 -04:00
xXJsonDeruloXx 85a07a556c Preserve native HID discovery and trace Deck enumeration 2026-10-02 12:00:12 -04:00
xXJsonDeruloXx 8dfc48d58b Fix Deck controller discovery across sysfs layouts 2026-10-02 10:45:25 -04:00
Magarto 82ebdaa0b5 Merge branch 'main' into upscaler-setting 2026-10-02 15:38:57 +02:00
Kurt Himebauch ec016f85bc Merge pull request #142 from Droid-Deck/xXJSONDeruloXx-patch-1
Update README.md
2026-10-02 01:55:28 -04:00
Kurt Himebauch c2c812deaa Update README.md 2026-10-02 01:55:15 -04:00
Kurt Himebauch eec3428f78 Merge pull request #141 from Droid-Deck/release/0.3.0
Release 0.3.0: version bump, Deck mode for everyone, release notes and media, Updates page fix
2026-10-02 01:39:31 -04:00
xXJSONDeruloXx 4a5e4b76e3 ci: publish the GitHub release from the Release APKs run, with the notes from docs/releases 2026-10-02 01:34:41 -04:00
xXJSONDeruloXx 43f952b2fb discord link and cleanup 2026-10-02 01:32:12 -04:00
xXJSONDeruloXx 0d86bec34d docs: drop the unused overlay GIF and the 'as above' it left behind 2026-10-02 01:16:04 -04:00
xXJSONDeruloXx 47a6f29ad7 docs: in-game GIF for the faster-in-games highlight 2026-10-02 01:15:25 -04:00
xXJSONDeruloXx 84834f180e docs: GPU drivers shot with DD-Turnip in use 2026-10-02 01:03:16 -04:00
xXJSONDeruloXx bfd445618a docs: dual-screen Deck controller and Stop GIFs, Deck mode for everyone in the 0.3.0 notes 2026-10-02 00:59:48 -04:00
xXJSONDeruloXx eddfd37b93 feat: move every install to Deck mode once, so updated installs get it too 2026-10-02 00:59:48 -04:00
xXJSONDeruloXx d7341863bf docs: show added apps on the Desktop page in the 0.3.0 notes 2026-10-02 00:49:37 -04:00
xXJSONDeruloXx 2f51be7b5a docs: 0.3.0 release notes with screenshots and GIFs 2026-10-02 00:47:35 -04:00
xXJSONDeruloXx fbc0014d14 fix: say Can't install, not Update available, for a build Android would refuse 2026-10-02 00:47:35 -04:00
xXJSONDeruloXx 46df6171fc release: 0.3.0 2026-10-02 00:47:35 -04:00
Kurt Himebauch 761b62914f Merge pull request #140 from Droid-Deck/chore/ui-cleanups
UI cleanup for 0.3.0: motion, focus ring, copy and game environment editor
2026-10-02 00:22:26 -04:00
Kurt Himebauch 91b0299f77 Merge branch 'main' into chore/ui-cleanups 2026-10-02 00:18:04 -04:00
xXJSONDeruloXx 6d08e59d10 frontend: draw the Steam wall on the first frame and keep it still
The wall waited about a second and a half for the game list on every start: Steam's own
manifests are quick, but the added games' folder walk and art lookups are not. The last list
is now kept in the app's files (LibraryCache) and shown before the first frame, and the fresh
one replaces it in a single update. The wall's order is fixed (last played, then game ID), so
the same games always land in the same capsules and the refresh moves nothing. The added-games
scan on every resume also leaves the main thread, which it stalled for most of a second.
2026-10-02 00:12:41 -04:00
xXJSONDeruloXx 83cc0fe585 ui: every way out of a session leaves as a session arrives
Backing out of the loading screen, closing the ended screen and the session ending on its own
used the slide-down card exit. Now, while the session is on screen, the blue spreads out of
the throbber's ball (the middle of the screen once a game is up) until it covers the screen,
and the front end opens on it and draws it back into the button the session came from, as it
already did after the drawer's Stop. A retry, a session closing off screen and animations off
still close at once; a timeout closes it should the flood never run.
2026-10-02 00:05:57 -04:00
xXJSONDeruloXx c711c627b8 i18n: add Spanish
All string resources get a Spanish translation except brand names and strings that are only a
format. The app declares English and Spanish in a locale config, so it is offered in Android's
per-app language setting and follows the system language otherwise. Short rail labels keep
to the rail's width (Configuración, Actualizar).
2026-10-01 23:56:02 -04:00
xXJSONDeruloXx 0b34690721 ui: the Games page keeps its game while it animates out
It was drawn with the current selection, so leaving it for another rail item turned the
outgoing page into a copy of the new one for a few frames, which then faded out before the
real page rose in: a flash and a dip moving from Games to Steam.
2026-10-01 23:47:50 -04:00
xXJSONDeruloXx b640734db5 ui: game environment editor works with a pad, on/off variables use the dropdown
Opened with a pad, the editor and its variable dialog put focus on their first control; as
windows of their own they otherwise gave the pad nothing to move from. On/off variables pick
from their two values in the same dropdown as other choices instead of a switch. App dialogs
keep the page's focus ring out of sight behind them. The footer's Add variable button only
appears once the settings are loaded: drawn disabled first, it came up without its box.
2026-10-01 23:37:41 -04:00
xXJSONDeruloXx f8feb63dde ui: rebuild the game environment editor in the app's own style
The settings row gets a one-line hint and the same secondary button as every other row. The
editor moves from a stock Material alert, with a paragraph of explanation and a column of text
buttons, into the app's dialog: a profile chip with its menu, the variables in a panel with
each one's value as a chip whose menu changes, removes or restores it, and a footer to add a
variable, reset the profile or finish. Editing a variable uses the same dialog, its name and
value as chips or the app's text fields, and the variable's note in the footer.
2026-10-01 23:24:11 -04:00
xXJSONDeruloXx 25a5d3fe5d ui: paging the session drawer lands on the page's first option
LB, RB or a tab now start focus at the top of the new page instead of wherever it was last
left on that page; closing a menu or reopening the drawer still returns to where it was. On
Settings the top option is Android apps when DroidDeck is the Home app.
2026-10-01 23:10:01 -04:00
xXJSONDeruloXx 41dd93fd25 ui: long focus jumps travel as a solid dot and take shape on arrival
The ring fills in as it pinches to a dot, crosses as one round dot on a timed ease with no
stretch, and opens into the control as it gets there instead of settling as a circle first.
2026-10-01 23:02:37 -04:00
xXJSONDeruloXx adf3c9405d ui: focus ring nits - held moves, far jumps, floods, selection rings
- A held direction runs the ring along as one piece and keeps its speed between steps;
  restarting each edge from rest (and the trailing edge's delay) made it stutter.
- A jump of more than 360dp pinches the ring to a drop, carries it over and opens it onto
  the new control instead of stretching a box across the screen.
- The ring keeps out of launch, stop and return floods (RingVeil) and appears on the
  focused control once they are done.
- Leaving a page by moving along the rail no longer pulls focus into the new pane; only
  backing out of a page hands focus to the control it was opened from.
- Selection is a radio dot or a tick, never a ring: the Updates channels and test builds,
  the GPU modes and driver pairs, the Games list and suggested app icons lose their
  persistent rings, and the GPU cards use the sliding ring for focus like everything else.
2026-10-01 22:57:32 -04:00
xXJSONDeruloXx abf4f57b47 ui: the cog floods its settings in as Play floods a session
The cog's tile stretches over the pane on the same loose springs as Play, far edges first and
blobby in flight, while the page behind sinks; the settings rise in once it is covered. Back
draws the tile down into the cog on critically damped springs. The focus ring stays out of
it and appears on the focused control once the motion is done, replacing the ring riding the
edge of a stretching box.
2026-10-01 22:44:32 -04:00
xXJSONDeruloXx c1e36975d4 ui: the focus ring carries the page bloom, focus returns to its opener
The bloom is now the opening control's own rounded box stretching out to the pane, not a
circle. With a controller the focus ring rides its edge out to the pane's frame and springs
in onto the page's first control; on Back it rides the box back into the control, which
takes focus again. By touch, with no ring up, the bloom traces the same edge itself.

Closing a page used to leave focus on the page while it animated out, so nothing took it
back afterwards.
2026-10-01 22:37:47 -04:00
xXJSONDeruloXx 0bfa0915d4 ui: bloom session settings out of the cog, open it without blocking
The page grows from the cog as a filled disc with an accent rim and folds back into it on
Back, while the pane under it sinks and returns. Pages also keep drawing while they leave
instead of falling back to the missing-page note for a frame.

Opening the settings no longer scans the added-games folders, reads the driver files and the
storage volumes on the main thread first: the page opens at once and those land while it
animates in.
2026-10-01 22:28:32 -04:00
xXJSONDeruloXx 4da87672a2 ui: move remaining launcher and second-screen copy to string resources 2026-10-01 22:15:45 -04:00
xXJSONDeruloXx 3ba0d287e9 ui: move content, games, GPU drivers and loading copy to string resources 2026-10-01 22:14:13 -04:00
xXJSONDeruloXx b4a86cb5bb ui: move store, updates, controller and child-process pages to string resources 2026-10-01 22:12:42 -04:00
xXJSONDeruloXx 0fa1f2f868 ui: move settings, components, drawer and setup copy to string resources, tighten wording 2026-10-01 22:10:22 -04:00
xXJSONDeruloXx 71610ddfdb ui: remove controller hints bar, route all motion through Motion
Compose already scales its animation clock by the system animator duration
scale, so Motion.tw no longer multiplies durations a second time; Motion.ms
stays for coroutine delays. The drawer tabs, launch flood and the default-spec
expand/fade transitions now go through Motion, keeping their current speeds.
2026-10-01 22:04:36 -04:00
Kurt Himebauch 5c9e683769 Merge pull request #138 from Droid-Deck/fix/second-library-steam-installs
fix: keep Steam-installed second-library games out of non-Steam shortcuts
2026-10-01 21:51:05 -04:00
Kurt Himebauch 32a5e83b88 Merge branch 'main' into fix/second-library-steam-installs 2026-10-01 21:36:40 -04:00
Kurt Himebauch 88904c08f7 Merge pull request #139 from Droid-Deck/fix/pr-release-publish
fix: find the stable app release and accept fork builds when signing PR test builds
2026-10-01 21:35:02 -04:00
xXJsonDeruloXx 9928bfe780 ci: keep component releases off latest and pre-release 2026-10-01 21:34:06 -04:00
xXJsonDeruloXx a491dec5c4 fix: check PR test builds against main's versionCode instead of a release 2026-10-01 21:29:01 -04:00
xXJsonDeruloXx 17ef33ee31 fix: find the stable app release and accept fork builds when signing PR test builds 2026-10-01 21:19:55 -04:00
xXJsonDeruloXx 06caaee343 fix: keep Steam-installed second-library games out of non-Steam shortcuts 2026-10-01 20:47:10 -04:00
MaxsTechReview 1c161a76dd Feature adding custom Linux applications (#130)
* Add your own apps to the Desktop page

An Add tile on Desktop takes a .sh script, an AppImage, a GitHub repo
(newest ARM64 AppImage release) or a Flatpak ID, with an optional name
and icon. Script folders are linked into the session, or copied when
they hold Linux programs (shared storage is noexec); folders outside
internal storage are bound at their own path.

Each app's page opens it under gamescope, edits its name and icon (with
icons suggested from its GitHub repo), checks a GitHub app for a newer
release and updates it in place, or removes it. Replaces the beta
AppImages section.

* Put Add at the end of the installable emulators

Installed emulators and added apps keep the top section, shown once there is one. Add closes the installable grid, or the installed one when nothing is left to install.

* Import DwarFS AppImages and their SVG icons

uruntime images built with sharun keep one program under several names
as hard links, which Android denies apps, so dwarfsextract aborted. The
extraction runs with proot's link2symlink, and each linked name becomes
a file of its own afterwards; their AppDir is followed through the
squashfs-root link.

An image's icon is a PNG of at least 128 px from the name its entry
gives, else its SVG (drawn with AndroidSVG), else the largest PNG, else
.DirIcon.

* Give AppImages the session's pads

sharun images start the app through their own ld.so, patched not to read
/etc/ld.so.preload, and drop LD_PRELOAD unless allowed, so the app never
loaded libfakeinput and saw no controller. The launcher now hands the
preload list over in LD_PRELOAD with SHARUN_ALLOW_LD_PRELOAD set.

Pad defaults also put Player 1 on the controller for yuzu-family
emulators, once, while it is still on the keyboard.

* Redesign the Add and Edit app dialogs

Source and appearance sit in two panels side by side, with a live
preview of the Desktop tile. A disabled primary button keeps an outline
so it still reads as a button.
2026-10-01 18:23:47 -04:00
Kurt Himebauch aa05ab03fa Merge pull request #113 from maxjivi05/feat/performance-fixes-2 2026-10-01 17:08:17 -04:00
Kurt Himebauch 43ea3f3181 Merge branch 'main' into feat/performance-fixes-2 2026-10-01 17:03:19 -04:00
Kurt Himebauch 3966b27948 Merge pull request #135 from Droid-Deck/fix/session-focus-overlay 2026-10-01 17:01:14 -04:00
maxjivi05 7e24327621 Merge branch 'main' into feat/performance-fixes-2 2026-10-01 16:40:42 -04:00
xXJsonDeruloXx d025aa3c6d Prevent full-screen Android focus highlight in sessions 2026-10-01 16:39:44 -04:00
Kurt Himebauch 325f6893c5 Merge pull request #134 from Droid-Deck/fix/secondary-library-dependencies 2026-10-01 16:35:02 -04:00
xXJsonDeruloXx 466419407d Keep secondary-library prefixes and Steam tools on private storage 2026-10-01 16:11:19 -04:00
Magarto cbd4ce6636 Merge branch 'main' into upscaler-setting 2026-10-01 22:02:07 +02:00
magartus d4130181e7 Add the upscaler to the session drawer, applied live
The drawer's Display page gets the same Upscaler and Upscaler sharpness
choices as the session settings. A pick is saved and handed straight to
the compositor, so it takes effect on the next frame without restarting
the session.
2026-10-01 21:21:52 +02:00
Kurt Himebauch 16a9d0af2a Merge pull request #131 from Droid-Deck/fix/qam-keeps-game-focused 2026-10-01 15:03:34 -04:00
Kurt Himebauch e51698e01f Merge branch 'main' into fix/qam-keeps-game-focused 2026-10-01 14:56:59 -04:00
xXJsonDeruloXx 519091dd86 chore: stage gamescope-3.16.29-p5 2026-10-01 14:53:28 -04:00
xXJsonDeruloXx 972088f1b3 fix: skip the pointer warps that jolted the game as the QAM opened and closed 2026-10-01 14:43:30 -04:00
Kurt Himebauch 542d31daf0 Merge pull request #119 from Droid-Deck/fix/adopted-storage-runtime-dir 2026-10-01 14:42:26 -04:00
Kurt Himebauch 8f9a849c48 Merge branch 'main' into feat/performance-fixes-2 2026-10-01 14:38:33 -04:00
Kurt Himebauch e8bfe6e520 Merge branch 'main' into fix/adopted-storage-runtime-dir 2026-10-01 14:36:18 -04:00
Kurt Himebauch 15618f357b Merge pull request #127 from Droid-Deck/fix/mangoapp-drm-listing 2026-10-01 14:35:58 -04:00
Kurt Himebauch b74010435a Merge branch 'main' into fix/adopted-storage-runtime-dir 2026-10-01 14:31:45 -04:00
xXJsonDeruloXx 0a58a7faee fix: keep the game running behind the Steam menu and QAM, as on a Steam Deck 2026-10-01 14:31:30 -04:00
Kurt Himebauch 0d21e2f913 Merge branch 'main' into fix/mangoapp-drm-listing 2026-10-01 14:31:23 -04:00
MaxsTechReview ebb82d2dc9 Merge branch 'main' into feat/performance-fixes-2 2026-10-01 14:03:18 -04:00
xXJsonDeruloXx 557e346810 perf: read GPU mapping sizes from maps and run costly overlay feeds only while it shows 2026-10-01 13:50:10 -04:00
xXJsonDeruloXx 08b2cf0894 feat: feed the overlay's fan, GPU memory and Valve msm reader paths 2026-10-01 13:28:26 -04:00
Magarto 4e90e03536 Merge branch 'main' into upscaler-setting 2026-10-01 19:10:55 +02:00
Kurt Himebauch 11d997dc65 Merge pull request #129 from maxjivi05/fix/lsfg 2026-10-01 13:08:37 -04:00
Magarto 454193da9b Merge branch 'main' into upscaler-setting 2026-10-01 19:06:00 +02:00
MaxsTechReview cdd5c99bc7 Merge branch 'main' into fix/lsfg 2026-10-01 13:02:30 -04:00
xXJsonDeruloXx fb1da1c39a fix: feed the overlay's GPU stats from every Adreno source other Android overlays read 2026-10-01 12:58:47 -04:00
xXJsonDeruloXx b29ece82c8 fix: give the session a live /proc/stat and read GPU load and clock where other overlays do 2026-10-01 12:53:17 -04:00
Kurt Himebauch 1330fe4d6c Merge pull request #63 from magarto/stretch-to-panel 2026-10-01 12:45:18 -04:00
Kurt Himebauch 0e1f6f15a1 Merge branch 'main' into feat/performance-fixes-2 2026-10-01 12:44:00 -04:00
Kurt Himebauch 4df0e012fd Merge branch 'main' into stretch-to-panel 2026-10-01 12:41:57 -04:00
xXJsonDeruloXx 48054a86ab fix: keep the performance overlay running and fed under an enforcing SELinux policy 2026-10-01 12:38:30 -04:00
magartus 38d658abf5 Expose the compositor's upscaler as a session setting
The Wayland compositor already carries the X11 renderer's spatial
upscalers (effects_chain.c: FSR 1 EASU+RCAS, Snapdragon GSR and its
quality variant, NIS, RCAS sharpen) behind
WaylandCompositor.nativeSetUpscaler / nativeSetUpscaleSharpness, but
nothing calls them since the Winlator-era drawer was removed, so the
scene is always enlarged to the panel with a plain linear blit.

Add "Upscaler" and "Upscaler sharpness" to the session page's Display
group (off by default, 75%), stored in SessionPrefs and applied next to
nativeSetScaleMode when the session's surface is set up. The choice is
also listed in device.txt.
2026-10-01 18:38:02 +02:00
MaxsTechReview 5547391cfc Merge branch 'Droid-Deck:main' into fix/lsfg 2026-10-01 12:34:24 -04:00
Kurt Himebauch 6782e5a717 Merge pull request #125 from Droid-Deck/feat/input-evidence-logging 2026-10-01 12:14:15 -04:00
xXJsonDeruloXx e1a80c1751 fix: let the Steam client find the Deck controller under an enforcing SELinux policy 2026-10-01 12:00:24 -04:00
maxjivi05 05933a5ee3 Clock frame generation by the game's frames
With frame generation on, a scene drawn for another window (Steam's
overlay) went into the engine as a game frame. Adaptive counted it as
source rate and planned against that (36 base for a game capped at 30),
and a fixed multiplier gave it present slots while game frames were
dropped (26 -> 51 at 2x). Such a change now waits for the game's next
frame.

The HUD's base rate is the game window's own frames, as Steam's overlay
counts them, instead of the compositor's scene rate.
2026-10-01 11:57:58 -04:00
xXJsonDeruloXx 1f29cd92a5 fix: log an empty focused-app or touch-mode property as none 2026-10-01 10:37:58 -04:00
xXJsonDeruloXx 4f30356c9b feat: always write the guest pad log to pad.log in the session folder 2026-10-01 10:35:24 -04:00
xXJsonDeruloXx baebf50197 perf: skip the per-event device lookup when input comes from the same pad 2026-10-01 09:59:13 -04:00
xXJsonDeruloXx 01674b7c6f feat: log controller, touch-mode and pad-ring evidence for input reports 2026-10-01 09:58:51 -04:00
Kurt Himebauch 728bfa2043 Merge pull request #124 from Droid-Deck/ci/auto-publish-maintainer-prs 2026-10-01 09:19:44 -04:00
xXJsonDeruloXx c4de482b6a ci: publish signed test builds automatically for PRs from this repo's branches 2026-10-01 09:15:03 -04:00
Kurt Himebauch 55697535a1 Merge pull request #123 from Droid-Deck/fix/deck-pad-touch 2026-10-01 09:02:50 -04:00
xXJsonDeruloXx 65733c23a1 fix: keep touch scrolling the Steam interface when the client finds no controller at start 2026-10-01 08:18:24 -04:00
Kurt Himebauch fd51b86060 Merge pull request #122 from Droid-Deck/feat/readable-session-logs 2026-10-01 07:48:03 -04:00
xXJsonDeruloXx d6e4ab1824 feat: name session log folders by day, number and mode, keep the newest 30, and add Clear logs 2026-10-01 07:43:21 -04:00
Kurt Himebauch 566c7d703a Merge pull request #102 from Droid-Deck/fix/x360-raw-pad-axes 2026-10-01 07:24:19 -04:00
Kurt Himebauch d7eb3256a5 Merge branch 'main' into fix/x360-raw-pad-axes 2026-10-01 07:19:45 -04:00
xXJsonDeruloXx c96f2d1070 chore: merge main into fix/adopted-storage-runtime-dir 2026-10-01 06:57:26 -04:00
Kurt Himebauch cc8b743ecb Merge pull request #120 from Droid-Deck/ci/publish-test-waits-for-build
ci: wait for the PR build before publishing a test build instead of failing
2026-10-01 06:57:01 -04:00
xXJsonDeruloXx 45422b739b ci: wait for the PR build before publishing a test build instead of failing 2026-10-01 06:54:05 -04:00
xXJsonDeruloXx f58bf9b23c fix: give the guest a short XDG_RUNTIME_DIR so gamescope starts with the app on adopted storage 2026-10-01 06:49:03 -04:00
Kurt Himebauch f0e8ef1274 Merge branch 'main' into fix/x360-raw-pad-axes 2026-10-01 06:21:24 -04:00
Kurt Himebauch bb6d6fee97 Merge pull request #93 from Droid-Deck/fix/a840v2-linux-turnip 2026-09-30 23:59:27 -04:00
Kurt Himebauch d59734e375 Merge branch 'main' into feat/performance-fixes-2 2026-09-30 23:56:24 -04:00
Kurt Himebauch 14db12290e Merge branch 'main' into fix/a840v2-linux-turnip 2026-09-30 23:55:50 -04:00
Kurt Himebauch 1e476539df Merge pull request #117 from Droid-Deck/fix/qam-battery-eta 2026-09-30 23:55:10 -04:00
Kurt Himebauch 3c8e370744 Merge branch 'main' into fix/qam-battery-eta 2026-09-30 23:50:02 -04:00
Kurt Himebauch 3fcd01f536 Merge branch 'main' into fix/x360-raw-pad-axes 2026-09-30 23:43:27 -04:00
Kurt Himebauch 0e2ee35b49 Merge pull request #116 from Droid-Deck/fix/pr-publish-post-release
fix: finish PR test publish after signing
2026-09-30 23:29:37 -04:00
xXJsonDeruloXx 27920711f0 fix: show QAM battery life and charge time estimates 2026-09-30 23:27:27 -04:00
xXJsonDeruloXx d4c16b5b59 fix: finish PR test publish after signing 2026-09-30 23:25:47 -04:00
Kurt Himebauch e392364e30 Merge pull request #115 from Droid-Deck/feat/update-catalog
feat: publish and consume update catalog
2026-09-30 23:17:38 -04:00
xXJsonDeruloXx 3cdb77fd65 fix: allow catalog to inspect active tests 2026-09-30 23:06:09 -04:00
xXJsonDeruloXx a91be68992 fix: reflect republished PR test freshness 2026-09-30 23:04:34 -04:00
xXJsonDeruloXx e16f351540 fix: bypass stale update catalog caches 2026-09-30 22:57:43 -04:00
xXJsonDeruloXx efdab60b22 docs: call main builds Preview 2026-09-30 22:54:32 -04:00
xXJsonDeruloXx 8dc24f244c feat: publish and consume update catalog 2026-09-30 22:46:44 -04:00
Kurt Himebauch b565c4a6e5 ci: streamline signed PR test builds (#114) 2026-09-30 22:18:08 -04:00
Kurt Himebauch a999fbdb38 Merge branch 'main' into fix/a840v2-linux-turnip 2026-09-30 21:47:17 -04:00
Kurt Himebauch 52eee568f5 Merge branch 'main' into fix/x360-raw-pad-axes 2026-09-30 21:46:58 -04:00
Kurt Himebauch 05cebd4a4f fix: offer updates to release-signed local builds and align the Updates page (#112) 2026-09-30 21:46:45 -04:00
maxjivi05 8cfd54a739 Add proot fast path for in-process path lookups
Preload library answers stat/open/access/readlink in-process via a
trampoline page proot 0014 lets through seccomp. On by default with a
Performance toggle; also sets GLIBC_TUNABLES rseq=0 and malloc top_pad.
2026-09-30 21:05:10 -04:00
xXJsonDeruloXx 8565acf12b Merge main into PR #93 2026-09-30 19:41:59 -04:00
MaxsTechReview 941a6c3ef9 fix: carry the installed Steam build into imported games so Steam sto… (#108)
* fix: carry the installed Steam build into imported games so Steam stops re-verifying them

A second-library game downloaded by WinNative was registered with a bare
manifest (no buildid or InstalledDepots), so Steam treated it as build 0 and
verified all ~92 GB of Final Fantasy XV through proot. Reuse WinNative's
.winnative-steam manifest, or a completed .DepotDownloader/depot.config, so
Steam only fetches what changed since the recorded build.

* fix: give queued Steam fresh installs of imported games the recorded build

Steam rewrites a bare import manifest into a fresh install at build 0
before downloading anything. While nothing is downloaded or staged, the
WinNative build is still safe to carry over, so Steam skips the full
re-download (FF7 Remake was queued for 98 GB this way).

* fix: mount the game storage at /mnt/droiddeck-sd

The second library was bound at /mnt/bannerlator-sd, a name left from the
Bannerlator port, which is what Steam shows in its paths and errors. Bind
it at /mnt/droiddeck-sd, keep the old path as an alias for links and
prefixes that still name it, move Steam's library entry in place, re-point
import links, read ownership snapshots taken under the old path, and key
added-game shortcut ids by the old path so their prefixes stay put.

* fix: restart Steam once when owned added games are confirmed

On a fresh install the importer runs before any ownership snapshot exists,
so every added game became a non-Steam shortcut and stayed one until the
client happened to restart. Once the watcher confirms owned titles that are
not imported, it asks the session for one restart, while no game runs and
no download is in progress, and the importer turns them into Steam entries.
2026-09-30 19:34:15 -04:00
Kurt Himebauch fe84c050ad Preserve added game launch options (#111) 2026-09-30 19:08:57 -04:00
Kurt HimebauchandMaxsTechReview f13c885fec feat: pick GPU drivers as matched pairs, set automatically for the device's GPU (#103)
* feat: pick GPU drivers as matched pairs, set automatically for the device's GPU

* DD-Turnip Driver Support (#106)

---------

Co-authored-by: MaxsTechReview <[email protected]>
2026-09-30 16:57:49 -04:00
MaxsTechReview 8ee1cbe3eb Additional performance fixes (#87)
* perf: answer Turnip's zero-timeout KGSL timestamp polls without blocking

Turnip polls a timeline point with IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID
and a timeout of 0, which KGSL reads as wait forever; Mesa does it under
a lock on every signalling submit, so vkd3d-proton, DXVK, gamescope and
Zink ran in lock-step with the GPU. The session preload now answers
those polls from the context's retired timestamp and passes everything
else to the kernel. Bannerlator measured a D3D12 demo at 614 -> 4335 fps
with the same answer in its Vulkan adapter. BL_NO_KGSL_POLL_FIX=1 turns
it off.

* perf: frame limiter, live panel refresh, opt-in GPU clock pin, cheaper cursor sync

- A per-mode frame limit (Steam and desktop settings, off by default):
  gamescope's -r, the compositor's buffer-release pacer, the display
  layer's frame-rate vote and a panel mode the cap divides evenly, as
  WinNative's per-shortcut limit does. BL_FPS was always 0 and the
  pacer's limit was never set.
- The compositor follows the panel's refresh rate when it changes
  mid-session instead of reporting the rate read at start. (WinNative.)
- Performance > GPU: hold the Adreno clock at its top through KGSL's
  power-control property, no root (Bannerlator's PerfGpuTurbo). Off by
  default; cleared when the session stops and at the next app start if
  a killed process left it set.
- The cursor image is copied across JNI only when its serial changes,
  not on every pointer move.

* perf: stop re-seeding every prefix every 5 s, and run the library helpers only on change

- bannerlator-seed-redists stamped its change check from a block that
  carried the current time, so it never matched: every prefix had ~32
  keys appended to system.reg on every beat. The stamp now comes from
  the names; Proton's prefix template is seeded too (date kept), with
  WinNative's built-in list until the client has the install scripts.
- The library beat runs the seeder every 15 s and steam-compat only
  when a directory it reads has changed, or once a minute.
- The client-core pin reads /proc itself and runs taskset -a only for
  new processes or ones that grew threads.
- The games watcher backs off after a failed ownership probe and does
  not probe while a game runs.
- The Proton launcher runs the newest ARM64 depot by build number, and
  PROTON_USE_PIPEWIRE defaults to 0. (WinNative.)

* perf: Mesa's database shader cache, and proot's tracer above the guest

- MESA_DISK_CACHE_DATABASE=1 for the session: the file cache opens a
  file per lookup and opens and renames one per store, each a proot
  stop; the database is opened once and used with pread/pwrite.
- proot's single tracer thread is raised to nice -6 (under the
  compositor's -8) two seconds after it starts, so the first guest
  process it forks keeps nice 0. setpriority() directly, not
  Process.setThreadPriority, which would also move its cgroup.
2026-09-30 16:43:52 -04:00
MaxsTechReview 6c5d6cda73 Added LSFG/Import/License Checking (#96)
* feat(lsfg): take Lossless Scaling from Steam by itself, import it once owned, rebuild on update

- Lossless.kt owns the DLL and its shader cache. The copy the Steam
  client installs (runtime library or the card's) is taken by itself
  and taken again when the client updates it (size/mtime stamp). The
  DLL is copied in and built into a cache named after its SHA-256, so
  a new DLL hands the compositor a new path; a build that fails keeps
  the working cache. Builds are serialized.
- The Steam session asks the signed-in client once per session whether
  the account owns app 993090 until the answer is yes, then records it
  and the app stops asking (BL_LOSSLESS_OWNED). Once owned, the frame
  generation menu offers "Import Lossless.dll…", on the main screen and
  in the session drawer; an import stays until Steam's copy changes.
- The compositor restarts the LSFG engine when its cache path changes,
  and a new engine selection clears an earlier start failure, so an
  updated DLL or a retry no longer needs the app restarted.
- The HUD says when LSFG is waiting on Lossless Scaling instead of
  "starting"; frame generation strings are resources.
- Removed LsfgNative's unused DLL variant and cache-match calls.

* Find Lossless.dll in added Games libraries too

* LSFG: 120, 90 and 60 fps targets alongside 2x-4x

The pacer's target-rate mode picks 0-3 generations per game frame. While it
is warm the input ring and shared chain follow every frame, so frames that
generate nothing don't leave the next one without history.

* Credit LSFG: Camille LaVey / Eden, ported to WinNative and DroidDeck by @maxjivi05

* LSFG: aim 0.1 fps under the target so output never passes the refresh rate
2026-09-30 16:40:46 -04:00
Kurt Himebauch 2725ee4512 Merge branch 'main' into fix/a840v2-linux-turnip 2026-09-30 16:28:51 -04:00
Kurt Himebauch 7cc1c93775 Merge branch 'main' into fix/x360-raw-pad-axes 2026-09-30 16:27:45 -04:00
Kurt Himebauch 38c9b733b5 Feat/step out stop confirm (#105) 2026-09-30 16:26:39 -04:00
Kurt Himebauch 27e5f86793 docs: add a join-the-Discord badge and community section to the README (#104) 2026-09-30 14:54:35 -04:00
xXJsonDeruloXx a6331e44ae fix: give games the Xbox 360 axis layout when Steam Input is off 2026-09-30 13:36:14 -04:00
Kurt Himebauch f53b844add feat: turn Deck mode on for new installs and add a performance overlay toggle (#101)
* feat: turn Deck mode on for new installs and add a performance overlay toggle

* feat: hide the session fps HUD where Deck mode's performance overlay runs
2026-09-30 13:06:12 -04:00
Magarto fb34098006 Merge branch 'main' into stretch-to-panel 2026-09-30 18:52:06 +02:00
Kurt Himebauch c729f4c8e7 fix: keep Deck mode on the Steam Deck client branch so Steam stops offering an update (#100) 2026-09-30 12:47:57 -04:00
Kurt Himebauch 9f343278c3 fix: stop update channel cards flashing as a controller moves between them (#99) 2026-09-30 11:45:44 -04:00
Kurt Himebauch 6beeb29486 feat: add an Updates page that installs Stable, Nightly or test builds in-app (#98)
* feat: add an Updates page that installs Stable, Nightly or test builds in-app

* style: put update channels on the left and the build on one line

* style: make the update card read like a system updater

* feat: expand update notes, show the build beside the title, fit with button hints

* style: check for updates with the refresh icon Components uses
2026-09-30 11:25:18 -04:00
Kurt Himebauch ade2e982b6 ci: sign PR test builds in every variant and take stable from the org repo (#97) 2026-09-30 10:53:26 -04:00
Kurt Himebauch 8a61a1898b Merge pull request #94 from Droid-Deck/fix/remove-pr-approval-gates
ci: remove PR approval gates from signed releases
2026-09-30 09:19:58 -04:00
xXJsonDeruloXx dfa3b12f48 ci: remove PR approval gates from signed releases 2026-09-30 09:14:30 -04:00
xXJsonDeruloXx 93382ff241 fix: report Adreno 840v2 as the A840 the runtime's Turnip knows 2026-09-30 07:53:12 -04:00
Magarto 24b74d8346 Merge branch 'main' into stretch-to-panel 2026-09-30 09:30:37 +02:00
Kurt HimebauchandMaxsTechReview 9ed25d8ae5 feat: Flatpak apps and a Flathub store (#77)
* feat: run Flatpak apps through a proot bwrap stand-in and add a Flathub store

* feat: give Flatpak apps the KGSL GPU, the controllers and a CPU LLVM can target

* feat: Flatpak games on the desktop, Store apps on the Desktop page, safer remove

* test: cover the sandbox planner and store helpers; docs: explain Flatpak on DroidDeck

* fix: keep controller focus through Store tabs and app pages; show progress per Flatpak step

* fix: let Chromium, Electron and Firefox Flatpaks start without their own sandboxes

* fix: fetch Flatpak's supporting libraries when the runtime lacks them

* fix: add Flathub without a download, log store commands, keep Electron apps on X11 under gamescope

* feat: import ARM64 AppImages from storage and run them from the Desktop page

* fix: report failed Flatpak installs as failures and deploy runtimes that ship a resolv.conf link

* fix: let Flatpak create its repo where Android denies hard links

* fix: point store commands at the current network's DNS

* fix: keep failed Flatpak extensions non-fatal, give Electron AppImages a bus, X11 and time to relaunch

* docs: AppImage import and what x86 apps would take

* feat: put the Flathub Store and AppImage import behind beta toggles in Setup

---------

Co-authored-by: MaxsTechReview <[email protected]>
2026-09-30 00:35:49 -04:00
The412Banner b4b31d5a08 ci: say what each DroidDeck-CI build is at the top of its release notes (#92) 2026-09-30 00:11:55 -04:00
The412Banner f394906726 Merge pull request #91 from Droid-Deck/feat/game-saves
Game saves: import and export save zips for added games
2026-09-29 23:49:50 -04:00
The412Banner b50dfa76d1 Game saves: show Manage saves only on added games 2026-09-29 23:36:49 -04:00
The412Banner 3f81ddff9b Game saves: Manage saves on each game's page instead of a separate page
The floppy disk beside the Steam cog and its Game saves page are gone. A game's
page now has Manage saves beside Proton prefix, a menu with Import saves (a
GameHub or Winlator zip, picked in the app's file picker; the saves there now are
backed up first), Export as GameHub zip, Export as Winlator zip (a folder picked
the same way) and Open save folder. The game's saves are looked for when the menu
opens. The Proton list and config.vdf reader only that page needed are removed.
2026-09-29 23:21:40 -04:00
The412Banner d1dec8d7a4 Game saves: import and export save zips per game
A floppy disk beside the Steam page's cog opens a Game saves page, laid out like
Components: the Proton at the top (from the client's compatibility tools, with
bannerlator-proton-arm64 shown as Proton Experimental), the zip type beside it
(GameHub or Winlator, LB / RB turn it), and the games the client runs with that
Proton (CompatToolMapping, its default for the rest), each with Import and Export.

Import picks a zip in the app's file picker, backs up the game's steamuser profile
to Download/DroidDeck/Saves/backups, then unzips drive_c with any user folder but
Public renamed to steamuser, so GameHub and Winlator zips both land. Export finds
the save folders by name under the usual roots (Bannerlator's SaveLocator), or
takes the whole profile, and writes <Game>_<date>.zip to a folder picked the same
way, under steamuser (GameHub) or xuser (Winlator). Imports wait while a session
runs. Ported from Bannerlator's GameSaveBackup and SaveLocator.
2026-09-29 23:01:21 -04:00
The412Banner e60097e5cb Merge pull request #89 from Droid-Deck/fix/resume-restore-minimized
Restore a game that minimized itself behind the Steam menu
2026-09-29 22:14:53 -04:00
The412Banner 7c7bf2dcbe Merge branch 'main' into fix/resume-restore-minimized 2026-09-29 22:10:04 -04:00
Kurt Himebauch 75593cfd29 feat: the pad as SteamOS has it - Steam Input's virtual pads for games, a Steam Deck controller for the client (#86)
* feat: stand in for /dev/uinput so Steam Input's virtual pads reach games

* feat: present the pad to the Steam client as a Steam Deck controller over hidraw

* docs: log the uinput stand-in and the Steam Deck controller

* feat: feed the handheld's gyro and accelerometer to the Steam Deck controller

* feat: let the Steam page choose between the Steam Deck controller and the Xbox 360 pad

* feat: Deck back grips and trackpads on the second screen

* fix: serialize ring teardown with its writers, fall back to the Xbox pad when the Deck cannot be set up, and read motion only for a Deck

* fix: keep the second screen's choice through sleep and bring it back on wake
2026-09-29 21:57:52 -04:00
The412Banner 00d0f8cd2b gamescope-3.16.29-p4: restore a game minimized behind the Steam menu (patch 0112) 2026-09-29 21:56:42 -04:00
The412Banner faf4b4e8b5 Restore a game that minimized itself behind the Steam menu
The Steam menu is an overlay that takes input without changing gamescope's focus
window. A fullscreen wine game minimizes itself when it loses input, and gamescope
only takes a window out of iconic when the focus window changes, so after Resume
the game stayed minimized: a black screen with its caption in the top-left corner
(Titanfall 2 on GE-Proton 11, proven on device).

gamescope patch 0112 remembers the iconify request and puts the window back to
NormalState before input returns to it, then hands focus over again so wine
restores and activates it. The session script also asks gamescope to restore the
game once input comes back from the client (GAMESCOPE_RESTORE_FOCUS_WINDOW);
BL_RESTORE_ON_RESUME=0 turns that off.
2026-09-29 21:52:34 -04:00
Kurt Himebauch 5a61d37c5d perf: faster local build, skip xalia and gamescope realtime by default, mic off by default, show the saved Deck mode (#85)
* perf: cache pinned downloads, skip unchanged proot rebuilds, and wait out a Docker Desktop restart in the local build

* perf: skip xalia and gamescope realtime queues by default, add a realtime toggle, default the mic off

* fix: show the saved Steam Deck mode on the Steam page before Performance has been opened
2026-09-29 20:00:10 -04:00
Kurt Himebauch 25079ab819 Merge pull request #84 from maxjivi05/fix/performance-misc 2026-09-29 19:41:38 -04:00
maxjivi05 736b7bed5e perf(gamescope): ask X for the pointer only while a cursor is drawn
The gamepad-cursor port queried the X pointer on every vblank, a
blocking round trip to Xwayland on the paint thread. With no cursor
image the position comes from wlserver as upstream; while the cursor is
hidden for inactivity X is asked every 50 ms. The realtime-queue comment
now says it is a no-op on KGSL Turnip.
2026-09-29 18:44:32 -04:00
maxjivi05 a3f4c911e5 perf: poll without starting processes, and stop rewriting unchanged Steam files
The session's watchers slept with sleep(1) and read files with cat, an
exec each under proot; they now wait in bash itself. steam-compat
writes its tool, the adopted wrappers and config.vdf only when they
change, the games watcher probes ownership when its matches change or
every five minutes instead of every 30 s, and Proton no longer inherits
the client's steamrtarm64 library path.
2026-09-29 18:44:32 -04:00
maxjivi05 caa7682c6f perf: default to Turnip sysmem, compact Zink descriptors and the Performance + TSO FEX preset
WinNative's defaults for every Linux session. Choices already saved by
a user are kept.
2026-09-29 18:44:32 -04:00
maxjivi05 e217cbe66d perf: pace buffer releases to the screen, read the requested refresh, drop sustained mode and ADPF
A replaced buffer went straight back to a game with no frame limit, so a
MAILBOX/IMMEDIATE swapchain drew frames that were never shown; it now
comes back once per refresh as in WinNative. gamescope's -r and the
compositor read the rate of the mode the activity asked for rather than
the one the panel was still in, and the zero-copy layer votes it.
Sustained performance mode, which caps clocks on most power HALs, and
the ADPF session, which reported frame intervals as work, are gone.
2026-09-29 18:44:32 -04:00
maxjivi05 4914b2cc69 perf: stop proot's fake_id0 and ioctl traps from stopping every stat, sendmsg and GPU ioctl
fake_id0 (loaded by -i for Xwayland's set*id) now traces only the set*id
family and the xattr fixups when the ids given are proot's own, and the
seccomp filter traces ioctl only for the termios and FICLONE requests
proot rewrites. The faccessat2 and statx exit stops are dropped on
kernels that have the calls.
2026-09-29 18:44:32 -04:00
Magarto 051b9a320e Merge branch 'main' into stretch-to-panel 2026-09-29 22:00:43 +02:00
Kurt Himebauch 6ecd6fa1d0 Merge pull request #81 from Droid-Deck/fix/proot-kompat-utsname-only
perf: stop proot's kompat from trapping hot syscalls, and stop respawning a crashing mangoapp
2026-09-29 15:38:52 -04:00
xXJsonDeruloXx 0f4e83eefd fix: stop gamescope respawning a mangoapp that keeps dying at start 2026-09-29 15:08:28 -04:00
xXJsonDeruloXx d62525f5af perf: stop proot's kompat from trapping futex/epoll/fcntl and stripping the vDSO when only the hostname changes 2026-09-29 14:39:53 -04:00
Magarto 71057fefe1 Merge branch 'main' into stretch-to-panel 2026-09-29 17:49:28 +02:00
Kurt Himebauch f4d2c37fbd Merge pull request #78 from Droid-Deck/fix/ci-publish-retention
Fix DroidDeck-CI main build publishing
2026-09-29 10:48:38 -04:00
xXJsonDeruloXx ffd9f92d7b ci: keep main DroidDeck-CI releases published 2026-09-29 10:46:50 -04:00
Kurt Himebauch 213d110766 Merge pull request #55 from Droid-Deck/fix/steamdeck-performance-overlay
Deck mode: working Steam performance overlay (MangoHud)
2026-09-29 09:41:51 -04:00
xXJsonDeruloXx 1a8c91b49b Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	app/src/main/java/com/droiddeck/launcher/MainActivity.kt
2026-09-29 09:41:11 -04:00
Kurt Himebauch 9644a4cc7f fix: use Android's 300 ms double-tap window for Back so the single-press action comes sooner (#73) 2026-09-29 02:10:51 -04:00
Kurt Himebauch 24c250fdb4 fix: send wl_keyboard modifiers so Shift and Caps Lock produce capitals (#71) 2026-09-29 02:10:01 -04:00
Kurt Himebauch d4636afa97 fix: give the QAM chord a Guide lead that stretches with slow frames so A is not taken as a press (#70)
* fix: give the QAM chord a Guide lead that stretches with slow frames so A is not taken as a press

* fix: start the QAM Guide lead at 80 ms and stretch it only when frames are slow

* fix: set the QAM Guide lead minimum to 80 ms

* fix: use Android's 300 ms double-tap window for Back so the single-press action comes sooner

* fix: keep the 500 ms Back double-press window
2026-09-29 02:01:21 -04:00
Kurt Himebauch 32930e704e feat: flood out of Stop and back into the launch button on quit (#72)
* feat: flood out of Stop and back into the launch button on quit

* refactor: drop the stop flood's warble to match the launch
2026-09-29 01:53:13 -04:00
Kurt Himebauch a8b18947a1 perf: cache the Steam client's repeated path lookups to cut startup from 31s to 6s (#69) 2026-09-28 23:18:48 -04:00
Kurt Himebauch 7c422577b6 fix: let the Steam client shut down cleanly when a session stops (#68) 2026-09-28 22:12:19 -04:00
Kurt Himebauch 1cc1ee994d feat: pair wireless debugging from a notification for the child-process gate (#67)
* feat: pair wireless debugging from a notification for the child-process gate

* fix: pair over loopback, handle Android 12 and 14 child-process routes, use D logo for notifications

* fix: read the adb tls port directly before falling back to mdns

* fix: end pairing on the same notification so progress never lingers
2026-09-28 21:37:10 -04:00
MaxsTechReviewandMaxsTechReview d8d375cb9a Steam Owned Games Importing (#62)
* Add ownership-aware Steam game folder imports

* Fix Steam title lookup and installed game manifests

---------

Co-authored-by: MaxsTechReview <[email protected]>
2026-09-28 18:13:08 -04:00
Magarto 49934ce933 Merge branch 'main' into stretch-to-panel 2026-09-28 23:13:01 +02:00
Kurt Himebauch 221644e8be Merge pull request #61 from magarto/guest-hostname-setting 2026-09-28 17:07:59 -04:00
magartus 5179b54745 Add an opt-in 16:9 stretch for screens narrower than 16:9
On a foldable's inner panel (Galaxy Z Fold8: 2448x1848, ~4:3) gamescope
is started with the nested size equal to the output, so games are asked
for the panel's own shape. Many cannot do it and letterbox themselves,
and gamescope's scaler has nothing to stretch. "Stretch games to fill
the screen" only forces window size, so it does not help.

New Steam setting "Stretch 16:9 games to a narrower screen" (off by
default): bannerlator-session then gives games a 16:9 surface at the full
output height (-w) and adds -S stretch, so they render at a shape they
support and fill the panel with no bars and no crop. An output that is
already 16:9 or wider is left untouched. Passed as
BL_GAMESCOPE_STRETCH_16X9 and listed in device.txt.
2026-09-28 23:03:10 +02:00
magartus 448feb30bc Let the user choose the guest host name
0.2.0 hardcodes the proot utsname nodename to "DroidDeck". Steam sends
it as the machine name, so an install updated from an earlier build
(which reported Android's "localhost") shows up in Steam under a new
name, with the same machine-id and login. /etc/hostname cannot fix it
from inside: proot's -k override wins.

Add a "Host name" field (Performance > Session identity), stored in
SessionPrefs and passed to guestUtsname(). Only a valid RFC 1123 label
is saved; blank or invalid falls back to the old default, "DroidDeck",
so nothing changes for anyone who leaves it alone. The value is also
listed in device.txt.
2026-09-28 22:58:54 +02:00
Kurt Himebauch 9d891d4db1 Merge branch 'main' into fix/steamdeck-performance-overlay 2026-09-28 16:36:33 -04:00
MaxsTechReviewandMaxsTechReview 240f1194c0 Fix/variables (#60)
* Add editable per-game environment settings below FEX presets

* Add typed preset variable pickers and default D3D12 capability levels

---------

Co-authored-by: MaxsTechReview <[email protected]>
2026-09-28 15:17:57 -04:00
Kurt Himebauch 7b74eebce2 Merge pull request #59 from Droid-Deck/ci/discord-build-announce
ci: publish each signed main build to DroidDeck-CI and post it to Discord
2026-09-28 14:54:29 -04:00
xXJSONDeruloXx 7592684b6f ci: publish to DroidDeck-CI with the org's CI app, not a personal token
DROIDDECK_CI_TOKEN never existed, so PR Release Publish skipped its
upload, and a personal token would expire and belong to one person. Both
workflows now mint a token of the org's DroidDeck CI Publisher app,
installed on DroidDeck-CI only with Contents: write (its ID in the
DROIDDECK_CI_APP_ID variable, its key in DROIDDECK_CI_APP_KEY). No
fallback: a main build without them fails its announce job.
2026-09-28 14:51:13 -04:00
xXJSONDeruloXx 7b78e1629b ci: publish each signed main build to DroidDeck-CI for the Discord post
Run artifacts need a GitHub sign-in to download. Each signed main build
is now also a prerelease main-<sha> of the public DroidDeck-CI (all
variants and their SHA-256s; the last 10 kept, PR releases untouched),
and the Discord post links its standard apk and the release. Without
DROIDDECK_CI_TOKEN the post links the run's artifacts as before.
2026-09-28 14:49:43 -04:00
MaxsTechReview 30741302fb Merge pull request #53 from maxjivi05/fix/adaptive-joysticks
Hide idle adaptive sticks and activate them at touch positions
2026-09-28 14:48:26 -04:00
MaxsTechReview 645f7d0bf1 Merge branch 'main' into fix/adaptive-joysticks 2026-09-28 14:48:14 -04:00
xXJSONDeruloXx 124f162ee8 ci: post each signed main build to Discord
A job after the signing of a main build posts to the Discord's builds
channel: the merged PR (or the commit), and a link to the run's signed
apks. The channel's webhook URL is the DISCORD_BUILDS_WEBHOOK secret;
without it the job posts nothing and passes.
2026-09-28 14:46:41 -04:00
Kurt Himebauch 3c577fd9dd Merge pull request #57 from Droid-Deck/fix/desktop-hardware-cursor
Desktop: hand labwc's cursor to the app, so full-screen games scan out
2026-09-28 14:40:02 -04:00
MaxsTechReview 659ff4817f Expand adaptive joystick activation area by fifty percent 2026-09-28 14:39:55 -04:00
Kurt Himebauch 626adb62af Merge branch 'main' into fix/desktop-hardware-cursor 2026-09-28 14:39:52 -04:00
Kurt Himebauch 71cb100a40 Merge pull request #58 from Droid-Deck/tools/local-release-signing
build_local: sign with DroidDeck's key when a local .signing.env has it
2026-09-28 14:39:22 -04:00
xXJSONDeruloXx 68002f3fa7 build_local: sign with DroidDeck's key when a local .signing.env has it
A local build was signed with the testkey alone, which a release refuses
as an update ("signatures do not match"): testing on a device with a CI
build meant uninstalling it and losing its data first.

With an untracked .signing.env (RELEASE_KEYSTORE, RELEASE_STORE_PASSWORD,
RELEASE_KEY_ALIAS) in the checkout, or in the main checkout for a
worktree, build_local.sh finishes with tools/release/sign-apk.sh as CI's
main builds do, and the apk installs over a release. Without the file
nothing changes. DROIDDECK_SIGNING_ENV points at the file explicitly.
2026-09-28 14:33:15 -04:00
xXJSONDeruloXx dae7592788 Desktop: hand labwc's cursor to the app, so full-screen games scan out
The desktop forced a software cursor (WLR_NO_HARDWARE_CURSORS=1) because
labwc's cursor never showed when handed to the app's compositor. With a
software cursor wlroots never scans out: every frame of a full-screen game
was composited by labwc first ("Direct scan-out disabled by software
cursor", 2284 times in one Adreno 825 report).

Why the cursor never showed: wlroots' Wayland backend attaches and commits
its cursor surface before wl_pointer.set_cursor gives it the cursor role,
and sets it again with nothing new committed each time the pointer enters.
The compositor released a role-less surface's buffer on commit, so the
cursor surface had no image by the time it became the cursor.

- A role-less surface's last buffer (cursor-sized only) is held until the
  next commit; set_cursor publishes it, and a repeat set_cursor of the
  surface already shown unhides its last image.
- A dma-buf cursor is read back after the client's implicit render fence,
  as the zero-copy path waits for it.
- The desktop no longer forces a software cursor.

Device-tested on the AYN Thor (Adreno 740, 1920x1080): labwc's own cursor
is drawn, with its shapes (I-beam over a terminal); wlroots logs "Direct
scan-out enabled" for a full-screen glxgears. At the same 60 fps on screen
the GPU is 3.0 % busy against 4.6 % with the software cursor, both at
550 MHz.
2026-09-28 14:33:08 -04:00
MaxsTechReview e10f5b4430 Limit adaptive stick activation near saved positions 2026-09-28 14:21:34 -04:00
MaxsTechReview 3e32b75a63 Merge branch 'main' into fix/adaptive-joysticks 2026-09-28 12:53:44 -04:00
Kurt Himebauch 435f7a1b03 Merge pull request #56 from Droid-Deck/ci/sign-all-variants
ci: sign every package variant on main
2026-09-28 12:46:40 -04:00
xXJSONDeruloXx 1fd8c50225 ci: sign every package variant on main
main's build signed only the standard package, so the pubg, antutu and
ludashi variants existed only after a manual Release APKs run. The build job
now reads the variant list from tools/release/variants.txt, as release.yml
does, and main's signing job signs all of them.
2026-09-28 12:42:56 -04:00
xXJSONDeruloXx e797bc5f01 mangoapp: stat sources for more Snapdragon generations
KGSL's files and thermal zone names differ between Snapdragon generations.
Each value now takes the first source that exists, in the order GameNative
and the Winlator forks read them: GPU load from gpu_busy_percentage or
devfreq/gpu_load, GPU clock from devfreq/cur_freq or gpuclk, GPU temperature
from KGSL's temp, devfreq/temp or a zone whose sensor name has "gpu". CPU
zones are any whose name has "cpu" and not "gpu" (cpuss-0, cpu0-silver-usr,
apc1-cpu0-usr, ...), matched without regard to case.
2026-09-28 12:28:45 -04:00
xXJSONDeruloXx 0cc3fe6bca mangoapp: GPU memory from KGSL
mangoapp's VRAM figure on a Qualcomm driver comes only from the gpu_mem_total
trace event. The tracefs stand-in's event list now declares that event as
Android kernels do, and reading the stand-in's events answers with one
record carrying the total KGSL reports in /sys/class/kgsl/kgsl/page_alloc,
which every Adreno under Android has and the app can read.
2026-09-28 12:28:45 -04:00
xXJSONDeruloXx ab8542e4e5 mangoapp: show Adreno GPU and CPU stats
Valve's mangoapp reads an Adreno GPU's load, clock and temperatures from the
paths of its own hardware, and the CPU temperature from thermal zones named as
mainline kernels name them. Every Adreno under Android keeps the GPU values in
KGSL's sysfs, readable by the app, and Android kernels name the CPU sensors
cpuss-N / cpu-N-M; the session binds each where mangoapp looks. Thermal zones
are found by sensor name, since their numbering differs per device, and
anything that cannot be found is left alone.
2026-09-28 12:19:01 -04:00
xXJSONDeruloXx c2bcf20172 mangoapp: run the performance overlay under Android
Three things kept Valve's mangoapp from drawing the overlay in a session:

- GLFW 3.4 takes XDG_SESSION_TYPE=wayland with WAYLAND_DISPLAY set as
  Wayland-only, and mangoapp needs gamescope's X server; the wrapper sets
  XDG_SESSION_TYPE=x11.
- On a Qualcomm display driver it opens a tracefs instance for the GPU memory
  total and aborts when that fails, which it always does for an Android app.
  The session preload now places the instance in an empty directory of its own
  when the real one cannot be made.
- gamescope sends it each frame's timings through a System V message queue,
  and Android kernels have no System V IPC. sysv.c now emulates the queue
  operations gamescope and mangoapp use, beside its semaphores and shared
  memory.
2026-09-28 12:19:01 -04:00
xXJSONDeruloXx 2341659cea Deck mode: enable Steam's performance overlay controls
The Quick Access menu's performance overlay toggle and level slider stayed
greyed out: the client exposes SteamClient.System.Perf only with -steamos3,
which SteamOS and Armada pass alongside -steamdeck. Deck mode now launches the
client with both. The session already puts package/beta back to the forced
branch before every start, which keeps -steamos3's own branch switch from
turning into an update loop.

The Deck mode switch moves from the Performance page to the Steam page's
Client settings, next to the client branch it selects by default.
2026-09-28 12:19:01 -04:00
xXJSONDeruloXx b2eaf380dc Blend translucent Wayland surfaces
The compositor composed every surface with a blit, which replaces what is under it. gamescope's
Wayland backend hands its overlay and notification planes to the host as full-size translucent
subsurfaces, so a Steam toast, the Quick Access menu or Deck mode's performance overlay blacked
out what was beneath it.

Surfaces whose buffer has alpha and that are not declared opaque now go through an alpha pass
(premultiplied source-over); everything else, and any image the driver cannot sample, keeps the
blit. wl_surface.set_opaque_region is honoured, and only for programs that declare opaque regions
at all, so Wine windows with a meaningless alpha channel are drawn as before. On the SurfaceControl
path a translucent plane is never promoted to the game layer, and the overlay layer is marked
translucent when its plane is.

From WinNative (maxjivi05, feature/wayland-gamescope 69218fc; GPL-3.0), with the SurfaceControl
layer handling added.
2026-09-28 12:18:50 -04:00
Kurt Himebauch 197fc3f163 Merge pull request #52 from maxjivi05/fix/splash-screen
Reduce Steam startup movie software decoding load
2026-09-28 11:35:25 -04:00
MaxsTechReview 9530e7b394 Hide idle adaptive sticks and activate them at touch positions 2026-09-28 10:54:24 -04:00
MaxsTechReview 0e0f133496 Reduce Steam startup movie software decoding load 2026-09-28 10:49:25 -04:00
Kurt Himebauch d98c211168 Merge pull request #50 from Droid-Deck/fix/release-blockers
Fix/release blockers
2026-09-28 10:32:44 -04:00
xXJsonDeruloXx 614b4d4de4 chore(release): set version 0.2.0 and remove code comments 2026-09-28 10:32:12 -04:00
Kurt Himebauch 43a19a831a docs(release): show every 0.2.0 section open (#48) 2026-09-28 10:24:24 -04:00
Kurt Himebauch 0d308af024 Merge branch 'main' into fix/release-blockers 2026-09-28 10:22:22 -04:00
The412Banner 128f73da8f Merge pull request #51 from Droid-Deck/fix/child-process-check
Child-process check: read the Developer options toggle too
2026-09-28 09:30:58 -04:00
The412Banner e22a55e949 Child-process check: read the Developer options toggle too
Developer options' "Disable child process restrictions" writes only the
feature-flag property persist.sys.fflag.override.settings_enable_monitor_phantom_procs,
never the global setting. Android (FeatureFlagUtils.isEnabled) uses the global
setting when it is set and that property otherwise, but the check read only the
global setting, so a device switched off in Developer options stayed flagged as
unset however often Check again was pressed.

The check now follows Android's order, and the Setup card no longer says "On"
for a setting that is unset or unreadable.
2026-09-28 09:24:19 -04:00
Kurt Himebauch 67cb005e6c fix(session): stop the session script's own loops when steam exits 2026-09-28 11:52:03 +00:00
Kurt Himebauch 73f76f6799 fix(home): open drawer apps in their own task 2026-09-28 11:52:03 +00:00
Kurt Himebauch 961a060461 fix(session): keep a retried session's first-frame and hud listeners 2026-09-28 11:52:03 +00:00
Kurt Himebauch dadbbf841a fix(session): cancel a session start that is still setting up 2026-09-28 11:52:02 +00:00
Kurt Himebauch 1ce8280371 fix(session): don't launch a cancelled session or end a newer one 2026-09-28 11:52:02 +00:00
Kurt Himebauch 78bdc0e3b5 chore(release): bump to 0.2.1 and check the version at release time 2026-09-28 11:52:02 +00:00
Kurt Himebauch ce5f2f5513 fix(runtime): keep the user's home when a runtime update fails mid-swap 2026-09-28 11:52:02 +00:00
Kurt Himebauch 0f2792c7ac docs: home app and dual-screen demos, trimmed 0.2.0 visuals, README hero (#47) 2026-09-27 22:48:33 -04:00
Kurt Himebauch 7185b775d4 0.2.0 release notes with screenshots, plus launch transition and UI fixes (#46)
* docs(release): draft 0.2.0 beta notes

* docs(release): credit prs and authors in 0.2.0 notes

* docs(release): add #39 and #45 to 0.2.0 notes

* chore: update readme

* fix(ui): cover the whole screen with the capsule wall

The wall was shifted right and then tilted around its own centre, which
left the bottom-left of the home and loading screens bare. Centre it and
size it to the tilted page's bounding box so every corner stays covered.

* feat(ui): flood from the play button into the throbber

Starting a session from a blue button no longer slides the session up
over the front end. The button stretches out on staggered springs until
the page is the ball's blue, the session opens on that same blue with no
window animation, and the blue gathers into the throbber's ball before
the throbber starts. Re-joining a running session, tiles and reduced
motion keep the old rise.

* docs(release): cover everything since 0.1 in the 0.2.0 notes

0.2.0 is the first release from the org, so list what DroidDeck does
as a whole, grouped by area: org pull requests as #N, pull requests
from before the move as The412Banner/DroidDeck#N, and the early work
that predates pull requests by author. CI and build work gets one
short section instead of a line each.

* docs(release): add screenshots and animations to the 0.2.0 notes

Lead with a game running and a few visual highlights, fold the full
list into collapsible sections by area, and shorten every line to the
feature and its credit. Pre-move pull requests read as legacy#N.

* fix(session): give the desktop its own loading hints

The loading screen's hints were written for Steam, so starting the
desktop or an emulator said the first run downloads Steam and that
leaving the app pauses Steam. Desktop and program sessions now rotate
through hints about the desktop instead.

* fix(build): skip incomplete NDKs when picking one locally

An interrupted sdkmanager install leaves a directory like
28.2.13676358.failed under ndk/, which sorted last and was picked
without a toolchain in it. Only consider directories that carry
source.properties.

* docs(release): show launching straight into Big Picture

* docs(release): credit the launch transition to #46
2026-09-27 22:14:23 -04:00
Kurt Himebauch cda6e89c12 fix(session): clear transient UI on pause (#45) 2026-09-27 20:37:58 -04:00
Kurt Himebauch 5f80898c27 Add game file and Proton prefix shortcuts (#39) 2026-09-27 20:37:43 -04:00
The412Banner 85560d6471 Merge pull request #43 from Droid-Deck/fix/session-stragglers
Session stop: kill guest processes proot lost, and cap the session log
2026-09-27 17:19:58 -04:00
The412Banner be6c875277 Session stop: kill guest processes proot lost, and cap the session log
Twice today a session's Xwayland aborted from one thread ("The futex
facility returned an unexpected error code"), proot lost the tracee, and
the process lived on reparented to init: proot's seccomp filter, with no
tracer left, answers ENOSYS, so Xwayland's main loop logged
"WaitForSomething(): poll: Function not implemented" until something killed
it - 9 GB after Dolphin, 13 GB after PPSSPP. --kill-on-exit and the tree
sweep only reach what proot still traces, and the straggler sweep ran only
at the next session's start.

Now a session stop also kills every process of ours whose executable lives
in the Linux runtime, and bannerlator-session keeps only the first
BL_LOG_CAP bytes (256 MB) of session.log, draining the rest so a writer
that filled it is not killed by SIGPIPE.
2026-09-27 16:52:51 -04:00
Kurt Himebauch 91beced154 Merge pull request #41 from Droid-Deck/fix/ui-ux-critique
Fix/UI ux critique
2026-09-27 16:41:18 -04:00
The412Banner ebc3da08b4 Merge pull request #42 from Droid-Deck/feat/cemu-wiiu
Cemu (Wii U) from the front end and the desktop, and two session fixes
2026-09-27 16:39:17 -04:00
xXJsonDeruloXx 41eb780495 fix(ui): remove duplicate focus imports 2026-09-27 16:34:34 -04:00
The412Banner 65fc2a38a3 Cemu (Wii U) from the front end and the desktop, and two session fixes
Cemu never showed a frame on DroidDeck: its Getting Started dialog blocked
every first launch (the folder picker cannot reach ROMs), the session's
controller helper aborted it, and the desktop's Vulkan renderer drew no
cursor. With the Turnip KGSL fix in the driver and the haptic-optional
Cemu in the catalog, this makes the app side complete:

- bannerlator-pad-defaults seeds Cemu's settings.xml once: the Wii U games
  folder, Vulkan, and Cubeb audio on the default device for both the TV
  and the GamePad speaker at full volume (Wipeout 3 plays through the
  GamePad; Cemu's own default is 20 %).
- The Cemu tile launches full screen (-f).
- libfakeinput.so is preloaded into every session process and exported its
  static C++ runtime (~7000 symbols); programs unwound through it and
  aborted (shadPS4 in pthread_exit, Cemu's controller thread). Link with
  --exclude-libs,ALL, and fail the build if the runtime leaks out again.
- On the Vulkan and GLES desktops the pointer worked but no cursor was
  drawn: wlroots hands the cursor to the app's compositor, which draws
  none. WLR_NO_HARDWARE_CURSORS=1 makes labwc composite it itself.

Device-tested on the FIT: Super Mario 3D World in-game at 35-37 fps with
sound, Deus Ex HR DC and Wipeout 3 launch with sound; RPCS3, Dolphin and a
Steam session (FlatOut) unchanged on the same driver.
2026-09-27 16:30:49 -04:00
Kurt Himebauch 359d1535ac fix(copy): american spelling, clearer keyboard and package labels 2026-09-27 14:23:19 -04:00
Kurt Himebauch 4c3a9f027c fix(focus): land controller focus by frame instead of fixed delays 2026-09-27 14:22:56 -04:00
Kurt Himebauch 5a6cc3c76e fix(motion): honor the system animation scale everywhere and on resume 2026-09-27 14:22:17 -04:00
Kurt Himebauch 89bd7deceb refactor(ui): share one frame generation menu and drop the unused dialog 2026-09-27 14:21:41 -04:00
Kurt Himebauch 169f2c3e34 fix(rail): keep sections in fixed places with an empty games state 2026-09-27 14:21:01 -04:00
Kurt Himebauch e6a797695a fix(setup): trim child-process fix to one line and three actions 2026-09-27 14:20:35 -04:00
Kurt Himebauch f1a2ecc857 fix(ui): raise buttons, chips and tabs to handheld touch sizes 2026-09-27 14:20:03 -04:00
Kurt Himebauch 36916f6bde fix(ui): show launch and tab hints where a pad does that 2026-09-27 14:19:32 -04:00
Kurt Himebauch 1b96d13a51 fix(ui): enable launches like play and say why they are disabled 2026-09-27 14:19:02 -04:00
Kurt Himebauch cb1d70bb14 fix(ui): flip booleans with a switch instead of on/off menus 2026-09-27 14:18:29 -04:00
Kurt Himebauch 37af590ca2 fix(ui): use one name each for child-process limit, proton and components 2026-09-27 14:17:49 -04:00
Kurt Himebauch 02f356f8c0 fix(session): keep ended screen with retry, share logs and back 2026-09-27 14:17:16 -04:00
Kurt Himebauch 5448177814 Merge pull request #40 from Droid-Deck/fix/review-hardening
Fix/review hardening
2026-09-27 13:57:20 -04:00
Kurt Himebauch ed6ce0ca41 Merge branch 'main' into fix/review-hardening 2026-09-27 12:27:22 -04:00
Kurt Himebauch 9b440686c5 Merge pull request #37 from maxjivi05/fix/performance-enhancements 2026-09-27 12:26:53 -04:00
xXJsonDeruloXx e7b7e9dc43 refactor(session): move the overlay setup out of SessionActivity.onCreate 2026-09-27 11:57:43 -04:00
xXJsonDeruloXx 1120040aee refactor(session): split runSession into its setup stages 2026-09-27 11:57:08 -04:00
xXJsonDeruloXx 3506a272da refactor(core): share one checksum helper across downloads 2026-09-27 11:56:15 -04:00
xXJsonDeruloXx b752748b7d refactor(compositor): move zwp_linux_dmabuf_v1 into wl_dmabuf.c 2026-09-27 11:54:48 -04:00
xXJsonDeruloXx 8990e7d7a4 refactor(launcher): resolve displayNameOf on the activity in DriverMenus 2026-09-27 11:48:05 -04:00
xXJsonDeruloXx c92a423ba1 refactor(launcher): move components, decky and proton state out of MainActivity 2026-09-27 11:43:35 -04:00
xXJsonDeruloXx 3b82b3e1f8 refactor(launcher): move driver menu state out of MainActivity 2026-09-27 11:41:21 -04:00
xXJsonDeruloXx e985ce1975 refactor(ui): move the session loading screen out of SessionOverlay 2026-09-27 11:39:57 -04:00
xXJsonDeruloXx 8496ce3e51 refactor(files): split FileManagerScreen helpers, items and dialog 2026-09-27 11:39:57 -04:00
xXJsonDeruloXx 47188f40ed refactor(ui): split FrontEndScreen into per-page files 2026-09-27 11:33:09 -04:00
Kurt Himebauch e186c9bc4c Merge branch 'main' into fix/performance-enhancements 2026-09-27 11:31:48 -04:00
xXJsonDeruloXx 7705f3c222 feat(runtime): install the runtime from a foreground service 2026-09-27 11:01:55 -04:00
xXJsonDeruloXx 5675b4897f build(deps): bump commons-compress to 1.27.1 2026-09-27 11:00:47 -04:00
xXJsonDeruloXx 1c87d92ef7 build(native): enable warnings for the compositor 2026-09-27 10:55:25 -04:00
xXJsonDeruloXx 031e64dfe1 fix(compositor): bound cursor buffer copies by stride and 64-bit size 2026-09-27 10:55:25 -04:00
xXJsonDeruloXx f590dd9763 fix(security): require a verified checksum for proton downloads 2026-09-27 10:54:31 -04:00
xXJsonDeruloXx af7dc40538 fix(security): require sha256 for every catalog and release download 2026-09-27 10:54:05 -04:00
xXJsonDeruloXx a28aae6e4a test: add unit tests for archive paths and log redaction 2026-09-27 10:52:01 -04:00
xXJsonDeruloXx c024470cc7 fix(runtime): refuse hard links that point outside the rootfs 2026-09-27 10:51:14 -04:00
xXJsonDeruloXx 16ae5ab84a fix(ui): keep the launcher alive when a pad connects 2026-09-27 10:49:54 -04:00
xXJsonDeruloXx 144947305a fix(runtime): single-flight the runtime install and reattach after rebuild 2026-09-27 10:49:54 -04:00
Kurt Himebauch 563bc6cad7 Merge pull request #38 from Droid-Deck/feat/steam-startup-toggle
Add optional Steam startup
2026-09-27 09:55:22 -04:00
xXJsonDeruloXx 59cde93821 Add optional Steam startup 2026-09-27 09:27:30 -04:00
maxjivi05 c104e1c276 proot: port WinNative's seccomp and tracee performance fixes
Brings the proot patch set up to WinNative 53836ca9 on top of the #772 port:

- trace prctl, setrlimit and prlimit64 in the seccomp filter only for the
  arguments proot acts on, and uname only where the core rewrites it
- answer a seccomp stop's filter flags from the registers fetched for its
  translation, reuse those registers, and skip the sysexit stop of wait4 left
  to the kernel and accept without a sockaddr; the flags come from proot's list
  merged with the enabled extensions, and a syscall an extension replaced
  keeps its exit stop
- take a clone3 child's flags and exit signal from struct clone_args
- name a thread's /proc/self by its thread group and resolve /proc/thread-self
- translate fchmodat2, send openat2 callers to openat, and bound sysnum lookups
  by the table length
- sweep relatives only for tracees that have children or ptracees, and empty
  the per-stop memory collector instead of reallocating it
2026-09-27 08:10:58 -04:00
Kurt Himebauch b5da0d0db6 Merge pull request #36 from Droid-Deck/fix/steam-startup-movie-overrides
Fix Steam startup movie override paths
2026-09-27 00:31:31 -04:00
xXJsonDeruloXx 5defc3e38b fix: use Steam startup movie override names 2026-09-27 00:16:37 -04:00
Kurt Himebauch 6a9f52f932 Merge pull request #35 from Droid-Deck/ui/launcher-refresh
feat(ui): launcher ui refresh
2026-09-26 23:19:09 -04:00
xXJsonDeruloXx 06a8d75769 feat(session): animate the logo on the loading screen 2026-09-26 22:44:09 -04:00
xXJsonDeruloXx ec12ddd0a8 ui: preserve rail focus on components 2026-09-26 22:16:38 -04:00
xXJsonDeruloXx 570902d5f3 ui: add fullscreen launcher and focus tabs 2026-09-26 22:10:17 -04:00
xXJsonDeruloXx 6120069975 ui: simplify suspended session overlay 2026-09-26 21:56:55 -04:00
xXJsonDeruloXx 3e337c9bda fix(ui): name the desktop cog's click 2026-09-26 21:37:40 -04:00
xXJsonDeruloXx 2869e76406 feat(ui): turn the steam tab into a capsule wall 2026-09-26 21:31:26 -04:00
xXJsonDeruloXx 61fdb80823 feat(ui): add a games tab with list and detail 2026-09-26 21:30:19 -04:00
Kurt Himebauch fd180a108a Merge branch 'main' into ui/launcher-refresh 2026-09-26 20:46:47 -04:00
Kurt Himebauch 4de74c740c Merge pull request #34 from Droid-Deck/fix/droiddeck-logo-white-on-black
Invert DroidDeck logo colors
2026-09-26 20:41:45 -04:00
xXJsonDeruloXx b7c32c92d2 Invert DroidDeck logo colors 2026-09-26 20:39:52 -04:00
xXJsonDeruloXx 8610ca0928 fix(ui): raise small text on the remaining pages 2026-09-26 16:43:46 -04:00
The412Banner a6b1ad37d0 Merge pull request #33 from Droid-Deck/fix/steam-readonly-files
First Steam session: Proton ARM64 ready at sign-in; Steam updates past read-only files
2026-09-26 16:43:35 -04:00
xXJsonDeruloXx 97b13f7488 feat(session): stack the loading screen on small screens 2026-09-26 16:42:58 -04:00
xXJsonDeruloXx 5d5b45a012 feat(session): explain paused sessions and offer stop 2026-09-26 16:42:28 -04:00
xXJsonDeruloXx 5ebafaabda feat(session): compact the session menu on short screens 2026-09-26 16:41:17 -04:00
xXJsonDeruloXx 56627a8aaf feat(session): redesign the session menu header and actions 2026-09-26 16:40:30 -04:00
xXJsonDeruloXx f59ced4e98 feat(ui): rework the components page layout 2026-09-26 16:38:53 -04:00
xXJsonDeruloXx 296e848718 feat(ui): split setup into tabs 2026-09-26 16:36:49 -04:00
xXJsonDeruloXx 8c63413019 feat(ui): redesign the desktop and emulator pages 2026-09-26 16:35:07 -04:00
xXJsonDeruloXx 2861c6d284 feat(ui): fit the game page to narrow screens 2026-09-26 16:33:53 -04:00
xXJsonDeruloXx 69c2d8e566 feat(ui): condense the steam page on small screens 2026-09-26 16:33:19 -04:00
xXJsonDeruloXx 414fddf7b8 feat(ui): scroll launcher pages as one and wrap actions 2026-09-26 16:32:04 -04:00
xXJsonDeruloXx b5ca6145a6 feat(ui): show an icon-only rail on narrow screens 2026-09-26 16:31:10 -04:00
xXJsonDeruloXx 5a5948b071 feat(ui): reflow settings pages in narrow panes 2026-09-26 16:30:21 -04:00
xXJsonDeruloXx e58b5088b3 feat(theme): add readable text colour for signal fills 2026-09-26 16:29:05 -04:00
The412Banner ecbce4c8d3 Steam updates: the client is not uid 0 in the session - drop that test
The session's proot runs with -i <app uid>, and the rootfs names that uid
root, so the client calls itself root while geteuid() returns the app's
uid. The read-only retry required geteuid() == 0 and never ran: the
Proton Experimental (ARM64) update still failed with Missing file
permissions on wineopenxr.dll (2026-09-26 16:07, 2,471 read-only files).

Also recognise the client by the path it was started by: the kernel's
/proc/self/exe names proot's loader for every process in the session.
2026-09-26 16:10:11 -04:00
The412Banner 96bf0a3bf3 First Steam session: Play sends no mode, and no mode is Steam - seed the Proton then too 2026-09-26 15:44:20 -04:00
The412Banner da7522a3ee First Steam session: place Valve's ARM64 Proton before the client first starts
The runtime setup now also fetches Proton Experimental (ARM64) and its
appmanifest from a catalog of their own (steam-seed.json) and unpacks them
where the client keeps them. At the first sign-in the compatibility tool is
already installed and the default, with no install dialog, no download and
no client restart; the client adopts the manifest and updates it like any
other app.

Done once: an install the client fetched itself, or one the user removed,
is left alone. A failed fetch stops nothing - the session asks the client
for it after sign-in, as before.
2026-09-26 15:36:23 -04:00
The412Banner 5605e777ca Merge remote-tracking branch 'org/main' into fix/steam-readonly-files 2026-09-26 15:33:02 -04:00
The412Banner a10e2e1fe8 Merge remote-tracking branch 'org/fix/first-login-proton-timing' into fix/steam-readonly-files 2026-09-26 15:33:01 -04:00
xXJsonDeruloXx 4fb623fa33 fix(ui): guard missing runtime version in setup 2026-09-26 15:19:40 -04:00
xXJsonDeruloXx 3b4956d088 feat(ui): add controller button hints 2026-09-26 15:19:00 -04:00
xXJsonDeruloXx 18deaafc34 feat(session): show staged progress on loading screen 2026-09-26 15:18:35 -04:00
xXJsonDeruloXx 0c4eede6d2 feat(ui): add system check to setup 2026-09-26 15:16:14 -04:00
xXJsonDeruloXx 54681f2647 feat(ui): add hero and launch settings to game page 2026-09-26 15:14:59 -04:00
xXJsonDeruloXx 5271edd972 feat(ui): add continue hero to steam page 2026-09-26 15:13:20 -04:00
xXJsonDeruloXx e4bb9856ef style(ui): drop random hue washes behind pages 2026-09-26 15:12:14 -04:00
xXJsonDeruloXx e462bbb24a feat(ui): replace nav drawer with permanent side rail 2026-09-26 15:11:47 -04:00
xXJsonDeruloXx d8f3090d9b refactor(ui): move build label into setup about 2026-09-26 15:09:34 -04:00
xXJsonDeruloXx d1af8b0114 feat(library): read hero art and last played from steam 2026-09-26 15:09:07 -04:00
xXJsonDeruloXx dae64bbe69 feat(ui): show game titles on library tiles 2026-09-26 15:08:30 -04:00
xXJsonDeruloXx 8b127bed84 fix(ui): raise small text to 12sp minimum 2026-09-26 15:07:42 -04:00
xXJsonDeruloXx 67187b1340 feat(theme): add graphite theme as default 2026-09-26 15:07:10 -04:00
Kurt Himebauch 02cac57438 Merge pull request #31 from Droid-Deck/docs/readme-banner
docs: add readme banner
2026-09-26 15:03:58 -04:00
The412Banner 277dcd174c Steam updates: rewrite read-only content files the way root could
The client runs as proot's root, which the kernel does not see, and
leaves many content files read-only; an update that rewrites one was
refused (EACCES) and cancelled with "Missing file permissions"
(Proton Experimental (ARM64), wineopenxr.dll). For the client's own
processes, a refused write-open of a file the app owns now gets the
owner's write bit - on the file, or on the directory a new file goes
into - and one more try. Games and Wine keep ordinary permissions.
2026-09-26 10:41:06 -04:00
xXJSONDeruloXx 3c23ae7310 docs: add readme banner 2026-09-26 09:48:11 -04:00
The412Banner 8cb13d40b5 First sign-in: ask for the ARM64 Proton after the client's post-logon compat phase, and retry
With Steam publicbeta 1790380355 the client spent 27 s in its post-logon
compat phase and dropped the install request the session sent 15 s after
sign-in, so a clean install never offered Proton Experimental (ARM64).
Wait for the console log's "Waiting for compat in post-logon took" (90 s
at most), then ask, up to three times a minute apart until the install
starts.
2026-09-26 08:21:14 -04:00
The412Banner c950d31960 Merge pull request #29 from Droid-Deck/feat/components-manager
Components manager: swap FEX / DXVK / VKD3D-Proton per Proton, enforced at launch
2026-09-26 06:16:04 -04:00
The412Banner ae570cd5b7 Components drawer: stacked rows so long names never squeeze the text, a focusable Running now card the pad can reach, and only the Proton a game actually runs on counts as running 2026-09-26 05:30:02 -04:00
The412Banner 0fd1dc4fce Proton wrappers: run the components check for non-Steam shortcuts too - decide by the game's own prefix rather than the app id, which is 0 for a shortcut 2026-09-26 05:15:36 -04:00
The412Banner f0854e8518 Merge org main into feat/components-manager 2026-09-26 04:41:10 -04:00
The412Banner ceffd3f759 Merge org main into feat/components-manager
Keeps both onResume additions: the phantom status refresh from main and the queued component swaps.
2026-09-26 04:36:02 -04:00
Kurt Himebauch dc1816612c Merge pull request #28 from Droid-Deck/fix/session-desktop-button-label
Keep the session Desktop button on one line
2026-09-26 00:53:17 -04:00
xXJsonDeruloXx 1d04a7e0e3 Keep session desktop button label on one line 2026-09-26 00:52:53 -04:00
Kurt Himebauch 1e523c4e93 Merge pull request #27 from Droid-Deck/fix/boot-video-pitch-black
Set boot movie background to pitch black
2026-09-26 00:36:22 -04:00
xXJsonDeruloXx d83f766cf3 Set boot movie background to pitch black 2026-09-26 00:35:54 -04:00
The412Banner bb5e1ae63d Proton wrappers: run the components check only for a real game launch (waitforexitandrun, non-zero app id)
With the check reached from the client's own runs right after login, the Steam client segfaulted
within seconds, four sessions out of four; the same build without the check (test/bisect-no-launch-check)
and the last build before it both reach Big Picture. Now every run that is not a game launch -
the client's probes, app id 0 - is left exactly as before. Each check logs one line.
2026-09-26 00:28:49 -04:00
Kurt Himebauch f27a4f6c95 Merge pull request #26 from Droid-Deck/ui/build-info-top-bar
Show build identity in the launcher top bar
2026-09-26 00:24:39 -04:00
The412Banner d7ff92106a Remove a stray __pycache__ from the overlay scripts and ignore it 2026-09-26 00:20:41 -04:00
xXJsonDeruloXx 34a17acaba Show build identity in launcher top bar 2026-09-26 00:03:56 -04:00
The412Banner 110aec0b6f Proton wrappers: drop the per-tool prefix for Steam's tool-0 runs - the client segfaulted right after its probe started (first in 25 sessions); keep the launch-time component check 2026-09-26 00:03:45 -04:00
The412Banner 06502bb49b Components: choices enforced at every game launch; Steam's tool-0 probes get their own prefix per Proton
Steam runs its graphics probe through every registered Proton at once after login, all in
compatdata/0. Prefix system32 links point into the Proton that set it up last, and the other
Protons' Wines, running outside Proton's pfx.lock, wrote their builtin DLLs through them into that
Proton's tree (device: GE's FEX DLLs became Proton Experimental's at session start).

- bannerlator-steam-compat: both wrappers (Valve's depot launcher and the third-party
  bannerlator-proton-wrap) give a tool-0 run STEAM_COMPAT_DATA_PATH=.../compatdata/0-<tool>, and
  before every other launch check the Proton's files against the app's desired.tsv and put back
  anything that differs (same Proton build only), logging to reapplied.log.
- ComponentsManager writes desired.tsv plus unpacked copies of the packages in use into the
  runtime after every change; the page and the drawer show 're-applied at launch'.
2026-09-25 23:54:10 -04:00
Kurt Himebauch c20a166134 Merge pull request #21 from Droid-Deck/fix/phantom-process-setup-gate
Gate Steam sessions on Android process limit
2026-09-25 23:51:11 -04:00
xXJsonDeruloXx 162fa8e2de Merge remote-tracking branch 'origin/main' into fix/phantom-process-setup-gate 2026-09-25 23:50:57 -04:00
xXJsonDeruloXx ae5614d212 Reuse saved wireless ADB pairing and connection 2026-09-25 23:48:47 -04:00
xXJsonDeruloXx b72d8a8bab Clarify Wireless debugging connection address after pairing 2026-09-25 23:39:31 -04:00
Kurt Himebauch d4cf890d8a feat: set DroidDeck startup movie by default (#25) 2026-09-25 23:36:29 -04:00
xXJsonDeruloXx 82e494269d Improve wireless ADB text input 2026-09-25 23:17:28 -04:00
The412Banner c982d65675 Session drawer: tab spacing fits the room between LB and RB, so all four tabs show 2026-09-25 22:59:50 -04:00
The412Banner f4e606fb15 Components: the pad's LB / RB step through FEX, DXVK and VKD3D-Proton (wrapping), with LB / RB badges beside the tabs 2026-09-25 22:49:10 -04:00
The412Banner 9dbdb5e771 Components: fixed columns so details and Download buttons line up; focused button is a solid blue fill with a white outline 2026-09-25 22:37:07 -04:00
The412Banner fa12478fdd Components: compact layout - one toolbar line (title, Proton drop-down, component tabs, in use, (i), refresh) and one list of short rows for installed and available packages 2026-09-25 22:27:25 -04:00
The412Banner c3b0d8ef03 Merge pull request #23 from Droid-Deck/docs/progress-log-0925
Progress log: 2026-09-25
2026-09-25 22:06:23 -04:00
The412Banner cbb9a168e6 Components: FEX, DXVK and VKD3D-Proton per Proton
A Components entry under Desktop opens a page built like the driver pages: a
Proton drop-down, FEX / DXVK / VKD3D-Proton tabs, what is in use, the installed
rows (each Proton build's original bundle and the stored packages; tap to
swap in, trash to delete), downloads from the Nightlies -Linux releases behind
the refresh button, and Import .wcp.

The session drawer gets a fourth tab, Components, for quick swaps between
what is installed. A swap into the Proton a running game uses waits until the
game closes and is applied then (drawer refresh, or the main screen on resume).

ComponentsManager keeps the same package format, originals layout and state as
the Decky Components Manager plugin and takes its data over once. A swap clears
only the component's own DLLs, so files a Proton keeps beside them stay.
2026-09-25 22:04:38 -04:00
The412Banner 73e2fc58a0 Merge pull request #24 from Droid-Deck/feat/drawer-tab-icons
Session drawer: QAM-style page tabs (Display, Controls, Settings)
2026-09-25 21:55:16 -04:00
The412Banner 500f4c69eb Session drawer: QAM-style page tabs - Display, Controls, Settings
The three page dots between LB and RB become icons standing in a line in page order, as in
Steam's Quick Access Menu. The selected one steps forward (full size, bright, a soft glow in the
accent), the others step back to half size and fade, and the line leans a little toward the
selection; tapping or picking a tab with the pad, and LB / RB, turn the page as before. The
first page's heading becomes Display to match its icon.
2026-09-25 18:11:04 -04:00
The412Banner 911ad29004 Progress log: 2026-09-25 2026-09-25 17:29:12 -04:00
xXJsonDeruloXx aa843b5a31 Merge remote-tracking branch 'origin/main' into fix/phantom-process-setup-gate 2026-09-25 17:23:08 -04:00
xXJsonDeruloXx f249acc50f Compact Steam process limit recovery for split screen 2026-09-25 17:21:43 -04:00
The412Banner 8cdedbe18a Merge pull request #22 from Droid-Deck/feat/steam-touch
Steam client: real touch in Big Picture, and a touchpad cursor that hovers and clicks
2026-09-25 17:06:19 -04:00
The412Banner 0f39d71f95 Merge remote-tracking branch 'org/main' into feat/steam-touch 2026-09-25 16:58:18 -04:00
The412Banner 1dc49b527b gamescope: stage 3.16.29-p3 (0111, pointer warps in Passthrough) 2026-09-25 16:38:50 -04:00
xXJsonDeruloXx 9422608960 Replace process gate dialogs with compact pages 2026-09-25 16:34:30 -04:00
The412Banner cc3fec0768 gamescope: the nested pointer moves Steam's pointer in Passthrough too
In touchpad mode the cursor moved but Big Picture highlighted nothing and a tap-to-click
selected nothing: the nested pointer's motion reaches wlserver as touch 0, and in Passthrough
(the touch mode Big Picture sets) a motion for a touch that is not down moves nothing, so the X
pointer stayed where it was. 0111 passes bAlwaysWarpCursor, as the other touch modes already warp.
2026-09-25 16:34:22 -04:00
Kurt Himebauch de37ddcfb5 Merge branch 'main' into fix/phantom-process-setup-gate 2026-09-25 16:28:53 -04:00
xXJsonDeruloXx 4c90196890 Replace process gate dialogs with pages 2026-09-25 16:28:15 -04:00
Kurt Himebauch 5270574e5a Add native Decky Loader support (#16)
* Add native Decky Loader management

* Enable Decky Steam frontend injection

* Disable unavailable stable Decky channel

* Decky: tie CEF debugging to session setting

* Decky: simplify Steam settings controls
2026-09-25 16:26:29 -04:00
xXJsonDeruloXx 30f6ca6ffc Replace wireless ADB modal with setup page 2026-09-25 16:19:01 -04:00
The412Banner a8af973726 gamescope: stage 3.16.29-p2 (0110, touch from the host) 2026-09-25 16:06:31 -04:00
The412Banner ed1f8dd2b6 gamescope: take the host's touch in the nested Wayland backend
3.16.29's Wayland backend bound only wl_pointer and wl_keyboard; touch from DroidDeck's
compositor was never read, so Steam could not be touched. 0110 binds wl_touch in the input
thread and feeds each finger to wlserver_touchdown/touchmotion/touchup, the Steam Deck
touchscreen's path, so Big Picture's Passthrough touch mode gets real touches.
2026-09-25 15:55:26 -04:00
The412Banner e8aaae1343 Touch: a lost finger-up no longer blocks every later touch
On the pointer path (a client that takes no wl_touch - gamescope), touch_down counted the
fingers still down before clearing a sequence whose up had been lost, so the stale point left
any_active set and every later tap or swipe was dropped: in Direct touch mode Big Picture
ignored the screen while the pad worked. The count is taken after the clean-up, and the app
ends whatever the compositor still holds when a new gesture's first finger lands (Android has
ended every earlier gesture by then). The compositor logs once per gesture whether the window
got touch or the pointer.
2026-09-25 15:55:26 -04:00
xXJsonDeruloXx ecff3240a6 Add reversible process limit controls 2026-09-25 14:52:04 -04:00
xXJsonDeruloXx ddd4a0c24d Add wireless ADB process-limit repair 2026-09-25 14:35:37 -04:00
xXJsonDeruloXx 019949847a Auto-dismiss process limit gate after status clears 2026-09-25 13:55:04 -04:00
xXJsonDeruloXx 4dcbf269d2 Gate Steam sessions on Android process limit 2026-09-25 13:49:58 -04:00
Kurt Himebauch 036317f371 Build local APK with CI payload parity (#20) 2026-09-25 13:20:51 -04:00
The412Banner ce0da01163 Merge pull request #19 from Droid-Deck/fix/log-privacy
Session logs: scrub public addresses, device id, Steam account names, and every file
2026-09-25 12:53:30 -04:00
The412Banner 75ae619e21 Logs: scrub Steam account and persona names for every user
The client's UI log names the signed-in account on every login change ("Login:
OnLoginStateChange <account> ...", "OnLoginUsersChanged <account>"), ~200 lines on one device.
An email account was caught by the email rule; a plain account name was not, so a user signing in
with one had it in every shared log. Those two lines now lose the account, and every
AccountName and PersonaName in the client's loginusers.vdf is learned (with the device's own
addresses, learnFromRuntime) and replaced wherever it appears. The one-time pass over older
folders runs again for this (marker .scrubbed-2).
2026-09-25 11:51:37 -04:00
The412Banner 3efa5acfa6 Logs: no public addresses or device id, and every session file scrubbed
A privacy pass over the session folders found three leaks:
- the Steam client's IPv6 check logs "external address <the device's public IPv6>" about
  twenty times a session, and the redactor kept IP addresses on purpose, so steam/
  connection_log.txt carried it;
- network.txt listed the device's addresses (public IPv6 included) and the session's made-up
  MAC, which is seeded from ANDROID_ID and so stable per device;
- session.log, app.log, desktop.log, steam-desktop.log, crash.log were never scrubbed (clean on
  device, by luck).

The redactor now learns the device's own addresses from the session's link file (a global IPv6
as its /64, a public IPv4 exactly) and removes them wherever they appear, along with anything
logged as an "external address"; Valve's server addresses stay. network.txt gives every address
as its kind ("<public IPv6>") and masks the MAC. Every text file in a session folder goes
through the redactor when the session ends (killed sessions at the next app start), and again on
its way into the share-logs zip. Folders written before this are scrubbed once at app start.
2026-09-25 11:30:16 -04:00
The412Banner 9369dc3031 Merge pull request #18 from Droid-Deck/feat/melonds-preset
melonDS out of the box, cursor shapes and auto-hide, Steam desktop switching
2026-09-25 11:15:19 -04:00
The412Banner 5d84d3fef7 Merge branch 'main' into feat/melonds-preset 2026-09-25 10:32:03 -04:00
The412Banner 51baf75af1 Session: start a relaunch request in place, so DESKTOP reaches the desktop
The drawer's DESKTOP ended Big Picture and landed on the front end. The session activity is
singleTop: startActivity(next) from it went to its own onNewIntent and was dropped when it
finished (on device the next session came up as "steam", not the desktop). It now restarts on
the request's intent (setIntent + recreate), as an agent start does; the desktop's Steam
launchers' handoff to a Steam session goes the same way.
2026-09-25 10:09:55 -04:00
The412Banner 20f2c98d18 Session drawer: DESKTOP in Steam sessions opens the desktop with Steam in it
Big Picture's own Switch to Desktop waits on SteamOS Manager and never finishes here. The
drawer's Steam row gets a DESKTOP button beside STEAM and QAM: the session ends and the desktop
starts in its place with Steam's desktop client open (the relaunch the Steam Desktop UI button
uses).
2026-09-25 09:56:33 -04:00
The412Banner e3ceae412b Revert the Steam session's D-Bus session bus
Tried on device: with a session bus the client still sat on "Switching to Desktop..." and
never reached its legacy path (no "No SteamOSManager support" line, steamos-session-select not
called), so the bus was not what it waited on. Sign-in was unaffected.

This reverts commit 40fc5463f3.
2026-09-25 09:55:59 -04:00
Kurt Himebauch 7e6affed12 feat: add session log sharing (#17) 2026-09-25 09:44:33 -04:00
The412Banner 40fc5463f3 Steam session: a D-Bus session bus, so Switch to Desktop reaches steamos-session-select
Big Picture's Switch to Desktop asks SteamOS Manager over the session bus before falling back to
steamos-session-select (steamui.so). The Steam session had no session bus, the ask never
finished, and the client sat on "Switching to Desktop..."; the new stub was never called. A
bus with nothing on it answers at once, and the client takes its legacy path.
2026-09-25 09:41:21 -04:00
The412Banner 039948f4d2 Steam desktop UI on the desktop; Big Picture's Switch to Desktop works
- The front end's "Steam Desktop UI" started a Steam session without -gamepadui, and the client
  put itself into Big Picture under gamescope anyway. It now opens the desktop with Steam's
  desktop client started in it (BL_DESKTOP_STEAM, the desktop's autostart, the same
  bannerlator-steam-launch the menu's Steam entry uses).
- Big Picture's power menu "Switch to Desktop" runs steamos-session-select, which did not exist,
  so it sat on "Switching to Desktop..." for ever. The new stub leaves the app a request
  (session=desktop in the steam-launch file); the app ends the Steam session and starts the
  desktop with Steam's desktop client open.
2026-09-25 08:59:06 -04:00
The412Banner bbb95e8a60 Desktop: centre melonDS's 800x600 window
AutoPlace on its own left the resized window in the top-left corner; with policy="center" it
opens in the middle of the desktop. Tried in a running desktop (labwc --reconfigure).
2026-09-25 08:36:04 -04:00
The412Banner 7763d00b78 Desktop: melonDS opens as an 800x600 window, centred
melonDS remembers a 1x window for the front end's full-screen toggle, too small on a desktop.
A labwc window rule (only the desktop reads rc.xml) resizes it to 800x600 on open and places
it again by the desktop's centre policy.
2026-09-25 08:28:56 -04:00
The412Banner 3452730725 Session: draw the program's own pointer - resize arrows at window edges, I-beams, its hide
The compositor threw every wl_pointer.set_cursor image away and the app drew one fixed arrow,
so the desktop never showed labwc's resize arrows at a window's edges and corners. Ported from
Bannerlator (7f19c669): the cursor surface gets a role, its image is snapshotted on commit with
its hotspot, and a NULL surface or buffer is an authoritative hide; the app's overlay draws the
image at the session's scale. New here: labwc on Vulkan/GLES hands its cursor over as a dma-buf
(wlroots' nested backend, usually UBWC), so it is read back through a small linear host-visible
copy (vkp_image_readback). The built-in arrow stays until a program sets a cursor.
2026-09-25 08:28:26 -04:00
The412Banner e50f61f5d9 Desktop: Qt falls back to X11, so melonDS opens from the menu
The desktop set QT_QPA_PLATFORM=wayland for every program. The melonDS AppImage bundles only
Qt's xcb plugin, so from the desktop it exited at once: "Could not find the Qt platform plugin
wayland ... Available platform plugins are: xcb" (exit 127). "wayland;xcb" keeps LXQt on
Wayland and lets such an AppImage use Xwayland. Proven in a running desktop: labwc mapped
"melonDS 1.1".
2026-09-25 08:18:43 -04:00
The412Banner a7a4308e3c Session: hide the mouse cursor while a controller or the on-screen controls are in use
The arrow is drawn by the app and, outside a Steam session, never went away once shown - on
the desktop and over emulators started from the front end alike. A button press, a stick,
trigger or d-pad past halfway, or any on-screen control now hides it (PadBridge tells the
activity); the next touchpad or mouse move brings it back, once the pad has been quiet for
1.2 s so a touch during play does not flash it up (Bannerlator's rule for its Wayland pointer).
2026-09-25 08:08:09 -04:00
The412Banner e811576e79 melonDS: leave full screen to a 1x window, so its menus are big enough to use
The guide button's toggle returns to melonDS's saved window size. A 512x288 window (1x wide,
16:9) is stretched by gamescope to fill the panel, menu bar and cursor with it; at 2x and up
the menus stay at their real size, tiny on a handheld. Written once, with the layout.
2026-09-25 07:52:26 -04:00
The412Banner 960abec0ed melonDS: big top screen with the bottom screen alone beside it
Hybrid repeats the big screen, small, above the bottom one. Horizontal with the top screen
emphasised fills the panel's height with the top screen and centres the bottom screen in the
rest. Installs set up with Hybrid (or still on melonDS's defaults) move once.
2026-09-25 07:47:59 -04:00
The412Banner 855ab88d6c HUD: follow the game window, not a program's cursor or menus
A program's small surfaces (gamescope's 1x1 cursor, Qt's menus and tooltips) announced
themselves after the game's window and took the HUD, which then read 0 fps while melonDS ran
at 60. A new window now takes the HUD only when it is at least as big, and a window quiet for a
second gives way to the next one that draws.
2026-09-25 07:39:16 -04:00
The412Banner f7ee6e78f0 melonDS: Hybrid screens, full screen from the front end, guide toggles full screen, BIOS found in the ROMs folder
- bannerlator-pad-defaults: new installs start on the Hybrid layout with sharp pixels; an
  existing install is moved once, only where still on melonDS's defaults (marker keeps later
  choices). Guide = HK_FullscreenToggle. The user's DS/DSi BIOS, firmware and NAND are found in
  ROMs/nds/{bios,firmware} and filled in only where melonDS has no path.
- Front end starts melonDS with -f, at the panel's own resolution unless the user chose one.
2026-09-25 07:37:21 -04:00
Kurt Himebauch 293c58aeba Merge pull request #13 from Droid-Deck/feat/hamburger-rail
Use hamburger drawer for launcher navigation
2026-09-25 01:25:53 -04:00
Kurt Himebauch 95b8a4c3c4 Merge branch 'main' into feat/hamburger-rail 2026-09-25 01:25:28 -04:00
Kurt Himebauch b7fd52de79 Merge pull request #15 from Droid-Deck/feat/droiddeck-steam-hostname
Make Steam identify DroidDeck as the host
2026-09-25 00:50:32 -04:00
xXJsonDeruloXx 53ce764548 Set PRoot guest hostname to DroidDeck 2026-09-25 00:48:03 -04:00
Kurt Himebauch 157e74907a Merge pull request #14 from Droid-Deck/feat/live-session-suspend-policy
Add live suspend policy control
2026-09-25 00:31:16 -04:00
xXJsonDeruloXx 015ac26e07 Add live suspend policy to session drawer 2026-09-25 00:26:58 -04:00
xXJSONDeruloXx 1fe7faaa8d fix: fit library art on short screens 2026-09-25 00:12:17 -04:00
xXJSONDeruloXx 2e7f99897d fix: add headroom above focused game tiles 2026-09-25 00:01:40 -04:00
xXJSONDeruloXx d7fa65ad20 feat: use hamburger drawer for launcher navigation 2026-09-24 23:57:01 -04:00
MaxsTechReview ae614ce476 Merge pull request #9 from maxjivi05/feature/controller-editor
Feature/controller editor
2026-09-24 23:08:23 -04:00
MaxsTechReview 7d6e1cce2f Merge branch 'main' into feature/controller-editor 2026-09-24 23:08:05 -04:00
maxjivi05 bbf46cad9d Fit the on-screen pad to phones, add adaptive sticks
Scale controls down on short screens with a 9.5 mm face-button floor and
keep a bottom margin. On phones the shoulders, View and Menu sit along the
top edge; bumpers and triggers are drawn as side-by-side rectangles.
Adaptive sticks centre under the thumb and return home on release, with a
toggle in the Controller settings. The editor toolbar is a compact pill.
2026-09-24 23:05:45 -04:00
Kurt Himebauch 68c7b4b3c8 Merge pull request #12 from Droid-Deck/fix/steam-library-capsules
Fix Steam library capsule artwork
2026-09-24 22:57:27 -04:00
Kurt Himebauch 5d38a6c8c5 Merge branch 'main' into fix/steam-library-capsules 2026-09-24 22:57:17 -04:00
xXJSONDeruloXx 99a0c44f98 Fix Steam library capsule artwork 2026-09-24 22:52:37 -04:00
MaxsTechReview 1812ba20e2 Merge branch 'main' into feature/controller-editor 2026-09-24 22:45:27 -04:00
The412Banner 771d3c399c Merge pull request #10 from Droid-Deck/feat/desktop-steam-dolphin
Desktop: Steam launchers on the desktop, two-finger drag, Dolphin GameCube/Wii
2026-09-24 22:43:34 -04:00
MaxsTechReview af74a4449e Merge branch 'main' into feature/controller-editor 2026-09-24 22:38:46 -04:00
The412Banner 8c171dd2c7 Merge remote-tracking branch 'org/main' into feat/desktop-steam-dolphin
# Conflicts:
#	app/src/main/java/com/droiddeck/launcher/session/SessionService.kt
2026-09-24 22:35:37 -04:00
Kurt Himebauch 3292ed6f1a Merge pull request #7 from Droid-Deck/feat/debug-agent-control
Add debug-only DroidDeck agent control
2026-09-24 22:34:41 -04:00
The412Banner 96bbe0c4cc Merge remote-tracking branch 'origin/feat/desktop-steam' into feat/desktop-steam-dolphin 2026-09-24 22:24:00 -04:00
MaxsTechReview 039e14de5a Merge branch 'Droid-Deck:main' into feature/controller-editor 2026-09-24 22:23:26 -04:00
xXJSONDeruloXx a419c8097f fix(agent-bridge): launch sessions through debug activity 2026-09-24 22:16:28 -04:00
maxjivi05 3fac2e8945 Add a Controller section to Setup with an on-screen pad editor
The on-screen pad is now placed from the panel's physical size: the
sticks and face buttons sit at the two-thumb rest point (about 17% of
the width inboard, 28% of the height up, clamped in millimetres), with
the d-pad and right stick below-inboard, shoulders above, and nothing
ever placed off screen or under a display cutout. Button sizes are
unchanged.

Setup > Controller holds the on-screen controls mode, a tint (Steam
blue #1A9FFF by default), opacity, button size, stick click (double-tap
for L3/R3), a full-screen layout editor, per-button mapping including
LT/RT, and resets for each.

Also from WinNative #772: the fakeinput ring poll backoff is capped at
2 ms instead of 16 ms, the session asks for unbuffered joystick input,
and the on-screen controls request unbuffered dispatch per gesture and
release everything when detached. The QAM button no longer draws at the
top-left corner in full-pad mode.
2026-09-24 22:13:48 -04:00
Kurt Himebauch c2d6d272a3 Merge branch 'main' into feat/debug-agent-control 2026-09-24 22:00:40 -04:00
Kurt Himebauch 846630e300 Merge pull request #8 from Droid-Deck/chore/codebase-deslop
chore/cleanup
2026-09-24 22:00:23 -04:00
Kurt Himebauch cc1f3d256b Merge branch 'main' into chore/codebase-deslop 2026-09-24 22:00:12 -04:00
Kurt Himebauch 243a760a12 Merge pull request #4 from maxjivi05/fix/proot-performance
proot: build from source with WinNative's performance fixes
2026-09-24 21:59:27 -04:00
xXJSONDeruloXx 4f744fc8fe chore: remove dead code and stale comments 2026-09-24 21:53:31 -04:00
xXJsonDeruloXx 54adf9ad68 Add debug-only agent control bridge 2026-09-24 21:34:50 -04:00
The412Banner 8b66adb8a5 Dolphin: guide = its Toggle Fullscreen hotkey (full default Hotkeys.ini), Wii Home on R1, input without focus from the rail 2026-09-24 21:31:25 -04:00
maxjivi05 9f0c1fc797 Merge Droid-Deck main into fix/proot-performance 2026-09-24 21:27:31 -04:00
Kurt Himebauch 4a46e16702 Merge pull request #6 from Droid-Deck/feat/in-session-sidebar-menu-review
Paged in-session drawer with controller navigation
2026-09-24 21:26:01 -04:00
Kurt Himebauch 608d011b44 Merge branch 'main' into feat/in-session-sidebar-menu-review 2026-09-24 21:20:21 -04:00
xXJSONDeruloXx 0d52ba66cd feat: confirm session stop from drawer header 2026-09-24 21:19:20 -04:00
Kurt Himebauch 431fac1fca Merge pull request #5 from Droid-Deck/chore/protect-main-signing
Gate release-signed PR APKs behind review
2026-09-24 21:15:27 -04:00
xXJSONDeruloXx 35006d6031 feat: add paged in-session drawer navigation 2026-09-24 21:14:55 -04:00
xXJSONDeruloXx 7ae66337ef Trim signing key script comments 2026-09-24 21:02:13 -04:00
xXJSONDeruloXx b75e0f9800 Trim workflow commentary and redundant build output 2026-09-24 21:01:35 -04:00
The412Banner ee3fd4d6b8 Dolphin from the rail: guide toggles full screen (its window and settings), drawn in the main window 2026-09-24 21:00:43 -04:00
xXJSONDeruloXx 0352f4b832 Simplify approved PR signing workflow 2026-09-24 20:45:25 -04:00
The412Banner 7fbfeb2edd Dolphin: its data where it looks for it (fonts, per-game fixes), game folders, Vulkan, no NKit box; full screen from the front end 2026-09-24 20:35:48 -04:00
xXJSONDeruloXx 8db69f073b Preserve the current PR APK until upload succeeds 2026-09-24 20:22:36 -04:00
xXJSONDeruloXx 226fa1c1ea Match PR APK version to latest stable 2026-09-24 20:19:14 -04:00
xXJSONDeruloXx 6f41ae58db Use the pinned release signer for PR APKs 2026-09-24 20:16:28 -04:00
xXJSONDeruloXx ae9186392f Require repo write access for signing approvals 2026-09-24 20:13:40 -04:00
xXJSONDeruloXx 4279ffd887 Gate release-signed PR APKs on maintainer approval 2026-09-24 20:08:33 -04:00
xXJSONDeruloXx ac058ea112 chore: harden release signing boundaries 2026-09-24 19:38:04 -04:00
The412Banner 5d42e5e87a Touchpad: one finger held, the other slides = drag (move windows, resize, drag icons) 2026-09-24 19:10:29 -04:00
The412Banner 96ac6967e6 Desktop: the Steam and Big Picture launchers run the client on the desktop itself 2026-09-24 18:56:24 -04:00
MaxsTechReview 0c301f3cec Merge pull request #3 from maxjivi05/ci/pr-release-publish
CI: publish each pull request's APK to DroidDeck-CI
2026-09-24 18:15:04 -04:00
The412Banner d7cadd05d6 Merge pull request #45 from The412Banner/release/0.1.7
Release 0.1.7 (versionCode 8)
2026-09-24 16:42:59 -04:00
The412Banner 2f8464b7f5 Release 0.1.7 2026-09-24 16:37:31 -04:00
maxjivi05 96314749da CI: publish each pull request's APK to DroidDeck-CI
After a pull request's Build APK run succeeds, its APK is published as the
pr-<number> prerelease in Droid-Deck/DroidDeck-CI, named
DroidDeck-PR.<number>-<commit>.apk, with release notes taken from the pull
request. New commits to the same pull request replace its APK. Pull requests
from forks are resolved by head owner, branch and commit.

A pull request's build is signed only with the committed testkey, so every CI
build installs over the last; the publish checks the certificate against it
and refuses anything else.

Needs a DROIDDECK_CI_TOKEN secret: a fine-grained token with Contents
read/write on Droid-Deck/DroidDeck-CI. Without it the run skips with a warning.
2026-09-24 16:07:28 -04:00
The412Banner 33f06d744f Merge pull request #44 from The412Banner/feat/home-screen-toggle
Home screen: a switch in Setup, off by default (replaces the Non-Launcher apk)
2026-09-24 15:06:39 -04:00
The412Banner d6ca48d80a Home screen: a switch in Setup (off by default) instead of a second apk 2026-09-24 14:39:20 -04:00
The412Banner c8b628393b Merge pull request #31 from The412Banner/docs/progress-log-0924
Progress log: 2026-09-24
2026-09-24 14:02:08 -04:00
The412Banner e9d4eb1e1b Progress log: PS1, controller focus, own signing key, CI hardening (2026-09-24) 2026-09-24 13:57:10 -04:00
The412Banner 89f78a9969 Merge remote-tracking branch 'origin/main' into docs/progress-log-0924 2026-09-24 13:55:03 -04:00
The412Banner 94f9935ad4 Merge pull request #41 from The412Banner/feat/release-signing
Signing with DroidDeck's own key, four package names, file-picker access, CI hardening
2026-09-24 13:42:47 -04:00
The412Banner 66c7db5a47 Merge remote-tracking branch 'origin/main' into feat/release-signing 2026-09-24 13:38:31 -04:00
The412Banner b959de3d26 CI: build pull requests with pull_request, not pull_request_target (no shared caches with main) 2026-09-24 13:37:43 -04:00
The412Banner b802485e31 CI: the key alias is plain text, not a secret 2026-09-24 13:24:47 -04:00
Kurt Himebauch ee20fd60c4 Improve session drawer stick navigation (#40) 2026-09-24 13:18:09 -04:00
The412Banner 52174e2ab0 sign-apk: match apksigner's 'V3.0 Signer:' lines too 2026-09-24 13:17:57 -04:00
The412Banner 216f3c1d62 sign-apk: read the signer digest in either apksigner format 2026-09-24 13:12:09 -04:00
The412Banner 7e6954304a CI: pass secrets on to sign.yml so it can read the environment's key 2026-09-24 13:06:26 -04:00
The412Banner 8718ae0aa8 The release key's certificate digest (public), checked against every signed apk 2026-09-24 12:59:34 -04:00
The412Banner 615311ecd6 CI: give the signing job the permissions sign.yml asks for 2026-09-24 12:46:39 -04:00
The412Banner 3a4ef4f6d6 Signing with DroidDeck's own key: hand-over from the testkey, test builds and a four-package release 2026-09-24 12:45:13 -04:00
The412Banner 403cfa77ae The app's files in Android's file picker (documents provider), for every package name 2026-09-24 12:35:32 -04:00
The412Banner c7998a82d5 Merge pull request #38 from The412Banner/feat/duckstation-play
PS1 one-tap launch, one tile per disc, controller lands on the main button and first game
2026-09-24 11:33:31 -04:00
Kurt Himebauch 86bc9f4a64 Update DroidDeck branding to split D mark (#37) 2026-09-24 11:33:14 -04:00
The412Banner 2f4c4624ae Merge remote-tracking branch 'origin/main' into feat/duckstation-play 2026-09-24 11:27:12 -04:00
The412Banner b83e5f87cf Front end: Down from a page's buttons goes to the first tile 2026-09-24 11:19:12 -04:00
Kurt Himebauch b49e2753df Merge pull request #33 from The412Banner/feat/secondary-screen-app-launch
Add per-launch Android display selection
2026-09-24 11:14:27 -04:00
xXJsonDeruloXx 3c09e1d736 Merge remote-tracking branch 'origin/main' into feat/secondary-screen-app-launch
# Conflicts:
#	app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt
#	app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt
2026-09-24 11:09:24 -04:00
Kurt Himebauch 0f07dafb36 Merge pull request #35 from The412Banner/fix/ci-build-cleanliness
Snapshot CI build cleanliness before generated assets
2026-09-24 10:45:35 -04:00
The412Banner 38fdca0b97 Front end: a page opened from a tile takes the pad on its main button 2026-09-24 10:42:46 -04:00
xXJsonDeruloXx e8efe2d290 Snapshot CI tree state before generated assets 2026-09-24 10:39:38 -04:00
The412Banner a1ae55125a DuckStation, ARMSX2: first-time setup answered for the user; ARMSX2 names the BIOS dump itself 2026-09-24 10:09:45 -04:00
Kurt Himebauch 706692c1fe Merge pull request #24 from The412Banner/feat/launch-build-status
Show build identity and latest build link
2026-09-24 10:09:09 -04:00
The412Banner 582f031243 DuckStation: don't try to inhibit a screensaver the session doesn't have 2026-09-24 09:46:59 -04:00
xXJsonDeruloXx 9c78373c73 Put build source below watermark identity 2026-09-24 09:43:09 -04:00
xXJsonDeruloXx e9f3416438 Show build source and worktree state 2026-09-24 09:40:50 -04:00
The412Banner ea44a77f8a Front end: one tile per disc - hide tracks a .cue/.gdi/.m3u names, skip BIOS folders 2026-09-24 09:39:38 -04:00
The412Banner 63d7051c4b Merge pull request #34 from The412Banner/feat/duckstation
DuckStation (PS1) setup + front-end controller focus
2026-09-24 09:34:53 -04:00
xXJsonDeruloXx 4803c9d221 Add display choice to Android app launches 2026-09-24 09:30:34 -04:00
The412Banner 6388edb706 Front end: come back to the last focused tile or button, not the grid 2026-09-24 09:26:46 -04:00
The412Banner 0774ade030 Front end: leave touch mode on open so the rail item can take focus 2026-09-24 09:19:10 -04:00
The412Banner e5ee766961 Front end: outline the rail item on open, remember the pane control across Left/Right 2026-09-24 09:12:31 -04:00
The412Banner 274f3d0c7a DuckStation (PS1) set up like ARMSX2
- From the rail: -batch -bigpicture -fullscreen (the same flags as PCSX2's,
  src/duckstation-qt/qthost.cpp) - straight into the game, gone when it is quit.
- bannerlator-pad-defaults: a new player starts on DuckStation's first-time setup
  with the BIOS folder in the ROMs folder (psx/bios, created, or an existing one),
  psx as the game list, Vulkan, the controller on Player 1 and the guide button
  on the pause menu (not the analog toggle). An existing install gets the guide
  and game-list changes, and once, its own empty BIOS folder moved to the ROMs
  folder and Automatic made Vulkan; later choices are kept. Users bring their own
  BIOS and games.
2026-09-24 09:03:34 -04:00
The412Banner 21be683768 Front end: controller focus starts on the rail and moves between rail and page predictably
- The app opens with the selected rail item focused (its outline shows).
- Right from the rail enters the page on its main button (Play, Open desktop,
  Open <emulator>, Launch, Launch in <emulator>); Down from there reaches the
  tiles as before.
- Left from anything at the page's left edge - the main button, the first tile of
  any row - returns to the selected rail item, not whichever one is nearest.
  (Up, where the rail sits above the page.)
2026-09-24 08:59:45 -04:00
The412Banner 695923aed7 Merge pull request #32 from The412Banner/feat/fullscreen-toggle
Stretch games to fill the screen (cog + live drawer), Quake-engine titles windowed at the session's size
2026-09-24 08:41:10 -04:00
The412Banner 649153257d Merge remote-tracking branch 'origin/main' into feat/fullscreen-toggle 2026-09-24 08:35:43 -04:00
The412Banner cbcd3f8023 Proton launch: Quake-engine titles stay windowed at the session's size
Quake III Arena, Team Arena, Return to Castle Wolfenstein and Jedi Academy set their own
resolution and switch the display to it. Under gamescope with the ARM64 Proton that switch is
refused ("calling CDS: failed, bad mode") and the game never gets OpenGL - device-seen on a
Retroid Pocket 5 on Proton Experimental build 25406135 - and a window left at the game's own size
draws small in a corner. Both wrappers (Valve's depot and an adopted GE or CachyOS) now add
+set r_fullscreen 0 +set r_mode -1 +set r_customwidth/height at BL_WIDTH x BL_HEIGHT for those
app ids, unless the user's launch options already set the video mode; the choice is logged.
2026-09-24 08:26:51 -04:00
xXJsonDeruloXx 41c5ce8d4e Keep the build hash visible in rail watermark 2026-09-24 07:50:22 -04:00
xXJsonDeruloXx 78ce23258b Log session drawer controller routing 2026-09-24 07:46:20 -04:00
xXJsonDeruloXx 3e864fda17 Move build status to launcher rail footer 2026-09-24 07:45:55 -04:00
xXJsonDeruloXx 0cdb945d9c Fix controller focus in session drawer 2026-09-24 07:36:48 -04:00
xXJsonDeruloXx 15a113f783 Add direct touchscreen input for Steam sessions 2026-09-24 07:35:47 -04:00
xXJsonDeruloXx 67f00d2123 Add second-screen controls close button 2026-09-24 07:35:26 -04:00
xXJsonDeruloXx d68dcd14e1 Make session menu controller driven 2026-09-24 07:35:26 -04:00
xXJsonDeruloXx 32994210ab Remove build badge caption 2026-09-24 07:33:16 -04:00
xXJsonDeruloXx cfec8ab88a Show build identity on launcher 2026-09-24 07:33:16 -04:00
Kurt Himebauch 87c44c3218 Merge pull request #20 from The412Banner/controller-back-button-gestures 2026-09-24 06:47:23 -04:00
The412Banner 35446df6d5 Progress log: GPU desktop, emulators like PS3, ARMSX2, game art (2026-09-24) 2026-09-24 06:30:39 -04:00
The412Banner 921b38bfb4 Session drawer: stretch games to fill the screen, live
The same choice as the Steam cog's, in the drawer's Now group, changed while the client runs:
the app writes it to ~/.droiddeck-fill (and again at every session start, so a file an earlier
session left never disagrees), and a watcher in the Steam session hands each change to gamescope
as GAMESCOPE_FORCE_WINDOWS_FULLSCREEN on the root window, which gamescope reads whenever it
changes. The session log says so when it does.
2026-09-24 06:29:33 -04:00
The412Banner 82bb924d0b Steam cog: stretch games to fill the screen, as a switch
0.1.6 forced every game window to the screen's size (--force-windows-fullscreen), which keeps
FlatOut filling the screen but leaves Quake 3 drawing small in the bottom-left: it sets its own
resolution and never looks at its window again, and OpenGL draws from that corner. The switch
sits in the Steam page's Games group, on by default as before, and reaches the session as
BL_GAMESCOPE_FORCE_FULLSCREEN, which the session script already reads. Listed in the device
report.
2026-09-24 06:26:12 -04:00
The412Banner 08a6769bc0 Merge pull request #29 from The412Banner/feat/armsx2-polish
ARMSX2 like RPCS3, controllers and on-screen controls everywhere, Back, game art
2026-09-24 05:44:41 -04:00
The412Banner dce87061d1 Front end: wide cover art shown whole, titles without dump tags
- A portrait tile or poster with wide art (a PS3 disc's ICON0, 320x176) shows it
  whole over a blurred, darkened copy of itself instead of cropping its sides
  ('Tomb Raider' read 'B RAI'); box art still fills the tile.
- A game's title drops the dump's tags - (USA), (En,Fr,Es,Pt), (v1.01), [!] -
  which stay in the file name and in the cover lookup.
2026-09-24 05:31:00 -04:00
The412Banner f4302e4fa9 Cover art: a PS3 disc image gives its own ICON0.PNG
The libretro PS3 collection holds 67 covers (Tomb Raider found none). A PS3 disc
image carries PS3_GAME/ICON0.PNG, unencrypted even on protected discs; it is read
out of the ISO 9660 image, the way an installed game's icon already is. The PS2
serial lookup uses the same reader.
2026-09-24 05:17:20 -04:00
The412Banner 0436953451 Front end: box art for emulator games, found automatically and kept
CoverArt looks a game up by its file name in RetroArch's libretro-thumbnails
collection for the system (exactly, then loosely: tags dropped, same region
preferred, against the collection's cached list of names), then by the disc's
own ID - PS2 serials from SYSTEM.CNF against xlenore/ps2-covers, GameCube/Wii IDs
against GameTDB. Covers are downloaded once into files/covers after the list is
shown, and the list is refreshed when some arrive; a game with none is retried
after three days; nothing is fetched offline. PS3 disc dumps use their own
PS3_GAME/ICON0.PNG.
2026-09-24 05:11:38 -04:00
The412Banner 7165907ba9 Front end: Back steps out of a game or emulator page; PS2 and other games found deeper in their folder
- Back / B on an emulator page went back to Android (the app closed); it now
  returns to Desktop, a game page to its emulator, a Steam game to Steam - the
  same as each page's '‹' link.
- Games are looked for up to three folders inside the system folder
  (ROMs/ps2/games/<game>/<game>.iso showed ARMSX2 with 0 games), and the loose-file
  pass over the ROMs root no longer walks into other systems' folders.
2026-09-24 04:55:01 -04:00
The412Banner e938127c7a Drawer: a game from the rail is headed by its emulator (ARMSX2, RPCS3), not "Desktop" 2026-09-24 04:43:24 -04:00
The412Banner b5c9d6bc50 ARMSX2: a new player starts on the Vulkan renderer (GSRendererType::VK = 14), not Auto's pick 2026-09-24 04:32:58 -04:00
The412Banner f7af5091cf ARMSX2 like RPCS3: front-end launches, first-time setup, PCSX2 settings, 16K pages; on-screen controls everywhere
- From the rail ARMSX2 starts the game in its full-screen, controller-driven UI
  and exits with it (-batch -bigpicture -fullscreen).
- bannerlator-pad-defaults: a PCSX2 player's settings and memory cards are
  copied into ARMSX2 once; a new player starts on ARMSX2's first-time setup with
  the BIOS folder in the ROMs folder (ps2/bios, created, or ps2/firmware), the
  ps2 folder as the game list and the controller on Player 1; the guide button
  opens the pause menu (how a rail game is left) instead of toggling analog.
  Users bring their own BIOS and games.
- The catalog can name a file per memory page size ("pages"); the device's own
  size picks it (ARMSX2 has 4K and 16K builds).
- On-screen controls: Auto keeps them off the desktop (the screen is a touchpad
  there) and shows them for rail games and Steam when no controller is
  connected; the drawer offers the setting in every session.
2026-09-24 04:29:57 -04:00
MaxsTechReview 7ac1786a2e Merge branch 'The412Banner:main' into fix/proot-performance 2026-09-24 03:43:13 -04:00
xXJsonDeruloXx 19a919e096 Revert "Improve accelerated drawer stick navigation"
This reverts commit 10f5506829.
2026-09-24 01:07:34 -04:00
xXJsonDeruloXx 10f5506829 Improve accelerated drawer stick navigation 2026-09-24 01:04:29 -04:00
xXJsonDeruloXx d3c6d54a58 Merge latest main and fix controller drawer focus 2026-09-24 00:54:32 -04:00
The412Banner 6980428f82 Merge pull request #26 from The412Banner/feat/armsx2
PS2: ARMSX2 replaces PCSX2 (ARM64 recompilers, full speed)
2026-09-24 00:12:18 -04:00
The412Banner 1a3b16f614 PS2 is ARMSX2 only: PCSX2 leaves the front end
On device NFS Underground 2 ran at 17 fps in PCSX2 (one core at 100%, the PS2's
CPUs interpreted) and 59.9 fps at 100% speed in ARMSX2 (EE thread ~15%, all
recompilers and fastmem on under proot). Existing PCSX2 configs are left alone;
bannerlator-pad-defaults still covers them.
2026-09-23 23:59:25 -04:00
The412Banner e0b9f462ce ARMSX2 (PS2 with ARM64 recompilers) in the front end, with Player 1 on the controller
Upstream PCSX2 interprets the PS2's CPUs on ARM64 (pcsx2/arm64/RecStubs.cpp):
NFS Underground 2 ran at 17 fps with one core at 100%. ARMSX2 is the PCSX2 fork
with ARM64 recompilers. It is listed beside PCSX2 for the ps2 folder, installs
from the desktop catalog (package armsx2), and bannerlator-pad-defaults gives it
the PCSX2 profile under its own folder (ARMSX2/inis/PCSX2.ini).
2026-09-23 23:55:20 -04:00
The412Banner f6f7470385 Merge pull request #25 from The412Banner/feat/desktop-gpu
Desktop on the GPU (Vulkan by default), emulator controllers, PC keyboard, PS3 shader fixes
2026-09-23 23:16:51 -04:00
xXJsonDeruloXx 214c708b49 Merge remote-tracking branch 'origin/main' into controller-back-button-gestures
# Conflicts:
#	app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt
2026-09-23 23:07:55 -04:00
xXJsonDeruloXx d567185fc0 Replace Back inversion toggle with order menu 2026-09-23 23:05:12 -04:00
The412Banner 24188bda10 Compositor: xdg_positioner, xdg_popup and wl_output requests no longer abort
Positioners and outputs were created without an implementation, and get_popup
created nothing. libwayland aborts on the first request to a resource with no
implementation - the compositor and the app with it. On device: a gamescope
started from the pixman desktop (droiddeck-gpu) beside labwc took the app down
(tombstone: abort in libwayland-server under wl_event_loop_dispatch). Positioner
requests are accepted and ignored, popups are dismissed at once (popup_done),
wl_output.release destroys the resource.
2026-09-23 22:59:44 -04:00
The412Banner 1a870c5c07 Desktop: Vulkan by default; a renderer that fails to start is remembered
vulkan is the default desktop renderer (settings list it first). When labwc
cannot start on a GPU renderer the desktop falls back to pixman as before, and
now records the renderer with the patched wlroots and Linux driver it failed
on, so later desktops go straight to pixman instead of waiting 20 s each time;
an app update, a different driver, or choosing a renderer again in the settings
(which deletes the record) retries.
2026-09-23 22:49:50 -04:00
The412Banner fa964802d9 PS3: one shader cache, no interpreter precompile
- A program from the rail draws with the desktop's Linux driver instead of the
  Steam session's. The driver's shader cache is keyed on its build, so with two
  drivers an emulator compiled everything once per way of starting it; seen on
  device: desktop on the imported Turnip 26.3.0, rail on the runtime's own.
- RPCS3's default shader mode builds 6650 interpreter pipelines every boot.
  bannerlator-pad-defaults starts a new install on Async Recompiler
  (multi-threaded) and moves an existing one still on the default once; a
  marker leaves any later choice in RPCS3's settings alone. Enum strings from
  rpcs3/Emu/system_config_types.cpp.
2026-09-23 22:35:28 -04:00
The412Banner 793180ce12 Controller profiles for every desktop emulator; PC keyboard under half the screen
bannerlator-pad-defaults now covers DuckStation, PCSX2, Dolphin (GameCube and
Wii), Cemu, melonDS, PPSSPP and RetroArch as well as RPCS3, from each project's
source: files an emulator would otherwise fill with keyboard-only defaults are
seeded when missing (Dolphin, Cemu, PPSSPP, RPCS3), or given SDL bindings in the
config the emulator wrote itself when nothing binds a controller yet
(DuckStation, PCSX2 with its SettingsVersion, melonDS). RetroArch gets an Xbox
360 profile for its sdl2 driver and is moved off udev, which finds no pads in the
runtime. The desktop re-runs it every 30 s, so an emulator's first config is
covered for its next start.

The PC keyboard takes 47% of the screen height, header included.
2026-09-23 22:20:04 -04:00
The412Banner 79e7be470c PC keyboard: shorter rows, so the program stays visible above it 2026-09-23 22:13:59 -04:00
The412Banner 921f156128 Desktop programs: controller seen by every emulator, Player 1 profiles, big-core pinning
- The pad keeps Steam Input's virtual-gamepad identity only in Steam sessions.
  SDL ignores a Steam virtual gamepad unless run under Steam, so on the desktop
  and from the rail every SDL emulator saw no controller at all; there it is a
  plain Xbox 360 pad now, which SDL and evdev map by themselves.
- bannerlator-pad-defaults writes Player 1 on the controller for emulators that
  have no profile yet (RPCS3 first: without one it takes the keyboard). Run at
  desktop start and before a program from the rail; never overwrites.
- The desktop and everything started from it, and a program from the rail, run
  on the game cores when chosen, else every core but the slowest cluster
  (program_cores); BL_PIN_CORES=0 turns it off. Game cores reach every mode.
2026-09-23 22:06:47 -04:00
maxjivi05 c771fc67ac proot: build the runtime's proot from source with WinNative's performance fixes
The runtime carries Termux's proot 5.1.107-70 as a prebuilt binary, built from
termux/proot 4dba3afb (every file:line site in it matches that commit). The apk
now builds that same commit with the NDK, with three patches ported from
WinNative's feature/proot-enhancements, and prefers it over the runtime's copy:

- Tracees are hashed by pid, so each ptrace stop finds its tracee without
  walking a list of every thread, and terminated tracees are swept only after
  a termination.
- A clean absolute path three or more directories deep under the rootfs alone
  has its parent opened once and taken as canonical when the kernel's path for
  it is the one proot would build, instead of an lstat per component; the final
  component of a call that does not follow it is no longer lstat'ed. Extensions
  still see the parent's host path, and the f2fs workaround keeps the old walk.
- A clone3 thread keeps its inherited flags when clone_args cannot be read.

talloc is linked in statically from Samba's release tarball, so libproot.so
needs only libc and libdl. On an x86_64 Android 15 emulator the same build ran
a path and thread test suite with output identical to the unpatched commit
(fake id, fake root, link2symlink, with and without seccomp), and a stat-heavy
benchmark ran 25-27% faster, 31-36% with 1500 live threads.
2026-09-23 22:03:57 -04:00
The412Banner 7f2d2ffa4a Session: on-screen PC keyboard (Hardware) beside the Android keyboard
The drawer's Keyboard row has two buttons: Hardware opens ui/PcKeyboard, a full
PC layout - Esc, F1-F12, Ins/Del/Home/End/PgUp/PgDn, the number and letter rows,
Tab, Caps, both Shifts, Ctrl, Super, Alt, AltGr, Space and the arrows - whose
keys are evdev codes pressed and released on the compositor's wl_keyboard, as a
USB keyboard would; Android opens the Android keyboard as before. Keys go down
on touch and up on lift (the program's key repeat works); modifiers are sticky
(tap: next key, tap again: lock, again: off). A controller moves over the keys
with the d-pad and presses with A; B closes it.
2026-09-23 21:52:47 -04:00
The412Banner 2d9e1c6d95 Compositor: declare send_toplevel_configure before surface_commit 2026-09-23 21:41:23 -04:00
The412Banner 66dafa6021 Desktop: remote runs from ~/.cache/droiddeck/run behind ~/.droiddeck-remote-run
For driving the desktop from a debugger: a script dropped there runs once inside
the session. Off unless the marker exists.
2026-09-23 21:37:16 -04:00
The412Banner 2df3d054e8 Compositor: answer a toplevel's initial commit with configure
xdg-shell has the compositor reply to the initial commit; we only sent one at
get_toplevel. wlroots' Wayland backend (the desktop's labwc) moves its toplevel
to a private queue after the initial commit and waits there; on the vulkan
renderer a buffer import in between read the early configure onto the default
queue, and labwc waited for ever (WAYLAND_DEBUG: commit, then nothing). The
early configure stays for Wine's driver.
2026-09-23 21:36:08 -04:00
The412Banner c9cfa2126e Desktop GPU: full labwc log on the GPU renderers; a labwc that hangs at start falls back to pixman
The vulkan desktop sat on the loading screen: labwc connected and never opened
its window, and it did not exit, so the exit-based fallback never fired. The
launcher now asks labwc's socket for an answer (wayland-info, 3 s) for up to
20 s, then stops it and starts the desktop on pixman. labwc runs with -d on
vulkan/gles2 so desktop.log shows where it stopped.
2026-09-23 21:24:24 -04:00
The412Banner 2220578cd1 CI: listing the apk's assets no longer breaks the pipe past 30 entries 2026-09-23 21:09:57 -04:00
The412Banner 5b18807f0e wlroots: stage the published wlroots-0.20.2-p1 build 2026-09-23 21:04:16 -04:00
The412Banner d984a4ba31 Desktop GPU: menu games run in their own gamescope; labwc on the patched wlroots
droiddeck-gpu runs a program in a gamescope of its own on the app's compositor
(wayland-0), full screen over the desktop - the path the rail's play chips use,
where Vulkan works. droiddeck-desktop-gpu gives every Game/Emulator menu entry a
per-user copy through it (auto: while the desktop is on pixman), keeping 'Run in
a desktop window' as an action.

droiddeck-desktop loads /usr/local/lib/droiddeck-wlroots for the vulkan/gles2
renderers and falls back to pixman when labwc stops at start. The build stages
the wlroots release named by tools/wlroots/release.env, once published.
2026-09-23 21:02:24 -04:00
The412Banner 7dc00d9b23 Desktop GPU: patched wlroots with a Vulkan DMA-BUF allocator; new windows take the keys
wlroots 0.20.2 (Arch's wlroots0.20, what the desktop's labwc links) gains an
allocator that exports Vulkan images as DMA-BUFs for a GPU node that is not a
DRM device - the Adreno KGSL stand-in - where gbm and the DRM lease path fail
('unable to create allocator'). linux-dmabuf skips its GEM import check on such
a node so client buffers are not all refused. Built by build-wlroots.yml.

Compositor: without a Wine desktop, a window that opens on top takes keyboard
focus (a gamescope started from the Linux desktop).
2026-09-23 20:59:57 -04:00
The412Banner 59123f1c89 Merge pull request #23 from The412Banner/release/0.1.6
0.1.6
2026-09-23 19:20:02 -04:00
The412Banner aa1df93a22 0.1.6 notes: say exactly what is fetched and what is bundled 2026-09-23 19:19:56 -04:00
The412Banner c99617427a 0.1.6
versionCode 7, versionName 0.1.6; release notes in docs/releases/0.1.6.md.
2026-09-23 19:19:45 -04:00
The412Banner 54a4677f11 Merge pull request #22 from The412Banner/feat/first-login-proton
First sign-in fetches the ARM64 Proton and restarts the client once
2026-09-23 19:08:29 -04:00
The412Banner edeb388233 Front end: credits and the primary button show a focus outline
With a controller, credits only showed the ripple's faint box on the
dark rail, and the primary button (Play) only grew slightly with a shine
that barely shows on its light fill. Both now get the 2 dp signal-blue
outline the other controls use; credits' text turns blue as well.
2026-09-23 19:00:52 -04:00
The412Banner 034d484271 Session drawer: opens focused on its first control, and the controller drives it
With the drawer open the controller still fed the game - PadBridge
consumed every pad key and axis - and nothing in the drawer had focus.
Now, while it is open, pad events go to Android's own handling instead:
the d-pad and stick move focus, and the framework's fallbacks make A
select and B Back (which closes the drawer), as on the app's main
screen. Opening it releases the game's pad (PadBridge.releaseAll), so a
button or stick held at that moment is not left down in the game.

It opens with focus on the STEAM button (in a desktop session, the first
setting), and the STEAM and ... buttons show a focus outline like the
rest of the app's controls. ToggleRow gains a modifier for its box.
2026-09-23 18:52:50 -04:00
The412Banner ebf5da9840 First sign-in fetches the ARM64 Proton and restarts the client once, as Bannerlator does
On a fresh install the client starts signed out and drops the
steam://install/4427310 the session hands it, so the Proton Experimental
(ARM64) dialog only appeared after the user restarted the client by
hand. Ported from Bannerlator's session (device-proven there 2026-09-22):

- With no remembered sign-in, a watcher on logs/connection_log.txt waits
  for the sign-in to complete and asks the running client again.
- A watcher on appmanifest_4427310.acf waits for StateFlags 4, registers
  the compatibility tool again, leaves ~/.bl-restart-once and asks the
  client to -shutdown; the start loop sees the marker and starts it once
  more (not counted against the update cap), so the tool lists and
  defaults are current before the user installs a game.
- The client-branch check runs before every start, not once, since the
  client rewrites package/beta during its run.

The loading screen no longer says Steam keeps running in the background:
sessions pause when the app leaves the screen (Background behavior).
2026-09-23 18:48:19 -04:00
The412Banner f9c9084584 Merge pull request #21 from The412Banner/rename/droiddeck
Rename the app SteamDeck → DroidDeck
2026-09-23 18:42:42 -04:00
The412Banner d160f464b6 Rename the app SteamDeck → DroidDeck
App name, app ID and code package: DroidDeck, com.droiddeck.launcher
(sources moved to com/droiddeck/launcher, the 68 JNI functions renamed
with them). The new app ID installs beside SteamDeck rather than over it.

Everywhere the name was the app's own: the sidebar title, the session
report, share zips, the shell title, wake-lock and log tags, User-Agents,
the Compose theme, the Download folder and switch files (droiddeck-env,
-osc, -driver, -tu-debug, -no-pad, -pad-log, -no-hud, -wlr-renderer; the
old names are not read), and inside the runtime (droiddeck-desktop,
Firefox's droiddeck.js, the shortcuts tag, the rumble socket, debug and
cache paths). Build scripts, CI artifact (droiddeck-apk), CMake project,
README and the progress log follow.

Kept on purpose: Valve's Steam Deck (the -steamdeck flag,
steamdeck_publicbeta/stable, 'Steam Deck mode', the steamos.cloud
package URLs) and names inside the hosted desktop package (steamdeck.png,
steamdeck-steam, steamdeck-bigpicture). 'Desktop installed' now tests for
labwc instead of the launcher the app stages itself. A committed
__pycache__ .pyc is removed.
2026-09-23 18:28:04 -04:00
xXJsonDeruloXx c013252a59 Expose Back actions in app settings 2026-09-23 17:26:27 -04:00
xXJsonDeruloXx 7fa71dc5c5 Add Back shortcuts for session menu and Steam QAM 2026-09-23 17:16:02 -04:00
The412Banner a19dfb88a0 Merge pull request #16 from The412Banner/feat/qcom-driver-and-deck-fixes
Deck mode performance overlay, killed-session cleanup, NIS
2026-09-23 16:44:57 -04:00
The412Banner 738cd059a2 Driver page: d-pad right reaches a driver's trash can
The trash can sat inside the row's single clickable area, so with a
driver focused there was nothing to the right for a controller to move
to. Each row is now two focus targets side by side - the driver and its
trash can - linked both ways (right/left); the row's outline still
follows the driver part, so it looks the same.
2026-09-23 16:33:17 -04:00
The412Banner aaa6a0452f Session settings open with the first control focused
Opened from the cog, the Steam/Desktop session page had nothing focused:
the cog that was focused is gone and a controller had to find its way
back in. The page now puts focus on its first control (Resolution) as it
opens, so the d-pad works at once; coming back from a driver page still
returns focus to the driver box that opened it. ChoiceRow gains a
modifier for its box.
2026-09-23 16:33:16 -04:00
The412Banner 0cb18839a2 Driver pages: back returns to where you left the settings page
Backing out of the Runtime or Display driver page rebuilt the session's
settings page from the top with nothing focused. Its scroll position is
now held above the page (SettingsPage takes an optional ScrollState) and
survives the driver page, and controller focus returns to the driver box
that was pressed to open it (ValueChip takes a modifier for its
FocusRequester).
2026-09-23 16:33:16 -04:00
The412Banner 991e1af331 Driver page: controller navigation and focus throughout
- Every control shows a focus outline: the driver rows (the list at the
  top had none), their trash cans (in red), the refresh button, Restore
  built-in drivers, the confirmation's buttons, and the page's Back
  (shared by every settings page).
- D-pad up from the first driver moves to the refresh button beside the
  title; the page opens with the driver in use focused.
- Back (the controller's B too) closes the driver page only, back to the
  session's settings, instead of leaving settings altogether.
- The delete confirmation opens on Cancel, so a stray A press never
  deletes a driver.
2026-09-23 16:33:16 -04:00
The412Banner c8a278d009 Driver selection as a full page with refresh and delete for every driver
Runtime driver and Display driver open a full page instead of a
drop-down, built from the settings widgets (as previewed): INSTALLED
lists every driver as a tappable row with a radio mark and a tag
(BUNDLED / DOWNLOADED / IMPORTED); AVAILABLE TO DOWNLOAD lists what the
last check of Banners-Turnip and WinNative found, with a Download button
and progress; Import stays at the bottom. The refresh button sits beside
the title (SettingsPage gains an action slot) and is the only thing that
goes online; it spins while checking.

Every driver gets a trash can with a confirmation, the bundled ones too:
deleting a bundled display driver removes its unpacked copy and hides it
(TurnipDriver.hideBundled), with 'Restore built-in drivers' to bring
them back. It stays in the apk and Auto still unpacks it on a GPU that
needs it, so a deletion can never leave a device without a driver. Auto
and Runtime default are settings, not drivers, and have no trash can.
2026-09-23 16:33:16 -04:00
The412Banner 39f5478ea9 Driver menus: WinNative drivers, newest release per variant, scrolling menus
- WinNative-Emu/Drivers joins Banners-Turnip as a source: WN-Turnip
  (Android, v1.x line) and WN-Linux-Turnip (Linux, linux-v0.x line), each
  in its Balanced and Performance builds for every Adreno.
- Each source's recent releases are read newest first and every variant
  is offered from the newest release carrying it. Banners-Turnip stopped
  publishing its A8xx Linux build after v26.3.0-20260923, so the runtime
  menu listed no 8xx driver at all; it now offers that last one. The
  refresh line names each source's newest release and any that could not
  be reached.
- Drop-down menus are capped at the window's height and their entries
  scroll under the title: a driver menu with its downloads ran off the
  bottom of the screen, hiding the refresh line and Import.
2026-09-23 16:33:16 -04:00
The412Banner d0b1686a84 Driver menus: latest Banners-Turnip release as downloads, refresh button, trash-can delete
Both driver menus (Runtime and Display) now offer the latest release of
The412Banner/Banners-Turnip: each asset that fits the menu and is not
installed yet is a 'Download Turnip <tag>' entry labelled by GPU
(plain / -A8xx / -710-720-Test zips for the display driver, -Linux ones
for the runtime driver; -Wayland ones are never offered). Nothing goes
online on its own: a refresh line checks GitHub when tapped and says
which release it found and when; the result is remembered. A download
goes through the same importer as a picked zip and lands as an ordinary
driver.

Imported and downloaded drivers get a trash-can button with a
confirmation instead of the small cross. Deleting the driver a mode is
set to puts that mode back on its default (Runtime default / Auto), and
a deleted release download is offered again.
2026-09-23 16:33:16 -04:00
The412Banner 95bd8ec2fe Deck mode performance overlay: stage Valve's aarch64 mangoapp
The QAM offers Performance Overlay levels but nothing drew them: the
runtime has no mangoapp. CI now fetches Valve's mangohud-20260902.1 and
the five libraries mangoapp needs that the runtime lacks, at the versions
it was built against (fmt 10.2.0, spdlog 1.13.0, glfw 3.4, libtraceevent
1.8.2, libtracefs 1.8.0 from Valve's mash-20240705.2 snapshot), each
pinned by sha256 in tools/mangoapp/packages.txt. They are staged under
usr/local/lib/mangoapp with a wrapper on PATH; the runtime image is not
touched.
2026-09-23 16:33:16 -04:00
The412Banner 85b5151e1a Session: clear a killed session's leftovers, offer NIS in Deck mode, mangoapp hook
Before the client starts, remove .crash, steam.pid and the web helper's
htmlcache Singleton* locks when no client is running - Android ends
sessions without warning and those files leave the next start on the
update spinner or a blank Gamepad UI (SteamOS-ARM-SM8550's RUNSTEAM.sh
does the same). Deck mode exports STEAM_GAMESCOPE_NIS_SUPPORTED=1 so the
QAM's scaling filter lists NIS, and passes gamescope --mangoapp when
mangoapp is present (BL_MANGOAPP=0 turns it off).
2026-09-23 16:33:16 -04:00
Kurt Himebauch a0be13adba Merge pull request #19 from The412Banner/remove-shade-default-home 2026-09-23 16:11:53 -04:00
xXJsonDeruloXx a0fb8fd73d Remove Home app chooser from in-game shade 2026-09-23 16:06:49 -04:00
Kurt Himebauch d3e70b99c0 Merge pull request #18 from The412Banner/launcher-panel-refresh
Simplify launcher panels and manage compatibility tools in place
2026-09-23 16:03:26 -04:00
xXJsonDeruloXx 0a0ec6acf5 Simplify launcher panels and package management 2026-09-23 15:57:44 -04:00
Kurt Himebauch 50df100569 Merge pull request #13 from The412Banner/steamdeck-second-screen 2026-09-23 14:29:31 -04:00
xXJsonDeruloXx 02489e617c Merge remote-tracking branch 'origin/main' into fix/termux-system-runtime
# Conflicts:
#	app/src/main/java/com/steamdeck/launcher/SessionActivity.kt
#	app/src/main/java/com/steamdeck/launcher/ui/SessionOverlay.kt
2026-09-23 14:22:49 -04:00
Kurt Himebauch 2f22050add Merge pull request #17 from The412Banner/feat/android-home-launcher
Add Android home app integration
2026-09-23 14:17:56 -04:00
xXJsonDeruloXx 0081c8f7c7 Hide home setting from session drawer when inactive 2026-09-23 14:12:32 -04:00
xXJsonDeruloXx 12c1036f17 Add Android home app integration 2026-09-23 14:08:16 -04:00
xXJsonDeruloXx 7ef7129f7a fix: rebuild Termux PTY helper with the app NDK 2026-09-23 14:05:27 -04:00
The412Banner 5cb6346017 Merge pull request #15 from The412Banner/fix/play-without-runtime
Play on a fresh install: don't close the session screen before it installs the runtime
2026-09-23 13:49:47 -04:00
xXJsonDeruloXx 5e599e5445 ci: allow Android system libstdc++ 2026-09-23 13:48:23 -04:00
The412Banner 146c07a25e Play on a fresh install: the session screen no longer closes before it can install the runtime
SessionActivity finished at once when no runtime was installed - a guard from before the loading
screen could install it - so Play (and Desktop) opened a screen that closed before anything
happened. It now logs and carries on to installThenStart.
2026-09-23 13:38:35 -04:00
The412Banner 44265c3fcd Progress log: onboarding, Desktop & apps page, 4:3 display, PR #11 audio 2026-09-23 13:30:27 -04:00
The412Banner 90c7574e28 Merge pull request #14 from The412Banner/fix/exact-panel-shape
Display: Exactly this panel shape + custom resolution per mode
2026-09-23 13:27:13 -04:00
xXJsonDeruloXx e4fbb6f956 feat: add Steam second-screen controls 2026-09-23 13:23:49 -04:00
The412Banner ad2f9f8aa6 Custom resolution per mode, as in Bannerlator
Resolution gains Custom…: a width × height box (320×240 to 3840×2160, evened) with one-tap
presets for 4:3 (960×720, 1024×768, 1280×960), 16:10 (1280×800) and 16:9. A custom size replaces
the cap and the shape for that mode - the Shape row greys out and says so - and the compositor fits
it to the panel with bars where the shapes differ. Choosing any cap clears it. The device report
lists it for both modes.
2026-09-23 13:18:15 -04:00
The412Banner 9c470950ea Display shape: Exactly this panel, for 4:3 and 3:2 handhelds
The panel's shape was never allowed narrower than 16:9 (the foldable guard), so on a 4:3 panel
such as the Retroid Pocket's both choices gave 16:9 with bars. A third choice, Exactly this panel,
presents the panel's own aspect with no floor; the other two are unchanged and foldables still
default to 16:9. Offered in the mode settings and the in-session menu.
2026-09-23 13:14:36 -04:00
The412Banner 0ddb0be65a Merge pull request #12 from The412Banner/feat/play-installs-runtime
First-run onboarding: Play and Desktop install what they need; Desktop & apps as a page
2026-09-23 13:10:54 -04:00
The412Banner cbd3bc95a1 Desktop & apps is a page with a dropdown per tier, not a dialog
The package list opens in the front end's pane like the Settings pages. Each tier (Native ARM64,
ARM64 experimental, source builds) is a dropdown row with the rail's caret and an installed/total
pill; Native ARM64 starts open. Each package has its own Install/Remove button, and the one that is
installing shows its own step line and bar in its row (its tier opens when the install starts).
Leaving the page does not stop an install. The rail's Install emulators and Setup's Desktop & apps
highlight while the page is open.
2026-09-23 13:04:02 -04:00
The412Banner f43795714d Desktop installs the runtime and the desktop on a fresh install, on the loading screen
Desktop is enabled without a runtime or the desktop package. The session's loading screen installs
whatever is missing first - the runtime, then the desktop ("downloading the desktop · 212 of 426
MB", checking, unpacking) - and starts the session when both are in. A failure ends on the loading
screen with the reason. Desktop UI and Desktop now give the same non-Adreno warning as Play before
any download.
2026-09-23 12:53:36 -04:00
The412Banner 586c9c28b4 Play installs the Linux runtime on a fresh install, on the loading screen
Play and Desktop UI are enabled without a runtime. The session's loading screen downloads and
unpacks it first, reporting on its usual step line and bar ("downloading the Linux runtime · 332
of 791 MB", then checking, then unpacking), and starts the session when it is in. A failure ends
on the loading screen with the reason. Non-Adreno devices still get the warning before any
download. Setup > Linux runtime is unchanged.
2026-09-23 12:48:49 -04:00
The412Banner f79dc2da43 Merge pull request #11 from The412Banner/fix/directaudio-default
Audio defaults: classic client sink, DirectAudio for games and as a client option; mic on; Share latest logs
2026-09-23 12:20:07 -04:00
The412Banner 3bb485a437 Steam client audio: the classic sink by default, DirectAudio as a choice; audio diagnostics on
- The client's sound goes back to the adaptive AAudio sink 0.1.5 shipped (bundled unchanged as
  module-aaudio-classic-sink, sink name AAudioSink). Through the DirectAudio relay it stayed
  choppy on an AYN Thor (Android 13, 20 ms bursts) even with a two-burst buffer.
- Steam cog > Audio: DirectAudio for games (on by default, unchanged) and Steam client audio
  (Classic by default, or DirectAudio). The relay also starts when only the client wants it.
- The relay runs with --log and the daemon at info level, so audio.log carries the stream each
  path was granted and, on the relay, Android's xrun count every 1000 callbacks.
- Device report shows both settings.
2026-09-23 12:10:34 -04:00
The412Banner 8dd0043072 Audio bundle re-unpacks whenever its contents change; the DirectAudio sink logs the buffer it asked for and what the relay settled at
The unpack check compared a hand-bumped stamp only. Two sink changes shipped without a bump, so a
device that had run the earlier build kept the old module: the 40 ms ring floor and the 24 ms
device buffer never reached the AYN Thor they were for. The stamp now carries a CRC-32 of
pulseaudio.tzst. The sink's connect line names the buffer it asked for, and one line ten seconds
in reports the relay's device buffer, ring target and underruns.
2026-09-23 11:50:24 -04:00
The412Banner 4c43fe3502 DirectAudio sink: ask the relay for a 24 ms device buffer
The relay's default is 12 ms rounded up to whole bursts. On a device where Android grants a fast
stream (4 ms bursts) that is three bursts; on one where it grants a legacy stream (20 ms bursts,
an AYN Thor on Android 13) it is a single burst, and the Thor's client audio stuttered. 24 ms is
two bursts there and 24 ms on the fast devices. buffer_ms= overrides it.
2026-09-23 11:38:46 -04:00
The412Banner 92f7709a76 Audio: keep at least 40 ms queued for the DirectAudio relay; logs: Share latest logs
- module-directaudio-sink keeps the relay's ring at least 40 ms deep. The relay starts at 8 ms,
  which the producer inside proot missed while the client loads (two underruns on the FIT's fresh
  install, steady stutter reported on another phone).
- Setup > Session logs > Share latest logs zips the newest session folder, wherever it was written
  (Downloads, or app-private files when Downloads is not writable), and opens the share sheet via
  a FileProvider scoped to that zip.
2026-09-23 11:27:25 -04:00
The412Banner 04f40282d2 Fresh installs get DirectAudio and the microphone on; the old armhf sink leaves the audio bundle
- Microphone defaults to on, and the app asks for RECORD_AUDIO once at first launch, together with
  the storage prompt. A refusal is not asked again at start-up.
- modules/armhf held the inherited Winlator AAudio sink and two protocol modules; the app is
  arm64-only and nothing loads that folder. Removed, bundle stamp bumped so installs re-unpack.
2026-09-23 11:09:01 -04:00
The412Banner 4b1804847f DirectAudio on by default
Everyone who never touched the toggle gets it; anyone who turned it off keeps it off (the setting is
only stored when changed). Off, the client's sound came from AAudio driven inside proot, which
stutters on some devices.
2026-09-23 11:08:39 -04:00
Kurt Himebauch 4e791d486f Merge pull request #7 from The412Banner/feature/session-suspend-audio-recovery 2026-09-23 11:01:00 -04:00
xXJsonDeruloXx 9a8732fba4 Add session suspend modes and audio recovery 2026-09-23 10:51:10 -04:00
The412Banner adddbb4e50 Merge pull request #9 from The412Banner/feat/armada-and-lineage
Deck mode, added games with art, own gamescope and audio sinks, client sound via DirectAudio, Winlator lineage removed
2026-09-23 10:03:35 -04:00
The412Banner af4df4177d Merge remote-tracking branch 'origin/main' into feat/armada-and-lineage
# Conflicts:
#	app/src/main/java/com/steamdeck/launcher/ui/MainScreen.kt
#	app/src/main/java/com/steamdeck/launcher/ui/ModeSettingsDialog.kt
#	app/src/main/java/com/steamdeck/launcher/ui/PerformanceDialog.kt
2026-09-23 09:56:51 -04:00
Kurt Himebauch fa54c5c5a0 Merge pull request #10 from The412Banner/chore/frontend-cleanup 2026-09-23 09:33:00 -04:00
xXJsonDeruloXx 768e6c8511 chore: trim remaining frontend copy 2026-09-23 09:26:44 -04:00
The412Banner eb1b9e0d78 Rebase follow-up: main's copy of the credits and file manager with the Winlator lines removed; progress log entry for the branch 2026-09-23 09:13:12 -04:00
The412Banner 2f80d97e17 session: gamescope forces game windows to fill the screen in Steam mode (a game that shrank its window while the Steam menu was open came back in the top-left corner) 2026-09-23 09:12:39 -04:00
The412Banner d9246c333e audio: the sink is named DirectAudio when the relay carries the sound, so the client's Audio settings show which road is in use 2026-09-23 09:12:39 -04:00
The412Banner 164a6e009b settings: the DirectAudio toggle says it now carries the client's sound too 2026-09-23 09:12:39 -04:00
The412Banner 69aba7d0b7 audio: the client's sound through the DirectAudio relay - module-directaudio-sink feeds the relay's render ring
A second sink module: connects to the relay's socket (lazily, the relay comes up after the
daemon; a clocked null sink until then), sends the hello, maps the render ring it gets back and
keeps it topped up to the relay's target with a futex wait per burst. Every AAudio call, the
adaptive buffer and the route handling stay in the relay, outside proot. With DirectAudio on the
daemon loads this sink instead of module-aaudio-sink; the bundle carries both.
2026-09-23 09:12:39 -04:00
The412Banner 60f393f878 aaudio sink: open the stream at the device's own rate unless rate= is given (44.1 kHz got a legacy stream: 20 ms bursts, three of them); the priority refusal is debug noise 2026-09-23 09:12:39 -04:00
The412Banner a14451b5e9 aaudio sink: a 30 ms starting buffer, half-buffer writes, the IO thread at audio priority, and underrun counts in the log
Two bursts (8 ms) of buffer with a write per burst left the client's menu sounds choppy on
device: the daemon runs under proot, where every system call is traced. The buffer now starts at
four bursts or 30 ms, whichever is more, each write hands over half of it, the IO thread asks for
nice -16 like Android's own audio threads, and the log says how big the buffer is and how many
underruns the device counted.
2026-09-23 09:12:39 -04:00
The412Banner 946bbc3023 front end: the shelf is laid out whole so the pad reaches every tile (a lazy grid only composes the rows on screen, and a press towards the next row bounced back among the visible tiles); added games' store art lookup starts on resume 2026-09-23 09:12:38 -04:00
The412Banner 474e75f649 steam shortcuts: art the user set inside the client is never recorded as ours, so it survives when a game's own art goes away 2026-09-23 09:12:26 -04:00
The412Banner 3c65fd8d9e added games: artwork - the game folder's own images first, else Steam's store art for the title, into the client's grid
frontend/AddedGameArt: cover/poster/boxart/folder-name images, header, hero, logo and icon from
the game's folder (or its art subfolder); otherwise a one-time store search by folder name and
the capsule, header, hero and logo from Steam's CDN into the app's cache (a miss retried after a
week); an icon pulled from the .exe. The rail shows the portrait. The session's listing carries
the paths; bannerlator-steam-shortcuts copies them into userdata/<acct>/config/grid under the
client's names, never over a file it did not write itself, and sets the shortcut's icon. A toggle
in the cog turns the store lookup off.
2026-09-23 09:12:26 -04:00
The412Banner 066011c5fb added games: any number of Games folders from anywhere, each bound under /root/Games on its own
Folders are a list (the single folder from before carries over); each gets /root/Games/<name>,
with a short hash of its path only when two chosen folders share a name, so adding or removing
one never moves another's games (their appids follow the guest exe path). Per-game exe choices
are keyed by the folder's path now. The cog lists each folder with its game count and a Forget,
plus an Add row.
2026-09-23 09:12:26 -04:00
The412Banner e14fc55030 deck mode: jupiter-dock-updater --check answers 7 (up to date); exit 0 read as an update waiting and showed a Dock Firmware Update row 2026-09-23 09:12:25 -04:00
The412Banner d7b61258ce added games: bind the chosen Games folder into the session at /root/Games, so a folder on an SD card or anywhere else is seen (it was skipped unless it sat inside internal storage, the ROMs folder or the app's own SD library) 2026-09-23 09:12:25 -04:00
The412Banner ffd544c42a aaudio sink: the sink is AAudioSink, as the daemon config expects; acknowledge rewind requests before rendering (assertion on device) 2026-09-23 09:12:15 -04:00
The412Banner cb99269a6d audio: the APK build compiles the AAudio sink into the bundle; the committed bundle carries no sink module any more
The inherited module is gone from app/src/main/assets/pulseaudio.tzst (74 → 73 modules); CI adds
the app's own at build time and refuses a bundle that already has one. BUNDLE_STAMP bumped so an
installed app re-unpacks.
2026-09-23 09:12:15 -04:00
The412Banner 74b9ea9f89 aaudio sink: declare pa__done ahead of use; tolerate the int-conversion in PulseAudio's own atomic.h 2026-09-23 09:12:15 -04:00
The412Banner aa863662b8 audio: a fresh AAudio sink module for the daemon, built from upstream PulseAudio 13 headers
Blocking-write design: the IO thread pulls one burst at a time from the sink and hands it to a
blocking AAudio output stream, whose buffer paces the thread; latency comes from the stream's
playback timestamp; with adaptive=1 the buffer grows a burst per underrun up to a cap. Same
module arguments as before, so the daemon's config line is unchanged. build.sh compiles it with
the NDK against the app's own libpulse* and a small config.h; a workflow checks it on every push.
2026-09-23 09:12:15 -04:00
The412Banner 118e78ceb2 build: fetch the gamescope asset through the API (private repository), and key the preload cache on its pin 2026-09-23 09:12:15 -04:00
The412Banner b14cdf50e6 gamescope: the apk carries build gamescope-3.16.29-p1 2026-09-23 09:12:15 -04:00
The412Banner 39dd9b03f9 steam shortcuts: name the source loader for the suffix-less registrar so the added games get their Proton mapping 2026-09-23 09:12:15 -04:00
The412Banner 5d55258383 steam shortcuts: extend the buffer instead of rebinding it inside the nested writer (UnboundLocalError on device) 2026-09-23 09:12:15 -04:00
The412Banner 3256809066 gamescope build: apply the patches from the checkout Arch's prepare() leaves us in; concrete mirrors ahead of the redirector 2026-09-23 09:12:15 -04:00
The412Banner dfbb1e5b98 gamescope build: makepkg -A, the PKGBUILD names x86_64 only 2026-09-23 09:12:15 -04:00
The412Banner e9915c44f9 gamescope: the apk stages its patched build over /usr/local/bin when it carries one, the APK build fetches it from the release release.env names, and the session asks for realtime Vulkan queues 2026-09-23 09:12:15 -04:00
The412Banner a90ba862df gamescope build: pacman without its download sandbox inside the container 2026-09-23 09:12:15 -04:00
The412Banner b6ab624862 added games: plain null check instead of continue inside a lambda; gamescope build also runs on pushes touching its inputs 2026-09-23 09:12:15 -04:00
The412Banner f66138505f gamescope: a build of the runtime's 3.16.29 with Armada's ARM64 client fixes and a realtime-queue switch, from Arch's own package in CI
Four of Armada's patches apply to 3.16.29 as they are (appid-focus fallback, ARM64 night mode,
ARM64 colour-temperature slider, P3 red); two more are ported by hand: realtime-priority Vulkan
queues on GAMESCOPE_FORCE_VULKAN_REALTIME=1 without CAP_SYS_NICE, and the gamepad cursor sprite
following the X pointer. build-gamescope.yml builds Arch Linux ARM's package with them in an
Arch container, checks every NEEDED library against the runtime's own list, and publishes the
binary as a release asset for the apk to stage over /usr/local/bin/gamescope.
2026-09-23 09:12:15 -04:00
The412Banner dab91bdfac Added games: the user's own Windows games in the client's library
A Games folder chosen in the Steam cog is scanned one level deep, one subfolder per game; the
program to launch is the .exe named after the folder, else the largest, never an installer,
redistributable or crash reporter, and the settings page lets the user pick another for any
game. Each session the app writes the list, and bannerlator-steam-shortcuts puts the games into
the client's shortcuts.vdf as non-Steam games (the client's own entries kept) with the appid the
client itself derives, mapped to the ARM64 Proton through the compat registrar. They appear in
the rail under Steam and launch by steam://rungameid/ like any title.
2026-09-23 09:12:15 -04:00
The412Banner 101d155855 Deck mode takes the Steam Deck channel unless a branch was chosen (device: publicbeta + Deck mode reinstalls every start) 2026-09-23 09:12:14 -04:00
The412Banner 10d7b4905b Deck mode: the four helpers the client really calls, at the path it calls them
On device the client's console log showed the calls: /usr/bin/steamos-polkit-helpers/steamos-update,
steamos-select-branch, jupiter-biosupdate and jupiter-dock-updater - the polkit-helpers path, not
/usr/bin. The missing steamos-update there was the 'Update Error' dialog. Stubs at both paths now.

Also seen on device: -steamdeck alone opts the install into the steamdeck_stable branch too, so the
branch guard is what keeps a session from reinstalling; and on the publicbeta channel Deck mode
reports installed version 0 at every start and reinstalls the same client (rc=42, the launch URL
lost with it), while on steamdeck_publicbeta the second launch comes up with no restart at all.
2026-09-23 09:12:14 -04:00
The412Banner 3afad00a84 lineage: the pad bridge's hat and Quick Access chord lines, and the audio component's context, on the new names 2026-09-23 09:12:14 -04:00
The412Banner ab8b06bacd battery: time to full while charging, so the Quick Access Menu stops saying calculating 2026-09-23 09:12:14 -04:00
The412Banner 9cfb96e358 The last of the Winlator lineage rewritten out; rumble on the phone
The five files the audit measured as Winlator's are replaced: a one-method callback interface by
java.util.function.Consumer, the component base by SessionPart (attach an application context,
start, stop), the environment list by HostEnvironment, the process starter by HostProcess, and
the pad's state holder by PadState with named sticks, triggers, directions and buttons. The
frame-generation tree is framegen/ (its contents were never Winlator's: lsfg-vk's port and
Bannerlator's Win-FG), the log tags are the app's, and the fake pad's rumble socket is now one
the app actually serves: RumbleComponent turns a game's force-feedback effects into the phone's
vibrator, which used to go nowhere. The two design notes describing Bannerlator's X11 path are
gone, and the README and credits say what is whose.
2026-09-23 09:12:14 -04:00
The412Banner 2fb79735c3 Deck mode made safe, a client-branch choice, and a battery the client can see
Deck mode passes -steamdeck alone: -steamos3 opted the install into the steamdeck_stable
branch and started the restart loop Bannerlator hit on device (38996ea1). The SteamOS helpers
the client calls in that mode - the two Armada found it needs under steamos-polkit-helpers and
the three under /usr/bin - are staged from the apk as no-op stubs. The client branch on disk is
kept in step with the one forced on the command line, and an update that cannot install stops
the session after three restarts instead of looping for ever.

The Steam cog gets a Client branch row: public beta (every session so far) or Steam Deck
public beta, the channel Armada bootstraps from.

The client reads /sys/class/power_supply/BAT<n>/; Android's supply is called battery and laid
out differently, so the Quick Access Menu had no battery. BatteryComponent writes BAT0 and BAT1
from Android's battery API every five seconds - status, percentage, charge, energy, power and
time to empty - into a directory bound over /sys/class/power_supply.
2026-09-23 09:11:54 -04:00
Kurt Himebauch a4e59986a8 Merge pull request #8 from The412Banner/chore/cleanup
chore: simplify UI copy and repository text
2026-09-23 08:32:51 -04:00
xXJsonDeruloXx 93b4ec47d1 docs: move progress log under docs 2026-09-23 08:27:09 -04:00
xXJsonDeruloXx a4b1d12f36 Merge remote-tracking branch 'origin/main' into chore/cleanup
# Conflicts:
#	README.md
2026-09-23 08:25:15 -04:00
xXJsonDeruloXx 217485e021 chore: remove lineage audit document 2026-09-23 08:23:55 -04:00
github-actions[bot] 1f41d6d282 README: contributions ledger 2026-09-23 12:23:14 +00:00
xXJsonDeruloXx 6876a8d649 chore: remove generated contributor ledger 2026-09-23 08:22:53 -04:00
xXJsonDeruloXx 02c159c5fd chore: simplify UI copy and repo text 2026-09-23 08:22:11 -04:00
github-actions[bot] a1e204c117 README: contributions ledger 2026-09-23 11:09:36 +00:00
The412Banner ece9f7cd46 docs: lineage audit — inherited from Winlator vs written fresh in the forks; the AAudio sink is the one inherited piece that does real work 2026-09-22 23:58:25 -04:00
The412Banner 16dab991e0 docs: the lineage audit — what is measurably Winlator's, what only carries the name, and what leaving it would take 2026-09-22 23:54:07 -04:00
github-actions[bot] c17975943a README: contributions ledger 2026-09-23 03:39:33 +00:00
Kurt Himebauch 9d9ee98eaf Merge pull request #6 from The412Banner/steam-qam-buttons 2026-09-22 23:39:23 -04:00
The412Banner b0bcac6926 PROGRESS_LOG: after the 0.1.5 cut, and what comes next 2026-09-22 23:32:34 -04:00
xXJsonDeruloXx 3b7ad53994 Start drawer QAM chord on button press 2026-09-22 23:26:13 -04:00
The412Banner 0fedfae1e6 PROGRESS_LOG: 0.1.5 released 2026-09-22 23:18:08 -04:00
The412Banner 3fa2a35c12 0.1.5 notes 2026-09-22 23:17:28 -04:00
xXJsonDeruloXx a39859a39e Move Steam shortcuts to the drawer header 2026-09-22 23:17:14 -04:00
The412Banner d2f4a84314 0.1.5 2026-09-22 23:13:14 -04:00
xXJsonDeruloXx 0959eacf39 Add QAM shortcut to the session drawer 2026-09-22 23:10:44 -04:00
github-actions[bot] 78c4b3b264 README: contributions ledger 2026-09-23 03:06:58 +00:00
xXJsonDeruloXx 9f4a841eb6 Add Steam and QAM touch controls 2026-09-22 23:05:29 -04:00
The412Banner 9b034fcbb2 Merge remote-tracking branch 'origin/main' into feat/frontend-motion 2026-09-22 23:02:38 -04:00
The412Banner 81cf964168 front end: every category starts folded when the app opens 2026-09-22 22:55:38 -04:00
The412Banner b5158210a9 themes: every screen colour comes from the palette; Setup › Theme switches at once 2026-09-22 22:49:21 -04:00
github-actions[bot] d1602710a8 README: contributions ledger 2026-09-23 02:44:16 +00:00
The412Banner ec1453831d themes: the icon's colours three ways, chosen under Setup › Theme, Paper on black by default
Palette (ui/Theme.kt): background, surfaces, lines, type, a primary with its gradient partner,
and a separate signal colour for rules, rings, dots and the eyebrow - the blue in every theme,
while the primary is white in Paper and blue in the other two. Every colour the screens used to
hard-code (the purple gradient end, the green, the two line greys) now comes from the palette,
so a change under Setup applies at once, and the session drawer and the file manager pick the
saved theme up when they open.
2026-09-22 22:43:55 -04:00
github-actions[bot] c63af629ac README: contributions ledger 2026-09-23 02:43:13 +00:00
Kurt Himebauch 916b1d46fc Merge pull request #5 from The412Banner/build/local-build-deploy 2026-09-22 22:42:59 -04:00
github-actions[bot] 7df694fe49 README: contributions ledger 2026-09-23 02:32:10 +00:00
xXJsonDeruloXx 071289b56e Add reproducible local build and deploy helpers 2026-09-22 22:31:10 -04:00
The412Banner eca355016c logo: the app's in-app mark is the new launcher icon (its two layers composed and cut to the launcher's circle), in the rail and the loading screen 2026-09-22 22:26:14 -04:00
github-actions[bot] 0053d12407 README: contributions ledger 2026-09-23 02:16:54 +00:00
Kurt Himebauch 4d6ee985b9 Merge pull request #4 from The412Banner/update-app-icon 2026-09-22 22:16:47 -04:00
github-actions[bot] 94d74d35db README: contributions ledger 2026-09-23 02:15:33 +00:00
xXJsonDeruloXx d6a157578d Update app launcher icon 2026-09-22 22:15:06 -04:00
The412Banner 0b9f3106fe gitignore: the Kotlin compiler's session files 2026-09-22 21:54:33 -04:00
The412Banner 17c75bf671 PROGRESS_LOG: pull-request CI and the contributions ledger 2026-09-22 21:46:47 -04:00
github-actions[bot] d6a9c2ed90 README: contributions ledger 2026-09-23 01:44:05 +00:00
Kurt Himebauch 9931dc13a7 Merge pull request #3 from The412Banner/fix/session-back-key-routing 2026-09-22 21:43:54 -04:00
github-actions[bot] 4a3d5a7d66 README: contributions ledger 2026-09-23 01:40:36 +00:00
The412Banner 38df1e8141 CI: full history for the pull-request merge (the depth expression came out shallow), and git's own words when a merge cannot start 2026-09-22 21:40:22 -04:00
xXJsonDeruloXx ac2cfc1266 Fix session hardware back routing 2026-09-22 21:39:54 -04:00
The412Banner e228eca65e CI: pull requests build on pull_request_target (a conflicting one never fires pull_request); README: contributors and a ledger of their pull requests kept by a workflow 2026-09-22 21:38:40 -04:00
The412Banner 6eddda9808 CI: the pull-request report — mergeable or the conflicting hunks, and a failed build's errors, as one comment kept current
A pull request is checked out at its own head and merged onto main by the job itself: clean,
and the merged result is what gets built; conflicting, and the files and every conflicting hunk
go into the run summary and the comment, and nothing is built. The Gradle log is kept, and a
failed build's compiler errors (paths shortened) and Gradle's own 'What went wrong' are quoted
back. One comment per pull request, rewritten on every push; fork pull requests, whose token is
read-only, get the same text in the run summary.
2026-09-22 21:23:53 -04:00
The412Banner 45ca3c95c1 CI: a pull request against main builds itself (Bannerlator's PR check, in this one workflow)
Every push to the pull request rebuilds it and cancels the build it replaces (the concurrency
group is already per ref, and a pull request's ref is its own); a draft waits until it is
marked ready. The apk comes back as the run's artifact.
2026-09-22 21:19:41 -04:00
The412Banner e0dd8b8c59 on-screen controls: two sticks beside the d-pad, and the controls take the bars beside or under the picture
A left and a right stick (a base and a knob that follows the finger, clamped) write the pad's
thumb axes the way a physical stick does, and only while they own them, so a physical pad's
sticks are not zeroed by a tap on A. The layout follows where the compositor letterboxes the
picture: on a wide panel showing 16:9 the clusters go in the side bars, on a squarer panel
showing a wide picture they go in the bottom band, and only when neither bar is wide enough do
they overlay the picture as before. The activity hands the picture rectangle over whenever the
surface or the output size changes.
2026-09-22 20:41:14 -04:00
The412Banner 95cd642745 tonight's Bannerlator Linux fixes carried over: the client's GPU process gets its frames back, client-interface switches, Deck mode, a GPU gate before the download
- libpci: an empty file bound over /proc/bus/pci/devices, created at session start, so
  Chromium's GPU process stops dying on its way up and CEF stops falling back to SwiftShader
  (WinNative deff1ac6 via Bannerlator 8fb668d1: 44 -> 85 fps in the Big Picture library on a
  OnePlus 15; not measured here yet).
- Client interface, as toggles on the Performance page and lines in the device report:
  mesa_glthread, ZINK_DESCRIPTORS=lazy (now on by default) and MESA_NO_ERROR on; Steam Deck
  mode (-steamdeck -steamos3, the script builds the line) off.
- A non-Adreno device is told before the runtime download that a session would be sound over a
  black screen, and may still go ahead.

Not carried: Bannerlator's proot fixes live in its proot source, which this repo does not
build (the runtime tarball ships its own); its apk-proot -i guard is moot here (the apk carries
no proot); its session-log head keep is moot (Steam's logs are copied whole).
2026-09-22 20:12:54 -04:00
The412Banner 96d453e370 PROGRESS_LOG: r7 on main, r8 drawer + FEX presets 2026-09-22 18:51:32 -04:00
The412Banner 90ff23aea4 session: back key no longer knows the frame-generation dialog 2026-09-22 18:46:47 -04:00
The412Banner 63cde17bf4 session drawer in the front end's dress + FEX presets + landscape file manager
The drawer Back opens over a session slides in from the right and uses the same rows and
anchored menus as the settings pages: Now (HUD, frame generation, touch, the virtual pad,
keyboard, the client's menu) and Next session (display shape, FEX preset, compatibility tools),
with Send to background and Stop at the end. Steam-only rows show only in a Steam session.

FEXCore presets, Bannerlator's table carried over (core/FexPreset): a Games row in the Steam
settings page and the drawer; the chosen preset's FEX_* variables go into the Steam session's
environment so the games the client launches inherit them. Default = FEX's own defaults, which
is what every session ran on before.

The file manager and the picker stay in landscape like the rest of the app.
2026-09-22 18:44:12 -04:00
The412Banner af3f377dfd front end: Setup rows join the rail's tracked column (the pill can reach Performance); the session drawer keeps its frame-generation dialog 2026-09-22 18:31:13 -04:00
The412Banner 8dd36b794e settings without a pop-up: pages in the pane with anchored menus under each value; frame generation, logs and offline open in place on the rail
The cog's settings window and the Performance dialog become pages in the front end's pane:
one row per setting, its value in a chip, and a small faded menu that opens under the chip
(SettingsWidgets: AnchoredMenu on a Popup with its own position provider, MenuItem, ValueChip,
Choice/Toggle/Multi/Action rows). One menu at a time; the page dims while it is open; the back
key, the rail and the page's Back all leave it. Steam and Desktop each get a Settings entry in
the rail that lights while its page shows, as does Performance under Setup. Frame generation,
Session logs and Start offline open their menu right on their rail row; the frame-generation
dialog is gone.
2026-09-22 18:28:33 -04:00
The412Banner 39272cf0f2 front end: Setup folds like Steam and Desktop (open until the runtime is installed); the desktop page says 'Linux desktop environment'; the emulator help link moves above the grid it was hiding 2026-09-22 18:10:19 -04:00
The412Banner 1ba803b1d5 session: the guest gets the device's time zone (the client's clock and logs were UTC) 2026-09-22 18:05:27 -04:00
The412Banner dab34d1ba9 PROGRESS_LOG: 720p default, builds r3/r4 2026-09-22 17:57:37 -04:00
The412Banner fb2a3d4dd8 session: the desktop caps at 720p by default too 2026-09-22 17:53:43 -04:00
The412Banner d1ced8706f session: the Steam client's display caps at 720p by default (desktop stays 1080); a chosen cap still wins 2026-09-22 17:49:29 -04:00
The412Banner 5d8fadec65 PROGRESS_LOG: motion front end + run-as-a-game builds r1/r2 2026-09-22 17:45:05 -04:00
The412Banner 546c063b0c game_mode_config: the attribute is allowGameDownscaling 2026-09-22 17:40:43 -04:00
The412Banner 06e67cd7d5 session: run as a game — game category + game_mode_config, sustained performance mode, fastest display mode, ADPF hint session on the compositor thread, 16 KB-aligned libraries
The manifest now declares the app a game (appCategory, isGame) with a game_mode_config that
welcomes Performance mode and refuses the OS-side FPS cap and resolution downgrade — the
interventions that make a regular flagship's Big Picture crawl while a gaming phone's does not.
The session window asks for sustained performance mode and the panel's fastest mode at its size,
tells GameManager it is in gameplay, and opens a PerformanceHintManager session over the
compositor thread fed with each presented frame's interval against the panel's period. All
version-guarded (6/7/12/13); the session log carries one 'perf:' line saying what took.

The app's own libraries link with 16 KB-aligned LOAD segments (loads unchanged on 4 KB kernels).
Six prebuilt PulseAudio libraries under jniLibs are still 4 KB-only and need a rebuild.
2026-09-22 17:39:04 -04:00
The412Banner 7ddfb57610 front end: qualify the play badge's AnimatedVisibility (the Column scope stole the overload) 2026-09-22 17:30:03 -04:00
The412Banner 19514c32b8 front end: motion system — gliding rail selection, unfolding sub-lists, page cascades, lifting tiles, session rise
The rail's selection is one pill that springs between rows; sub-lists unfold with
their children staggering in. A page change sinks the old page out and cascades the
new one in row by row over a blurred wash of the selection's own art. Tiles lift,
ring and shine when the pad lands on them and pop a play badge; the launch button
sweeps a sheen on focus and squashes on press. The session activity rises over the
front end and sinks back on finish. Every duration follows the system animator
scale, so 'no animations' in developer options snaps everything.
2026-09-22 17:27:25 -04:00
The412Banner d343b6f859 PROGRESS_LOG: checkpoint after the front-end merge 2026-09-22 14:52:13 -04:00
The412Banner 36cde7d1af docs: the front end and the first emulator game proven (README ledger, PROGRESS_LOG) 2026-09-22 13:32:57 -04:00
The412Banner 703f73610f The game's arguments reach the run-mode session
The activity forwarded mode, program and the Steam extras to the service and dropped
EXTRA_PROGRAM_ARGS, so RPCS3 opened with no game. device.txt names run mode too.
2026-09-22 13:23:45 -04:00
The412Banner 496ab5787a The game's arguments reach the run-mode session
The activity forwarded mode, program and the Steam extras to the service and dropped
EXTRA_PROGRAM_ARGS, so RPCS3 opened with no game. device.txt names run mode too.
2026-09-22 13:22:47 -04:00
The412Banner 7645db94d1 Front end: emulator tiles are square 2026-09-22 12:41:37 -04:00
The412Banner 6462b9b4d6 Front end: PS3 games as they really are on disk, and RPCS3's installed games
The scan goes one folder deeper (a dump comes as a folder named for the game with the image
inside), treats a folder with PS3_GAME as a disc, drops the ".dec" suffix, and lists what RPCS3
has installed on its own HDD (dev_hdd0/game/<ID> with a PARAM.SFO title, ICON0 art and an EBOOT
to boot) - a PSN package installed through RPCS3 is not in the ROMs folder at all.
2026-09-22 12:20:17 -04:00
The412Banner 4836558658 Front end: focus outline for pad navigation, art 25% larger, each emulator's own icon
The emulators' icons are bundled (the runtime keeps them as Papirus SVGs the app cannot draw):
the projects' own from their repositories, PPSSPP's from the runtime, RetroArch's flat icon
from libretro's assets. ROM tiles carry their emulator's icon.
2026-09-22 12:05:05 -04:00
The412Banner 78a7629eaa Front end: game art at 40% of the first cut 2026-09-22 11:52:16 -04:00
The412Banner 8581adcc46 Front end, slice 1: a rail of Steam and its games, the desktop and its emulators and their games
FrontEndScreen replaces the main screen on this branch. Steam's installed games come from the
client's own appmanifests (both libraries, with the library-cache art) and launch straight
into a Steam session with steam://rungameid; the emulators list their games from the ROMs
folder (a system folder as a hint, by extension) and launch under gamescope with the game on
the command line (EXTRA_PROGRAM_ARGS). A session running in the background is the first thing
on the rail and tapping it goes back to it. Landscape: rail beside content; narrow: above.
Every dialog and setting from the main screen is reachable from the rail's lower list.
2026-09-22 11:30:47 -04:00
The412Banner d39fd02d04 An ending session takes its log folder with it; the session replacing it gets its own
Handing off from the desktop, the new session claimed the folder the desktop was still
finishing, and the desktop's collector then stopped the new session's app.log capture. The
folder is taken from the shared slot as the session stops and handed to its collector; the
next session's beginOrCurrent finds the slot empty and makes a new folder.
2026-09-22 11:24:05 -04:00
The412Banner 68b09c1ed1 The session's number is claimed when it is registered, not when its process starts
The replaced desktop's proot exits on SIGTERM within a tenth of a second - before the new
session's start thread had numbered itself - so its exit still matched the current number and
ended the new session (4 s on the ended card, then the main menu, with the client audible in
the background). The number is taken in onStartCommand now, ahead of any thread.
2026-09-22 11:22:31 -04:00
The412Banner 74b4c2ef09 A replaced session's exit no longer ends the one that replaced it
Hand-off from the desktop: the new session started, and a second later the old proot - killed
by its own teardown - reported its exit, which the service took as the current session ending
("The session ended 137"). Exits are matched to the session that started the process. The same
race took the new session's log capture and its folder (stopFor / release by folder), and a
finishing activity unhooked the replacement's end listener (compared by identity now).
2026-09-22 11:15:35 -04:00
The412Banner 593ef8fcaf A finishing activity no longer takes the next one's Surface; the library label is environment, not an argument
Hand-off from the desktop to a Steam session: the new activity attached its Surface, then the
old one's surfaceDestroyed detached it - the compositor presented nothing while gamescope sent
frames by the thousand (black screen, sound only). detach() now takes the Surface being destroyed
and ignores one that is not attached. BL_LIBRARY_LABEL was added to the guest list after the
script's mode and reached the Steam client as an argument; it is an environment variable now.
2026-09-22 11:01:58 -04:00
The412Banner b28e91128f The desktop uses the client's own game entries; a steam shim hands them to the app
The client already writes one menu entry per installed game (steam steam://rungameid/<id>, with
the game's icon); they were dead for want of a steam command, and r45 added a second entry per
game beside them. Now: steam on the desktop is a shim that hands off to bannerlator-steam-launch
(and is the client itself anywhere else), and bannerlator-desktop-games writes an entry only for
a game the client wrote none for.
2026-09-22 10:48:26 -04:00
The412Banner d0ca41d160 The client's games on the desktop, launching Steam's desktop UI under gamescope
bannerlator-desktop-games writes one menu entry per installed game (both libraries, the
client's own artwork) and links Steam Games / SD Games in the home folder. The entries, and the
menu's Steam / Big Picture ones, run bannerlator-steam-launch: the client cannot draw on the
desktop (labwc, no dma-buf), so it leaves a request in the session directory; the service
watches it (FileObserver), ends the desktop session and hands the activity a Steam session to
start - desktop UI (BL_STEAM_UI=desktop drops -gamepadui) with the game's rungameid URL.
2026-09-22 09:46:19 -04:00
The412Banner 1374091d1b Game storage is automatic: a card in the phone is a Steam library by itself
The choice belongs inside the client - the drive drop-down on Install, Settings > Storage - so
the app no longer needs a setting for the common case. Default = automatic (the first SD card
present); the cog keeps Internal only and a chosen folder.
2026-09-22 09:41:58 -04:00
The412Banner fa25451766 Game storage: a second Steam library on an SD card or a chosen folder
The runtime already knew how to register /mnt/bannerlator-sd with the client; the app never
bound anything there, so the client only ever saw internal storage. The Steam cog's Game storage
section offers every removable volume (through the app's own folder on it, the one place a card
lets a targetSdk-28 app write) and any folder from the File Manager, proven writable first. The
session binds the choice and names it (BL_LIBRARY_LABEL); bannerlator-steam-library registers it,
or forgets it when nothing is bound. The client does the rest: asks where to install, shows both
on Storage, moves games between them. device.txt records the choice.
2026-09-22 08:18:20 -04:00
The412Banner 2cbd015d80 Sysmem says it costs frames; a session that died in the ENOSYS storm says which switch answers it
Two field bundles: an S25 running with Turnip sysmem on (it turns tiled rendering off - a
corruption fix, not a speed setting, and the text did not say so), and a Fold 5 ending, again,
in dozens of 'Function not implemented' lines from proot's seccomp acceleration failing Steam's
xalia helper. The ended-session message now reads the log's tail off the main thread and, when
it finds that storch, names 'Run proot without seccomp' / 'Skip xalia' in Performance.
2026-09-22 06:41:13 -04:00
The412Banner 501786dc08 Main screen: the ROMs folder is a chip beside the emulator ▶ buttons; Session logs takes the last row 2026-09-22 05:42:48 -04:00
The412Banner eb8d6b76c2 Main screen: a ▶ chip per installed emulator under Desktop, and a ? that explains them
One chip per emulator the desktop packages installed, starting it fullscreen under gamescope;
the ? says what they are, how to use them (ROMs at /root/ROMs, the phone at /root/Storage) and
why they are here rather than on the desktop (labwc on pixman offers no dma-buf).
2026-09-22 05:35:18 -04:00
The412Banner 785bf23087 Emulators run under gamescope (▶ in Desktop & apps); the device's volume keys reach Android
RPCS3 on the desktop died at its first frame with VK_ERROR_SURFACE_LOST: labwc composites in
software (pixman) and initialises no dma-buf, so no Vulkan program on the desktop can get a
swapchain. The GPU path that works is gamescope's, which Steam's games already render through.
MODE_RUN starts a program inside the runtime as a session of its own under gamescope (the
script's run mode: AppImage, script or binary), taking Steam's display/driver/HDR settings; the
apps dialog offers ▶ for every installed emulator (AppImages, PPSSPP, RetroArch).

Volume Up/Down/Mute were forwarded to the guest as keys and never reached Android, so the
device's volume never changed during a session; they now fall through, on the media stream.
2026-09-22 05:17:21 -04:00
The412Banner 903c83718a docs: PROGRESS_LOG (state, pipeline, 0.1→0.1.4 timeline, lessons, backlog) + the 0.1.1–0.1.4 release notes under docs/releases 2026-09-22 04:37:16 -04:00
The412Banner 8e58e8e3eb 0.1.4 2026-09-21 23:21:19 -04:00
The412Banner 6ae860045c The mode's cog is a settings window: a rail of sections on the left, one section on the right
Display, HDR, Runtime driver, Display driver, Touch & controls, Audio (Steam) / Renderer
(desktop), each found by name instead of by scrolling past the others.
2026-09-21 23:15:39 -04:00
The412Banner ecd0aecbc5 Fix the build: the rail's prefs without androidx.preference, the Description icon import, Guide index as Int 2026-09-21 23:07:16 -04:00
The412Banner 9ad922348b Bannerlator's File Manager, a cog per launch mode with HDR10, leftover-process sweep, the drawer's Steam menu
The file picker is now Bannerlator's File Manager itself (files/FileManagerScreen.kt, carried over
whole with its rail, favourites, grid/list, copy/cut/paste, rename, delete, properties) minus what
has no meaning without a Wine container: run, add to shortcuts, C:/Z: drives, the archive
unpacker. StorageRoots, CollapsibleRail, FavoritesStore, PeIconExtractor, OutlinedAlertDialog and
MenuStyle come with it unchanged; FileOps carries the progress copy/move. A Files tile opens it;
its pick mode chooses the ROMs folder and imports drivers.

Play and Desktop each get a cog: resolution cap (1080p default, 900p, 720p, panel), shape, HDR10,
the mode's Linux runtime driver, the display driver, touch, and per mode the on-screen controls +
audio (Steam) or the renderer (desktop). The Graphics drivers and Audio tiles are gone into them.

HDR10: HdrSupport reads the panel's HDR types; the switch is greyed with the reason unless HDR10 is
listed. On, the activity feeds the compositor's gate (nativeSetHdrDisplay + nativeSetHdrRequest),
turns zero-copy on, sets BANNER_WAYLAND_HDR=1, and the session gets BL_HDR=1 (gamescope
--hdr-enabled) and DXVK_HDR=1.

OrphanReaper kills every process of our uid but the app at session start and on an app crash: a
tree left by a killed app held the rootfs, the GPU and Steam's lock, and the next session never
came up (Thor Pro report). The drawer gains Steam menu, which sends the Guide button.
2026-09-21 23:03:57 -04:00
The412Banner cb5a1b5a31 ROMs folder and Storage in the session's home, the app's own file picker, logs finished on a crash, a two-column menu
A program's file dialog inside the session never showed the phone: internal storage was bound
at its own path, but proot binds do not appear in /proc/mounts and nothing under home pointed at
it. Every session now binds internal storage at /root/Storage and the folder chosen with the new
ROMs folder tile at /root/ROMs - a bind, so a folder on an SD card works the same.

Folder and file choosing is the app's own picker (files/), Bannerlator's File Manager pick mode
carried over: StorageRoots verbatim for the drive menu, the same intent API (InAppFilePicker),
folders first, an up-arrow floored at the volume root, the last folder remembered. Both driver
imports use it instead of the system document picker.

A session folder is finished whichever way the session ends: SessionArtifacts is shared by the
ordinary stop, an uncaught-exception handler (App/CrashHandler, writes crash-app.txt and finishes
the folder before the process dies) and a sweep at the next app start for a folder without its
.complete marker (a kill or a native crash), which gets ended-without-teardown.txt and the crash
buffer. A Session logs tile turns the folder off: it then lives in the cache and is deleted at
the end.

The main menu is two columns of tiles, each with its setting's value under its title.
2026-09-21 22:43:56 -04:00
The412Banner 3aa0162055 Answer for NetworkManager, so the client's network page works
The Steam client builds Settings > Internet and its connection indicator on
NetworkManager and looks for one on the system bus as it starts. This runtime
had neither, so the page stopped with "StartScanningForNetworks is not a
function" under a header that showed no connection while downloads ran. A user
on an Odin 2 reported exactly that, and I told him it was cosmetic and unfixed -
wrongly: Max wrote the answer for WinNative (e7a224ae) and Bannerlator has
carried his port for days. This app was the only one without it.

The session now starts a small system bus and his stand-in, which reports one
connection that is up while Android has a route out and refuses what it cannot
do, since nothing here can scan for or join a network. Both the script and the
bus config come from the apk at every launch, so an installed runtime gets this
without a re-host; dbus-daemon and python-gi are already in r9.

Named bannerlator-netmanager rather than steamdeck-: CI copies
overlay/usr/local/bin/bannerlator-* by glob, so anything else would have been
built, committed and never packaged - the same shape as the proot artifact that
was produced and discarded on every run for a week.

Version 0.1.3, and the README gains a section for it.
2026-09-21 21:35:51 -04:00
The412Banner 1f0292c8e3 Version 0.1.2, and a README that matches the app
The README had grown a section per feature in the order they were written, and
the parts that describe the whole had fallen behind: it still said version 0.1,
the ledger listed none of the drivers, audio, cores, session fixes or logs, and
the architecture diagram showed neither the audio relay nor that there are two
Turnips with two import lists.

Now: the ledger covers everything the app does with an honest state against
each - and says outright where something sets what it claims but has never been
measured; the diagram shows both driver slots and where a game's audio can go;
the session-log folder has a section of its own instead of being an appendix to
the device switches; and Installing mentions the one Android setting that
decides whether a session stays up at all, since that has cost two people an
afternoon.
2026-09-21 21:01:38 -04:00
The412Banner ec14fa1785 Collect a session's logs off the main thread, and wait for logcat properly
Leaving the desktop hung the app until Android ANR'd it, and the log I had just
added is what showed why: stopSession runs on the main thread - the
notification's Stop action arrives there - and it called the collector
synchronously. That meant copying the compositor's log, scrubbing every Steam
log line by line (42 files on the measured run) and then waiting on logcat, all
on the main thread: about four and a half seconds, ending in signal 3 and
stack traces written to tombstoned.

    20:45:32.721  session ended: 137
    20:45:34.280  collected 42 Steam log(s), scrubbed          <- main thread
    20:45:34.282  started logcat -b crash                      <- main thread
    20:45:36.537  reacting to signal 3 ... Wrote stack traces

The collector now runs on its own thread. Nothing in it touches the service's
state after the components have stopped, and the app process outlives the
service, so there is nothing to race.

The crash dump was also wrong twice over: it polled for the output being
non-empty, but the lines arrive through a callback on another thread, so it
declared the buffer empty every time and still paid the wait; and it printed
exec()'s return value as "rc" when that is a pid, never an exit status. It now
waits on the process ending, with a real timeout, and says which of the three
things happened - could not start, did not finish, or genuinely nothing has
crashed.

Nothing else in that session had gone wrong: labwc and Firefox's shutdown noise
is ordinary, and proot needing SIGKILL on the desktop path is expected.
2026-09-21 20:54:20 -04:00
The412Banner e69dfbd5e4 Compositor log: frame-size changes, and a window being renamed
Two events the log was silent about, both of which a report needed this week.

A window that changes the size of the frames it commits has rebuilt its
swapchain, and that is what a game suddenly filling one corner of the screen
looks like from the compositor's side: FlatOut came back from the Steam menu in
a smaller swapchain and the log said nothing at all, so the size had to be
inferred from a screenshot. It now says both sizes on the line.

And a window already on screen being renamed is something else taking it over:
gamescope keeps one toplevel and retitles it as the game takes the screen. That
was silent, which left the log showing a window "closed" under a name it had
never been logged as opening - reading like a missing event when it was really
a rename. Both are rare by nature; there is nothing to rate-limit.
2026-09-21 16:11:20 -04:00
The412Banner 2570562792 Version 0.1.1 (versionCode 2) 2026-09-21 13:52:43 -04:00
The412Banner 4041849791 Logs: the app's own log, the crash buffer, audio and the network
Closes the four gaps that were left after the session folder: everything the
app itself decided was reaching logcat and nowhere a user can get at, sound had
no record at all, the network had none either, and a session the system killed
left its only trace in a buffer nobody was reading.

app.log mirrors this process's logcat for the length of the session - which
driver was chosen and why, the audio line, a rival client stopped, a helper
missing, the exit status. An app may always read back its own entries, so this
needs no permission; where a ROM refuses, the file says so rather than being
absent and looking like silence.

crash.log is Android's crash buffer at teardown. proot once died before main
over a missing library and said so there and in no other log, and a session the
OS kills for spawning children says it there too.

audio.log takes the PulseAudio daemon's output and the relay helper's, both of
which went to logcat only. The 17.0-module failure that cost the microphone a
whole round of builds would have been one line in this file.

network.txt records the transport, whether the link validated, the address
families, the DNS the system offered and the resolv.conf the runtime was
actually given - never the network's name, which is identifying. It also states
plainly that the client's own "wifi is off" is cosmetic, since that reading has
now cost two people an afternoon.

Left out on purpose: a Vulkan dump. It needs a new native entry point for
information wayland.log already carries on its first lines.
2026-09-21 13:09:23 -04:00
The412Banner 0dcbf64aa5 Fix the quote that broke the build
The proot switch's description quoted the error it is for, and the quotes went
in unescaped - the Kotlin string ended mid-sentence at "Function".
2026-09-21 12:59:35 -04:00
The412Banner aa047ce942 Logs: one folder per session, a device report, and credentials scrubbed
A session used to leave a loose session-<stamp>.log in Download/SteamDeck and
a compositor log in a different folder entirely, with Steam's own logs copied
beside the first one only if the session exited cleanly - which the sessions
worth reading never do. Reports arrived needing three questions answered
before anything could be read: what device, which drivers, which settings.

Now each session claims Download/SteamDeck/session-<stamp>/ and everything
lands in it: device.txt, session.log, wayland.log, steam.log plus steam/ in
Steam mode, desktop.log in desktop mode. The folder is claimed by whoever
starts first, since the activity starts the compositor before the service
starts the session, and the compositor is told where to write through its
environment - it opens that file once, so the service also copies it in at
teardown, which covers the second and later sessions of one process.

device.txt is written before the session starts, so a session that dies in its
first second still says what it ran on: model, SoC, Android, kernel, cores and
ceilings, RAM, the GPU as KGSL names it, the panel, the driver each half of the
session chose, the runtime version, every switch in effect, and whether Android
is set to kill the session's child processes.

Nothing identifying is collected - no serial, no device or advertising id, no
account name, no network names - and Steam's logs are scrubbed line by line on
the way in, with Bannerlator's patterns: session tokens (including the bare
numeric "Using JWT" form), machine-auth GUIDs, WebAPI keys, Guard codes,
emails, and SteamIDs masked to their last four digits so lines still correlate.
loginusers.vdf, config.vdf and the ssfn files are never copied.

Also from Max: a switch to run proot without its seccomp acceleration
(PROOT_NO_SECCOMP=1), for kernels that handle that filter badly - ENOSYS from
calls that plainly exist is the signature, which is what the Fold's storm looks
like; and the phantom-process warning above, which is his "your OS killed it".
2026-09-21 12:56:25 -04:00
The412Banner 13c512de0b Performance: a switch to keep Steam's xalia helper out of the session
xalia is an x86 Windows program Proton launches to give Windows programs
gamepad navigation. Under FEX it cannot load the session's aarch64 preload
shim, so its socket() and memfd calls reach the vendor's seccomp filter raw -
and where that answers ENOSYS, measured on a Galaxy Fold, it storms: 89
refusals of each in a few seconds, then a broken Wayland pipe and a session
that is gone 6-12 seconds after Big Picture appeared. Fourteen sessions in
eighteen minutes died that way on one device while the same build ran for
minutes on another.

Proton has its own gate for it, PROTON_USE_XALIA, so the switch sets that
rather than interfering with the helper. Off by default: xalia is Valve's, and
on a device whose seccomp answers its syscalls normally there is no reason to
take it away.
2026-09-21 11:59:35 -04:00
The412Banner d7038a2b50 Performance: sysmem and Zink lazy-descriptor switches
The two tunables the Fold experiment needs, as switches under Performance
rather than files in Downloads: TU_DEBUG=sysmem for the runtime's Turnip and
ZINK_DESCRIPTORS=lazy for the client's menus. The Downloads files still win.
2026-09-21 11:38:37 -04:00
The412Banner f45bca182d Session: a Downloads file of extra environment variables
Downloads/steamdeck-env holds KEY=VALUE lines that go into the session's
environment as written, after the app's own, so a line here wins. For a device
that cannot be reached with a debugger and an experiment that would otherwise
cost a build per attempt: Zink and Turnip tunables, gamescope's, the client's.

The case at hand is a Fold whose Steam menus run at 14 fps on the A8xx Turnip
while a game on the same session runs at 89 - the client's Chromium goes
through ANGLE and Zink, and ZINK_DESCRIPTORS=lazy is the first thing to try.
2026-09-21 11:12:05 -04:00
The412Banner beab4e7849 Performance: the client and game core masks, from Bannerlator's Linux session
A Steam session runs two things that want different cores at the same time,
and one affinity cannot serve both (Bannerlator cfca3912).

The client is the case that prompted it: Steam pins its own interface renderer
to a subset of cores it chooses - five of eight on one device, leaving out both
little cores and the fastest one - reasonable while a game is running and wrong
when the client is all there is, where it shows as Big Picture feeling sluggish.
BL_CLIENT_CPUS overrides it, re-applied on a beat by the session script because
steamwebhelper spawns children continuously and each inherits Steam's choice.
That block is Bannerlator's, verbatim. One deliberate difference: the client's
mask is sent whenever the override is on, even when it names every core,
because undoing Steam's pin is the whole point and the scheduler's default is
exactly what that pin takes away.

A game gets BL_GAME_CPUS instead, consumed by the Proton wrappers that r27
already carries, and as in Bannerlator it is sent only when it is a real
restriction: a list naming every core is what the scheduler does unaided.

Both live behind a Performance dialog with a checkbox per core, labelled with
its ceiling, and apply at the next session start.
2026-09-21 10:49:34 -04:00
The412Banner fbe3737dc4 PulseAudio: the 13.0 pipe modules, and a bundle that re-unpacks when it changes
The bundle carried a PulseAudio 17.0 glibc build of module-pipe-source (and
-sink): NEEDED libpulsecore-17.0.so and ld-linux, beside a 13.0 bionic daemon.
It could not dlopen, the DirectAudioMic source never existed, and the Steam
client reported no input device - the first thing the microphone port ran into
on a device. Bannerlator replaced both modules with its own 13.0 builds
(997acba2); this is that bundle, byte for byte, the other 75 files unchanged.

The component also unpacked the bundle only once, ever - a device kept the
modules from its first run whatever a later apk shipped. It now unpacks whenever
BUNDLE_STAMP changes, so this fix and the next one reach an installed app.
2026-09-21 10:36:19 -04:00
The412Banner 849596facb CI: fold the platform-library list before matching it
The list spans three lines inside one string, and the case pattern looked for
the name between two spaces - so the last name on each line, followed by a
newline, could never match. libaaudio.so is one of those, and the DirectAudio
relay helper is the first library we ship that needs it.
2026-09-21 09:18:20 -04:00
The412Banner f542e59a20 Audio: DirectAudio for games, and the microphone, from Bannerlator's Linux session
Two opt-ins on a new Audio screen, both ported from Bannerlator's gamescope
runtime where they are proven with voice in Steam's own tester.

DirectAudio replaces the audio driver inside the games the client launches -
Wine's mmdevapi driver, glibc build - with one that talks to a helper on the
Android side over a unix socket. The client keeps PulseAudio either way: it is
a native Linux program with no other way to make a sound. The helper runs here
because nothing inside the session can open Android's audio: a game is a glibc
process in a rootfs that binds no /system, and the uid the helper runs under is
what the recording permission is checked against.

The microphone is its own opt-in. The helper opens the input stream only when
asked and writes it to a pipe; PulseAudio's module-pipe-source turns that into
a source the client sees as DirectAudioMic. The daemon makes the pipe and the
helper waits for it - the two used to race, and the module fails on EEXIST.

The Proton wrappers gain Bannerlator's bl_directaudio, which adds the driver's
directory to WINEDLLPATH and names the driver in the prefix, and refuses on any
Proton that is not Wine 11 rather than pair a driver with an interface it was
not built against, which would be silence rather than an error. bl_game_cpus
comes along in the same splice and is inert until BL_GAME_CPUS is set.

The driver is staged into the runtime at every launch like the session scripts;
the helper ships as libdirectaudiorelay.so beside the PulseAudio daemon. Both
are the binaries Bannerlator carries, built by The412Banner/directaudio's relay
workflow (linux-wine11 build against ValveSoftware/wine proton_11.0).
2026-09-21 09:13:33 -04:00
The412Banner 3182ba780e Session: a TU_DEBUG switch, and sysmem by default for the A710/A720/A722 driver
Turnip's debug flags reach the runtime's driver and everything on it through
TU_DEBUG. Downloads/steamdeck-tu-debug now sets it verbatim, like the other
device switches. With nothing there, an imported driver from the A710/A720/A722
legs gets "sysmem" on its own: both the patch's author and our release notes
advise it for those GPUs, and nothing else in the list needs it.
2026-09-21 08:57:57 -04:00
The412Banner fe812081ec Restore Steam's overlay, and stop the app that takes the Steam login
Two deviations from a normal Steam launch, both removed rather than worked
around — Bannerlator's fix (334955ea), brought across unchanged.

The overlay was being stripped from every game because it landed in front of
open() and read() on /dev/input, letting Wine open the session's pad and then
answering every read of it with nothing. That was the wrong cure for an
ordering problem: glibc reads LD_PRELOAD before /etc/ld.so.preload, and the
interposer lives in the latter, so the overlay was simply ahead of it. Naming
the interposer first in LD_PRELOAD puts it back in front and the overlay loads
as Valve intends, in both wrappers — Valve's depot and an adopted GE-Proton or
proton-cachyos. Every difference between the launch Valve ships and what runs
here is a difference this runtime would otherwise have to account for.

Second, the competing Steam client is asked to stop rather than only named.
killBackgroundProcesses is a normal permission and ends exactly the case that
keeps recurring: GameHub declares boot receivers, so it is running from the
moment the phone starts without ever being opened, and it takes the login off
the client seconds after every sign-in. Done at Steam-session start only; the
desktop signs into nothing.

README: the GPU requirement now says which Adrenos Turnip actually drives. A
user's session log ended at "device (chip_id = 7010000, gpu_id = 710) is
unsupported" — an Adreno 710, which Mesa has no entry for.
2026-09-21 08:42:17 -04:00
The412Banner 539fd692ed Graphics drivers: choose and import the Linux runtime driver and the display driver
Ported from Bannerlator's driver selection so the standalone stops being pinned
to whatever Turnip the runtime image was built with (Mesa 26.2.2 today, while
Bannerlator already runs 26.3.0 in the same role).

Linux runtime driver: LinuxVulkanDriverManager imports a "-Linux" (glibc)
Turnip zip under files/linux_vulkan_drivers/<id>/ with a generated ICD manifest
whose library_path is absolute, validated by the libc the driver links against
(libc.so.6, never libc.so) and by its ELF header. The choice is per mode, so
Steam and the desktop can differ. SessionService hands the session BL_VK_DRIVER,
and the session script — staged from the apk at every launch, so no runtime
re-host — checks the manifest and its library from inside before exporting
VK_DRIVER_FILES; the runtime's own driver is what anything unreadable falls
back to. That block is Bannerlator's, verbatim.

Display driver: TurnipDriver now also imports AdrenoTools zips beside the two
bundled builds and honours a chosen id ahead of the GPU pick, with the same
refusals as Bannerlator's AdrenotoolsManager — a zip with no libraryName, a
Linux runtime driver, or a glibc library dressed as one is refused with the
list it belongs in, never installed as a black session.

One dialog on the main screen carries both lists; a removed import falls back
at once. Documented in the README.
2026-09-21 08:22:03 -04:00
The412Banner 54417dd3a7 Release notes: the published APK's checksum and build 2026-09-20 05:51:16 -04:00
The412Banner 05fc566704 README: requirements, installing, an honest state table, known limits and where the logs are 2026-09-20 05:46:19 -04:00
The412Banner 6cdae9abfa Release notes for 0.1 2026-09-20 05:44:07 -04:00
The412Banner d5a0316ae2 README: the desktop, and starting the client offline 2026-09-20 05:43:27 -04:00
The412Banner 95894f7401 Merge the desktop: an LXQt session beside the Steam client
The app gains a second thing it can run. Where the Steam mode hands the
session to gamescope, the desktop mode hands it to labwc with LXQt's panel and
file manager, Xwayland for the X11 programs, and a catalog of packages hosted
beside the runtime: the desktop itself, an emulator set, and the standalone
emulators that have no ARM64 Linux build upstream and are compiled from source.

With it come the pieces a desktop needs and a game does not: a pointer the
touchscreen can drive as a touchpad or directly, a resolver written from the
live network so a browser reaches the internet, fontconfig defaults so text
has a face, and the child-process sandboxes turned off for programs that
cannot set them up under proot. The Steam side gains one thing: starting the
client offline, decided before launch because the client reads it only once.
2026-09-20 05:42:25 -04:00
The412Banner c18fe1a86e Desktop: turn Firefox's child-process sandboxes off
The screenshot shows the tab crashing while the chrome around it draws text
perfectly, and the 'no fonts' line is an annotation on that crash, logged by
the child. The parent reads the fonts; the child, sandboxed with seccomp and
namespaces that do not behave under proot, cannot, and Gecko takes a fatal
path with no usable face. The prefs for this are advisory and a profile can
override them, so the sandboxes are turned off in the environment the session
hands every program it starts.
2026-09-20 05:33:14 -04:00
The412Banner ce9cf2bdf3 Desktop: name a default font family, and rebuild fontconfig's cache once
Firefox's own network log shows it fully online - DNS answering for both
families, sockets connecting, HTTP/2 and HTTP/3 sessions carrying data - so
the blank pages were never the network. They are the fonts: Gecko reports
'no fonts' with 13 families found, which is what it says when no generic
family resolves to a face. The linked rules map sans-serif onto faces a full
distribution installs; this rootfs has DejaVu and nothing else, so local.conf
names it outright for every generic, and the cache built into the image is
rebuilt once so the new rules are seen.
2026-09-20 05:24:21 -04:00
The412Banner 1b3c1cbf86 Offline mode on the main screen: start the client without signing in to Valve
The client runs without a connection - installed games launch, Proton and the
frame-gen are all local - but only with credentials from an earlier sign-in,
and it decides by two keys in loginusers.vdf that it reads once while it
starts. That is why the switch belongs here and not in the session's drawer:
changed during a session it would write the file and change nothing until the
next launch. The button is disabled, and says so, until the client has signed
in once. The client rewrites the file when it exits, so the request is kept as
the app's own preference and written in again at every session start.
2026-09-20 05:11:56 -04:00
The412Banner 6c380528f3 Desktop: give fontconfig its default rules
/etc/fonts/conf.d in the rootfs holds only the README: none of fontconfig's
rules that map sans-serif, serif and monospace onto installed faces. Programs
find the fonts and no default font; Firefox logs 'no fonts' and draws pages
without text. The desktop launcher links the 33 default rules in when the
directory has none.
2026-09-20 05:00:24 -04:00
The412Banner 5417587ac4 Desktop: Firefox writes its network log in debug runs
The session reaches the internet over both families (r32's probe), so a
browser saying otherwise has to explain itself: with the debug marker set,
Firefox logs its name lookups, connects, HTTP transactions and link/offline
decisions to ~/.cache/steamdeck/firefox-net.log.
2026-09-20 04:59:35 -04:00
The412Banner 3d4cfededf Desktop: keep Firefox out of Work Offline, and probe the network in debug runs
Firefox decides whether it is online by dumping the routing table over
netlink, which Android denies to apps; it sees no default route, switches to
Work Offline and every page fails while the network is fine. A defaults file
staged into Firefox's pref directory turns that off, and drops the process
sandboxes that have nothing to set up under proot. Debug runs now log a name
lookup and a fetch over IPv4 and IPv6 from inside the session.
2026-09-20 04:51:50 -04:00
The412Banner 36b0bea470 Compositor: tell a nested toplevel the output's size
labwc's Wayland backend, configured with 0x0, opened wlroots' default
1280x720 window and the whole desktop was upscaled from it - the r29 and r30
logs both show the output at 1280x720 whatever rc.xml and wlr-randr asked.
The configure now carries the output's size; gamescope is told the same
numbers on its command line, so nothing changes for the Steam client.
2026-09-20 04:49:13 -04:00
The412Banner 8362f48750 Desktop: name the panel in panel.conf, size the nested output to the app's surface
The panel was there all along, a bare strip at the top: the first package's
panel.conf described a panel it never named in [General] panels=, so
lxqt-panel built itself a default one with no plugins. panel.conf now travels
with the app like the rest of the desktop's defaults, and the old nameless
file on a device is replaced once. The nested labwc output was 1280x720 -
wlroots' default window - with the desktop upscaled from that; rc.xml now
carries the app's surface size and the autostart sets it again via wlr-randr.
Debug runs keep each program's full output under ~/.cache/steamdeck/.
2026-09-20 04:40:23 -04:00
The412Banner f10919d4be Desktop: give pcmanfm-qt's desktop the wallpaper, and a debug marker that logs window mapping
The desktop came up black with the panel alive but unseen. pcmanfm-qt's
desktop surface, unconfigured, paints solid black over swaybg's wallpaper; it
now gets the wallpaper. With ~/.steamdeck-desktop-debug present, labwc logs at
debug level and the panel and file manager log their shell, layer and output
protocol traffic, capped, so where a window went can be read off the session
log.
2026-09-20 04:28:46 -04:00
The412Banner 07dadb13f3 Desktop: clear last session's single-instance state, run under a session bus, stage the launcher from the app
The desktop came up with a wallpaper and nothing on it the second time. The
runtime directory outlives a session, and lxqt-panel and pcmanfm-qt are
single-instance programs that leave a lock and a socket there; finding last
session's, they decided they were already running and exited without a word.
The launcher now clears everything but the compositor's own socket, runs labwc
under dbus-run-session so the panel's menus and the file manager have the bus
they expect, and logs what the panel and the file manager say. The launcher
and labwc's defaults are staged by the app over the desktop package's copies,
so a fix here no longer costs a 426 MB re-download.
2026-09-20 04:16:07 -04:00
The412Banner 3ea82f6447 Cursor: half the size 2026-09-20 04:01:28 -04:00
The412Banner e11af116e9 Drawer: the touch mode switch 2026-09-20 03:54:59 -04:00
The412Banner 6568bf667d Touchpad on the desktop, and the guest's resolver from the live network
Touch on the desktop now works the way Bannerlator's desktop containers do:
the arrow stays where it is and a finger dragged anywhere moves it from there,
tap clicks, a second finger tapping while one holds (or a two-finger tap)
right-clicks, two fingers scroll, hold-then-drag and double-tap-hold drag.
Steam keeps the direct pointer; the drawer pins either.

The runtime's resolv.conf is written from the active network's DNS servers on
every link change — IPv4 first, never Android's link-local IPv6 ones, the
public pair as fallback — which is what glibc, and so Firefox, resolve with.
Routing needs nothing: proot makes no network namespace.
2026-09-20 03:51:59 -04:00
The412Banner f5ca6d0053 A pointer for the desktop: an arrow, touch gestures and a real mouse
The pointer moved but nothing drew it — the nested desktop asks its parent
compositor for a cursor and ours ignores the request — and touch knew only
move and tap. Now the app draws the arrow where it last sent the pointer, touch
works like a remote desktop (tap clicks, hold then lift right-clicks, hold then
drag drags, two fingers scroll), and a mouse arrives as itself: hover moves,
every button presses, the wheel scrolls, with Android's own pointer hidden so
there is one arrow.
2026-09-20 03:43:53 -04:00
The412Banner d6789f7b61 Desktop: composite the shell with pixman
labwc died at "unable to create allocator": wlroots allocates through gbm on a
DRM render node, and ours is a KGSL stand-in gbm cannot use — the wall the
freedreno spike hit, which gamescope only clears with its own Vulkan-WSI path.
pixman uses a shm allocator and no DRM at all, so the desktop boots; clients
on it pay a CPU copy. steamdeck-wlr-renderer in Downloads overrides the
choice for a device with a real node.
2026-09-20 03:36:09 -04:00
The412Banner 2e3196788e Desktop: an lxqt session mode, the hosted-package catalog and the Desktop & apps screen
A second thing to launch beside Steam: the LXQt/labwc desktop, a hosted package
the app installs into the runtime like the runtime itself, with the emulators
and apps as further packages from the same catalog (desktop.json in
winlator-contents). A tar package extracts over the rootfs with the runtime's
own extractor; an AppImage lands in /opt/appimages with a .desktop entry and
runs extracted, since proot has no FUSE. The session script's lxqt mode runs
the desktop's labwc in place of gamescope.
2026-09-20 03:16:09 -04:00
The412Banner 7f154b933d The drawer scrolls
With Keyboard and Compatibility tools added, a landscape phone no longer had
the height for it and Stop session fell off the bottom. The panel scrolls, and
the two session-leaving actions ride at the end of the list rather than pinned.
2026-09-20 02:22:35 -04:00
The412Banner af25620316 Keyboard in the drawer, and GE-Proton / proton-cachyos on request
A session had no way to type without a hardware keyboard. The drawer's Keyboard
opens the soft keyboard onto an invisible host View whose InputConnection turns
what the IME commits into the key events that would have typed it, and the
key path carries Bannerlator's two soft-keyboard fixes: a symbol key with no
scan code is worked back from the character it stands for, and a capital gets
the Shift a soft keyboard reports but never sends — not a hardware one, whose
own Shift would be released early by a second.

Compatibility tools, on the main screen and in the drawer, queues a GE-Proton
or proton-cachyos install as a line in the runtime's ~/.bl-proton-extra, which
the session script hands to bannerlator-proton-extra before the client starts;
it shows what is installed and removes a build.
2026-09-20 01:07:09 -04:00
The412Banner b74aa97996 Bring the runtime up to date with Bannerlator, and end a session the way proot can survive
From Bannerlator's feat/linux-gamescope-runtime since this repo was cut:

- Session teardown (4509d788): proot was SIGKILLed, which never lets its
  --kill-on-exit cleanup run; its tracees lived on with no tracer, every
  seccomp-trapped syscall failing with ENOSYS, spinning at a full core for over
  an hour. Now SIGTERM, a grace for proot's own cleanup, SIGKILL only if it
  will not go, then a sweep of the tree it had — snapshotted first, each pid
  checked against its start time.
- The Steam client's controller interposer is its own source
  (fakeinput_steam.cpp, 28922e7c), separate from the one preloaded into Wine.
- The seeder runs on a beat so a prefix the client creates mid-session is
  covered before its install scripts run (6ee86bff).
- GE-Proton and proton-cachyos as compatibility tools: the registrar
  bannerlator-proton-extra and the compat script's adopt_extras (03867656).
- space.c in the session shim: a library's own free space, not the phone's.

The env-vs-args ordering bug (98d38931) does not apply here: nothing is added
after the script name.
2026-09-20 00:55:37 -04:00
The412Banner 9ee59066a3 Credit Winlator
The audio stack, the gamepad model and the shape of a session's host-side
components are brunodev85's, and both apps this was lifted from are Winlator
lineage. The credits and README named the descendants and not the ancestor.
2026-09-19 20:09:04 -04:00
The412Banner 83ad9dfa79 Re-arm frame generation when the presenting window changes
Opening the Steam menu over a fullscreen game and closing it again left the
game as a strip in a corner with an engine on. gamescope forwards a fullscreen
game's own buffers straight through and hands over its composited output while
the menu is up; each is a different window to the compositor, and the engine's
ring and history straddled the switch. The compositor already reports every
change of presenting window, so the engine is disarmed on it and re-armed
against the new one, as it is across a Surface resize.
2026-09-19 17:19:19 -04:00
The412Banner 565987edae Disarm frame generation across a Surface resize, re-arm after
With the session activity resizable the fold works — with frame generation off.
With an engine on the picture still came back stretched: its ring, effects
chain and multi-present pacing were built against the old extent. The engine is
now disarmed before the window is rebound and re-armed once the new swapchain
has settled, so everything on its side is built again for the new size.
2026-09-19 17:03:14 -04:00
The412Banner 357ba28b6a Let the session activity resize
It was declared non-resizable. On a foldable that means the system keeps a
fixed-shape window across the fold and stretches it onto the other panel — the
app never receives a size change, so the compositor's re-fit, the swapchain
rebind and the letterboxing all had nothing to react to. Resizable, the window
follows the panel and the picture keeps its shape with bars.
2026-09-19 16:52:15 -04:00
The412Banner de1bfb6b94 Rebind the compositor when the Surface changes size
A foldable opening or closing resizes the Surface rather than recreating it.
The compositor's swapchain, built for the old size, kept presenting, and the
system scaled those buffers onto the new window — on a Fold a 16:9 picture
came back stretched to the square panel, the same width and 2.09x the height.
Rebinding the window on a size change rebuilds the swapchain at the new size,
and the fit mode letterboxes as it should.
2026-09-19 16:35:53 -04:00
The412Banner 033db93058 Main screen: scale to the window, and keep clear of the system bars
Panels and densities vary too much for fixed dp: a 7" 1080p handheld in
landscape has barely 400dp of height and the column did not fit it, with the
logo under the status bar and the credits off the bottom. The column is now
designed at one size and scaled by the height available — logo, text, buttons
and spacing shrink together until it fits, down to 55% — with scrolling as the
last resort and a width cap so the buttons never span a wide panel.
2026-09-19 16:16:31 -04:00
The412Banner 3877d0696f Keep the session's output size across a Surface recreation
gamescope's display is sized once when the session starts and cannot change,
but the size was recomputed on every surfaceCreated. A foldable recreates the
Surface on the other panel, so a session started on the 21:9 outer screen and
opened onto the square inner one told the compositor its 21:9 buffer was 16:9
and the picture came back squashed sideways. While a session runs its size is
kept; the compositor letterboxes it onto whatever panel is current.

Also the Compose build: BoxWithConstraints' maxWidth read outside its scope.
2026-09-19 16:09:09 -04:00
The412Banner aa7c71bfb6 Every menu, dialog and overlay on Jetpack Compose
The main screen, the loading overlay, the HUD line, the drawer and the three
dialogs are composables now, driven by state the activities own. The session's
SurfaceView and the on-screen pad stay Views underneath a single Compose layer,
which is what they are in any Compose app: a compositor surface and a canvas,
not menus. The XML layouts are gone; the activities extend ComponentActivity
and Back goes through its dispatcher.
2026-09-19 16:05:54 -04:00
The412Banner 18364845ad Display shape: the panel's own or a fixed 16:9, defaulting to 16:9 on a foldable
gamescope's display cannot change size once the session is up, so a foldable
that starts on one panel and opens onto the other can only be letterboxed
differently, never re-shaped. A fixed 16:9 sits with modest bars on either
panel; the panel's own shape fits one and leaves a strip on the other. A device
with a hinge sensor defaults to 16:9; the drawer toggles it for the next
session.
2026-09-19 15:54:25 -04:00
The412Banner d2ea9b898a Never hand the guest a display narrower than 16:9; letterbox in the compositor
A foldable's inner panel is nearly square, and the session sized its output to
match it. A game given a square display draws for the frame it was made for
and cuts the sides off itself — FlatOut on a Fold lost the edges of its own
menus. The output now keeps the panel's aspect only above 16:9 (a phone's 20:9
is fine), the compositor's scale mode is set to fit rather than its stretch
default so the picture keeps its proportions with bars, and touch is mapped
through the fitted rectangle instead of the whole view.
2026-09-19 15:50:48 -04:00
The412Banner d2f2aa9c23 Grey out LSFG until Lossless Scaling is installed in the client
The engine cannot run without the DLL, so a pickable LSFG row only produced a
setting that quietly did nothing. The rows are dimmed and unselectable until
the DLL is in the runtime's Steam library, and say what to install.
2026-09-19 15:40:41 -04:00
The412Banner 51303fa342 A drawer on Back: HUD, frame generation, on-screen controls, background, stop
Back used to background the app outright. It now opens a slim panel on the
right with the switches a player reaches for mid-game — the performance HUD,
the frame-generation picker (shared with the main screen), the on-screen
controls' mode (auto / always / never, replacing the file-only override) —
and the two things that leave the session: send it to the background, or stop
it. Back again, or a tap outside, closes it.
2026-09-19 15:37:37 -04:00
The412Banner 3582186951 A performance line in the corner: fps, and base → generated when frame generation runs
Whether frame generation is doing anything was only knowable from a log. The
top-right line counts the game's own frames from the compositor's per-frame
callback, and while an engine generates it shows the engine's telemetry beside
it — "LSFG 2×  60 → 118 fps" — or says the engine is starting, unsupported or
failed. Touches pass through it. steamdeck-no-hud in Downloads hides it.
2026-09-19 15:29:16 -04:00
The412Banner df61d1bbad Ship the scanout header VulkanRendererContext.h includes
Header only: the frame-generation engines take a VkTable out of that context
and never touch the direct-scanout side, so nothing links against it.
2026-09-19 15:17:20 -04:00
The412Banner 0425023063 Frame generation in the compositor: Win-FG Native and LSFG Native
The compositor was built with its frame-generation leg stubbed out. Bannerlator's
two engines and the DXBC translator they need now ride along, so extra frames
are generated on gamescope's final output — any game, nothing changed in the
runtime. Win-FG needs nothing; LSFG takes its shader chain from the user's own
Lossless Scaling, which in this app is installed from the Steam client and read
straight out of the runtime's library. One picker on the main screen: Off, or an
engine at 2x/3x/4x, applied live on the next return to the session.
2026-09-19 15:14:45 -04:00
The412Banner d46bd4a0c3 Start a session on every Play, not only on the compositor's first start
The compositor lives for the whole process, so the second Play after a session
had ended re-attached the Surface, started nothing, and left the loading panel
counting up over a dead session — it looked like a login that never came. The
service now starts whenever no session is running; the compositor's one-shot
first-frame notice is re-armed for the new session, or the panel would never
leave; and whatever the previous session left behind (gamescopereaper, the
session script, a dead client's helpers, the audio server) is killed first,
since it is our uid and still holds the socket the new one needs.
2026-09-19 14:59:12 -04:00
The412Banner ebae834e36 A loading panel that proves the session is moving
Between Play and the client's first frame there was a line of text in a corner
of a black screen, for minutes on a first run. The panel in the middle now
shows the runtime's milestone, the client's own download percentage lifted out
of its log (its bootstrap prints "Downloading update (a of b KB)", which the
milestones never reflected), a clock, and a rotating hint about what is
happening. It reads the log's tail only, twice a second; a session that has
already drawn a frame does not get the panel again.
2026-09-19 14:45:22 -04:00
The412Banner b27a4f5b31 Main screen: scroll, and size from the window rather than the panel
On a landscape phone the install button sat under the navigation bar with no
way to reach it. The screen now scrolls, everything on it is smaller, and the
side gutter grows with the window width (24dp, 96dp from 600dp, 240dp from
960dp) so the buttons stay a sensible width on a wide panel instead of spanning
it. The logo is the icon clipped to a circle; shown raw, an adaptive icon is a
bare square.
2026-09-19 14:32:37 -04:00
The412Banner f86b64b18e Verify the signature from minSdk 21, or apksigner never looks at v1
It only checks the schemes an apk's own minSdk requires, so at 26 it reported
v1 as false while the JAR signature was sitting in META-INF the whole time.
The file check beside it says the same thing a second way.
2026-09-19 14:27:51 -04:00
The412Banner 432e197454 On-screen controls for a device with no pad, gone the moment one is attached
A phone had no way to answer Big Picture: touch moved the pointer and nothing
pressed a button. The on-screen pad writes into the same rings the physical one
does, so the client sees a single Xbox pad whichever is in use and both work at
once. An InputManager listener takes the controls away when a controller
appears and brings them back when it goes; steamdeck-osc in Downloads pins it
either way. Virtual input devices no longer count as controllers — the system's
own synthetic devices would otherwise hide the controls with nothing to
replace them.
2026-09-19 14:27:05 -04:00
The412Banner cc868496d1 Sign the apk with apksigner so v1 is actually there
AGP drops JAR signing at minSdk 24 and above and the DSL flag does not move it,
release build or not. zipalign then apksigner with the three schemes named
explicitly is the only version of this that can be checked.
2026-09-19 14:25:16 -04:00
The412Banner 71f84b93cc Tell lint that targetSdk 28 is the point
A release build fails on ExpiredTargetSdkVersion, which exists because Play
will not take an sdk that old. It is right about the rule and wrong about this
app: 29+ forbids executing a file from the app's own data directory, which is
the one thing this app exists to do.
2026-09-19 14:23:08 -04:00
The412Banner 4b43a617d4 Build the release variant so the apk carries v1 signing too
AGP drops JAR signing from a debug build when minSdk is 24 or higher, whatever
the signing config says, so the check added with the testkey failed on its
first run. Nothing is minified in either variant; this only changes which one
CI hands over.
2026-09-19 14:20:07 -04:00
The412Banner 344e70afc6 Keep the session alive in the background, rename the package, sign with the testkey, and draw the app a logo
The session now belongs to a foreground service rather than the activity.
Android demotes a process as soon as its last visible activity goes and the
low-memory killer reaps the guest, so leaving Big Picture to answer a message
came back to a dead Steam; the service holds the process at perceptible
priority, a partial wake lock keeps the CPU from dropping the guest's threads,
and a high-performance WiFi lock keeps a backgrounded download from being
throttled to nothing. The notification is silent (IMPORTANCE_LOW), carries a
Stop action, and returns to the session when tapped. Back now puts the app away
and leaves Steam running; a swipe out of recents ends the session.

The compositor moved with it: it is process-wide, started once, and merely
swaps the Surface it presents into as the activity comes and goes.

Package is com.steamdeck.launcher, signed with the AOSP testkey (v1+v2+v3) so
every build installs over the last one, and CI asserts all three schemes.

The launcher icon is our own controller on a purple glow — the spirit of the
Steam tile the client entry uses, none of its geometry.
2026-09-19 14:17:25 -04:00
The412Banner 4031f512fc Cache everything CI can, and credit both authors
The repo is private, so each run is billed. Three caches now carry a rebuild:
Gradle's own through setup-gradle (dependencies, build cache, configuration
cache), the CMake/ninja tree keyed on the C sources and the NDK, and the two
cross-compiled preloads — the last of which also skips installing the cross
toolchain, since a run that changes only Kotlin has no reason to touch a C
compiler. Superseded pushes are cancelled, documentation pushes do not build,
and the apk is stored rather than recompressed.

The preload assertions moved from the cross binutils to readelf, which reads
any ELF whatever the host, so they keep running on a cache hit.

Credits name The412Banner and maxjivi05: the runtime under all of this is
Max's gamescope work from WinNative.
2026-09-19 14:07:06 -04:00
The412Banner 97ee688eff Size the session from the panel, not the app window
resources.displayMetrics is the window after the system bars and the cutout are
subtracted, so the first session on a 1080p device came up 1920x968 and
gamescope patched its EDID to suit. The panel's own bounds are what a client
should be told the display is.
2026-09-19 14:01:40 -04:00
The412Banner 814ab2cac0 Add to the process environment rather than replacing it
Matches the exec path this was ported from; the guest command already starts
with env -i, so clearing the host side changed nothing except how far the code
sits from a configuration proven on a device.
2026-09-19 14:01:08 -04:00
The412Banner 085f663347 Ship libsndfile.so, which the PulseAudio core links
Caught by the closure check the previous commit added, before a device had to
find it: the daemon would have failed to start and the session would have run
without audio.
2026-09-19 13:57:01 -04:00
The412Banner 20a3b1ad63 Ship libffi.so, and check the whole native dependency closure in CI
libwayland-server.so links libffi, so loading the compositor threw
UnsatisfiedLinkError the moment Play started a session. The library was there,
its dependency was not — which is exactly the kind of gap a build cannot see
and a device finds immediately, so CI now walks every NEEDED of every .so we
package and fails on anything that resolves to neither the apk nor the
platform.
2026-09-19 13:54:41 -04:00
The412Banner 83cc1c4ec8 README: how to skip the 790 MB download on a device that already has the runtime 2026-09-19 13:53:33 -04:00
The412Banner c22cbc9f43 Type into the session: hardware keys to the compositor, and a log path that always exists
The session script redirects its own output into BL_LOG with exec, and a
redirection a non-interactive shell cannot open ends that shell — so a denied
storage permission would have killed the session rather than just losing the
log. Downloads is used once proven writable, files/logs otherwise.

Keyboard events now reach wl_keyboard, which is what the client's first-run
login needs on a device with a keyboard attached.
2026-09-19 13:49:29 -04:00
The412Banner 9b3dd05f4f Ship every shader header the effect chain includes
effects_chain.c runs the X11 renderer's committed SPIR-V; copying only the ones
named in the first screenful left deband_frag.h behind.
2026-09-19 13:45:20 -04:00
The412Banner 9119eb0045 Import the downloader the installer uses, and package libwayland-server.so
The compositor links libwayland-server.so as an imported target, so CMake never
copies it into the apk; it has to travel in jniLibs like the PulseAudio set.
2026-09-19 13:42:45 -04:00
The412Banner 4022351e22 SteamDeck: a standalone launcher for the native ARM64 Linux Steam client
Lifts Bannerlator's Linux/gamescope runtime out into an app of its own: the
Wayland compositor, the proot session, the runtime installer, the fake-evdev
controller path and the PulseAudio server, with none of the Wine, container or
store machinery around them.

One activity installs the runtime from the same winlator-contents catalog and a
second one runs the session: compositor on a SurfaceView, gamescope and Valve's
native arm64 client under proot, a physical pad republished into the fake-input
rings, and the session's milestones on screen until the first frame arrives.
2026-09-19 13:39:17 -04:00
1271 changed files with 271575 additions and 820 deletions

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
name: Build AAudio sink module
# A quick check of tools/aaudio-sink on its own; the APK build compiles the same module into the
# audio bundle. Runs on pushes that touch the module.
on:
push:
paths:
- 'tools/aaudio-sink/**'
- '.github/workflows/build-aaudio-sink.yml'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: PulseAudio 13.0 source
run: |
set -euo pipefail
curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1
- name: Build
run: |
set -euo pipefail
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/out"
- uses: actions/upload-artifact@v4
with:
name: audio-sink-modules
path: out/*.so
+101
View File
@@ -0,0 +1,101 @@
name: Build desktop package (KDE Plasma)
# The desktop the app installs into its Linux runtime on first use: KDE Plasma as SteamOS's desktop
# mode has it, the closure of tools/desktop-kde/seeds.txt over Arch Linux ARM minus what the runtime
# already carries, plus tools/desktop-kde/overlay (tools/desktop-kde/build.sh), every package
# signature-checked. Published as a release in Droid-Deck/DroidDeck-Components with DroidDeck's
# other components; the apk pins it in tools/desktop-kde/release.env (tag, sha256, size). A
# published tag is never rebuilt: the packages are Arch Linux ARM's current ones, so a rebuild
# would not match the pinned sha256. A changed package gets a new tag (desktop-kde-r2, ...) and a
# release.env update.
on:
push:
paths:
- 'tools/desktop-kde/**'
- '.github/workflows/build-desktop-kde.yml'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish as (e.g. desktop-kde-r1); empty = artifact only'
required: false
default: ''
permissions:
contents: write
jobs:
kwin:
runs-on: ubuntu-24.04-arm
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- name: Build KWin with nested desktop scaling
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq zstd xz-utils gpgv patch binutils >/dev/null
bash tools/desktop-kde/build-kwin.sh out/kwin
- uses: actions/upload-artifact@v4
with:
name: desktop-kde-kwin
retention-days: 7
path: out/kwin/
build:
needs: kwin
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: desktop-kde-kwin
path: out/kwin
- uses: actions/create-github-app-token@v2
id: components-token
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
owner: Droid-Deck
repositories: DroidDeck-Components
- name: Refuse a published tag
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ steps.components-token.outputs.token }}
run: |
if gh release view '${{ inputs.tag }}' -R Droid-Deck/DroidDeck-Components >/dev/null 2>&1; then
echo "::error::${{ inputs.tag }} is already published, and published tags are never replaced: use a new tag"
exit 1
fi
- name: Build
run: |
set -euo pipefail
sudo apt-get update -qq && sudo apt-get install -y -qq zstd xz-utils gpgv >/dev/null
KDE_KWIN_OVERRIDE="$PWD/out/kwin" tools/desktop-kde/build.sh out
cp out/kwin/desktop-kde.kwin.txt out/
echo "size $(stat -c %s out/desktop-kde.tar.zst) sha256 $(cat out/desktop-kde.sha256)"
# The package itself too, briefly: enough to try a build on a device before publishing it.
- uses: actions/upload-artifact@v4
with:
name: desktop-kde
retention-days: 7
compression-level: 0
path: |
out/desktop-kde.tar.zst
out/desktop-kde.packages.txt
out/desktop-kde.kwin.txt
out/desktop-kde.sha256
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ steps.components-token.outputs.token }}
GH_REPO: Droid-Deck/DroidDeck-Components
run: |
set -euo pipefail
TAG='${{ inputs.tag }}'
if gh release view "$TAG" >/dev/null 2>&1; then
echo "::error::$TAG is already published in DroidDeck-Components, and published tags are never replaced: use a new tag"
exit 1
fi
gh release create "$TAG" --latest=false --prerelease=false --title "$TAG" \
--notes "KDE Plasma desktop package for DroidDeck's Linux runtime ($GITHUB_REPOSITORY tools/desktop-kde, $GITHUB_SHA). size $(stat -c %s out/desktop-kde.tar.zst), sha256 $(cat out/desktop-kde.sha256). desktop-kde.packages.txt lists the signature-checked Arch Linux ARM packages it was built from." \
out/desktop-kde.tar.zst out/desktop-kde.sha256 out/desktop-kde.packages.txt out/desktop-kde.kwin.txt
gh release edit "$TAG" --latest=false --prerelease=false
+48
View File
@@ -0,0 +1,48 @@
name: Build gamescope (patched)
# The runtime's own gamescope (Arch Linux ARM's 3.16.29 package) rebuilt with the patches under
# tools/gamescope/patches, in an Arch Linux ARM container on an arm64 runner, then published as a
# release asset the apk build fetches (tools/gamescope/release.env names the tag and sha). The
# hosted runtime image is never changed: the apk stages the binary over /usr/local/bin.
on:
push:
paths:
- 'tools/gamescope/**'
- '.github/workflows/build-gamescope.yml'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish as (e.g. gamescope-3.16.29-p1); empty = artifact only'
required: false
default: ''
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-24.04-arm
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Build in Arch Linux ARM
run: |
set -euo pipefail
docker run --rm -v "$PWD:/work" -w /work menci/archlinuxarm:base-devel bash tools/gamescope/build-in-arch.sh
sudo chown -R "$(id -u):$(id -g)" .
- uses: actions/upload-artifact@v4
with:
name: gamescope-patched
path: |
gamescope.tzst
gamescope.tzst.sha256
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber
# A component the apk fetches by tag, never something users install: it must not
# become the repo's Latest release or appear as a pre-release, also when re-uploaded.
gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false
+758
View File
@@ -0,0 +1,758 @@
name: Build APK
run-name: ${{ github.event_name == 'pull_request' && format('building pr {0} on {1}', github.event.pull_request.number, github.event.pull_request.head.sha) || format('building {0} on {1}', github.ref_name, github.sha) }}
on:
push:
branches: [ main ]
paths-ignore:
- '**/*.md'
- 'LICENSE'
- '.gitignore'
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, ready_for_review ]
paths-ignore:
- '**/*.md'
- 'LICENSE'
- '.gitignore'
workflow_dispatch:
permissions:
contents: read
pull-requests: write
concurrency:
# Replace older builds only for the same PR. Each push/dispatch gets its own group so one main
# build cannot cancel another before it signs and publishes.
group: build-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
PRELOAD_CACHE_VERSION: v3
jobs:
build:
name: Build APK
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 45
outputs:
variants: ${{ steps.variants.outputs.list }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- name: Check co-authors
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
git fetch --no-tags origin main
base=$(git merge-base origin/main HEAD)
messages=$(git log --format=%B "$base..HEAD")
messages+=$'\n'"$(jq -r '.pull_request.title, (.pull_request.body // "")' "$GITHUB_EVENT_PATH")"
if grep -Eiq '^[[:space:]]*Co-authored-by:.*(Claude|Anthropic|OpenAI|ChatGPT|GPT|Copilot|Cursor|Gemini|Codex|Devin|Windsurf|Codeium|Amazon Q|Tabnine|Cody|\[bot\])' <<<"$messages"; then
echo "::error::AI and bot identities cannot be co-authors; remove the Co-authored-by trailer."
exit 1
fi
- name: The packages a signed build ships as
id: variants
run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT"
- name: No private key in the tree
run: |
set -euo pipefail
keys=$(git ls-files | grep -iE '\.(jks|keystore|p12|pfx|pk8|pem|key)$' | grep -vx 'keystore/testkey.p12' || true)
if [ -n "$keys" ]; then
echo "::error::a private key is committed - remove it from the history and make a new one: $keys"
exit 1
fi
- name: Version bumps are isolated
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
git fetch --no-tags origin main
base=$(git merge-base origin/main HEAD)
diff=$(git diff -U0 "$base..HEAD" -- app/build.gradle)
grep -Eq '^[+-][[:space:]]*version(Code|Name)[[:space:]]' <<<"$diff" || exit 0
other_files=$(git diff --name-only "$base..HEAD" | grep -v '^app/build.gradle$' || true)
other_gradle=$(grep -E '^[+-]' <<<"$diff" | grep -Ev '^(---|\+\+\+)' | grep -Ev '^[+-][[:space:]]*version(Code|Name)[[:space:]]' || true)
if [[ -n "$other_files" || -n "$other_gradle" ]]; then
echo "::error::versionCode/versionName changes must be isolated in their own PR"
echo "A version PR may only change the versionCode/versionName lines in app/build.gradle." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
- name: Merge onto main, or say what conflicts
if: github.event_name == 'pull_request'
run: |
set -uo pipefail
git config user.name "ci"
git config user.email "[email protected]"
git fetch --no-tags origin main
BASE=$(git rev-parse --short origin/main)
HEAD=$(git rev-parse --short HEAD)
if git merge --no-ff --no-edit origin/main >/tmp/merge.out 2>&1; then
{
echo "### Mergeable"
echo
echo "\`$HEAD\` merges cleanly onto main (\`$BASE\`). What follows was built from the merged result."
} > /tmp/pr-merge.md
cat /tmp/pr-merge.md >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
FILES=$(git diff --name-only --diff-filter=U)
if [ -z "$FILES" ]; then
{
echo "### ERROR: Could not merge onto main (\`$BASE\`)"
echo
echo '```'
cat /tmp/merge.out
echo '```'
} > /tmp/pr-merge.md
cat /tmp/pr-merge.md >> "$GITHUB_STEP_SUMMARY"
echo "::error::git could not merge main: $(head -3 /tmp/merge.out | tr '\n' ' ')"
exit 1
fi
{
echo "### ERROR: Not mergeable - conflicts with main (\`$BASE\`)"
echo
echo "Files that conflict:"
echo
for f in $FILES; do echo "- \`$f\`"; done
echo
echo "Each conflicting hunk, as git leaves it: \`<<<<<<< HEAD\` is this pull request, \`>>>>>>> origin/main\` is main."
echo
for f in $FILES; do
echo "<details><summary><code>$f</code></summary>"
echo
echo '```'
awk '/^<<<<<<< /{p=1} p{print} /^>>>>>>> /{p=0}' "$f" | head -300
echo '```'
echo "</details>"
echo
done
echo "Not built: rebase or merge main into the branch and push again."
} > /tmp/pr-merge.md
cat /tmp/pr-merge.md >> "$GITHUB_STEP_SUMMARY"
git merge --abort || true
echo "::error::the pull request conflicts with main: $(echo $FILES | tr '\n' ' ')"
exit 1
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: gradle/actions/setup-gradle@v4
with:
cache-cleanup: on-success
- name: Resolve the runner's NDK
run: |
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
VERSION=$(sed -n 's/^Pkg.Revision *= *//p' "$NDK/source.properties")
echo "NDK_VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Set the APK build label
id: build-label
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BUILD_REF: ${{ github.ref_name }}
BUILD_SHA: ${{ github.sha }}
run: |
if [[ -n "$(git status --porcelain)" ]]; then
tree_state=dirty
else
tree_state=clean
fi
echo "DROIDDECK_BUILD_TREE_STATE=$tree_state" >> "$GITHUB_ENV"
if [[ -n "$PR_NUMBER" ]]; then
label="pr${PR_NUMBER} at ${PR_HEAD_SHA:0:7}"
else
label="${BUILD_REF} at ${BUILD_SHA:0:7}"
fi
echo "label=$label" >> "$GITHUB_OUTPUT"
echo "DROIDDECK_SOURCE_COMMIT=${PR_HEAD_SHA:-$BUILD_SHA}" >> "$GITHUB_ENV"
echo "DROIDDECK_PR_NUMBER=${PR_NUMBER:-0}" >> "$GITHUB_ENV"
echo "Build: \`$label\`" >> "$GITHUB_STEP_SUMMARY"
- name: Cache the preloads
id: preloads
uses: actions/cache@v4
with:
path: app/src/main/assets/linuxfs
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*', 'tools/msitools/**', 'tools/linuxfs/debian-image.env') }}
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
if: steps.preloads.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu >/dev/null
D=app/src/main/assets/linuxfs
mkdir -p "$D"
aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
-pthread -std=c++17 -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \
-o "$D/libfakeinput.so" app/src/main/cpp/fakeinput_steam.cpp -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/libfakeinput.so"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$D/libblsession.so" tools/linuxfs/preload/*.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/libblsession.so"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$D/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/libblfastpath.so"
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -nostdlib -ffreestanding -fno-stack-protector \
-fno-tree-loop-distribute-patterns -Wl,-z,defs -o "$D/libblaudit.so" tools/proot/fastpath/audit.c
aarch64-linux-gnu-strip --strip-unneeded "$D/libblaudit.so"
# Force-SSBS adapter, preloaded into a game's Wine only when the game asks for it.
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$D/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/libssbs.so"
mkdir -p "$D/usr/local/bin"
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard"
for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
done
for f in tools/linuxfs/overlay/usr/bin/* tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/*; do
[ -f "$f" ] && install -Dm644 "$f" "$D/${f#tools/linuxfs/overlay/}"
done
if [ -f tools/gamescope/release.env ]; then
. tools/gamescope/release.env
gh release download "${GAMESCOPE_TAG}" -R "${GAMESCOPE_REPO:-${GITHUB_REPOSITORY}}" -p gamescope.tzst -O gamescope.tzst
echo "${GAMESCOPE_SHA256} gamescope.tzst" | sha256sum -c -
tar --use-compress-program=unzstd -xf gamescope.tzst -C "$D"
test -f "$D/usr/local/bin/gamescope"
rm gamescope.tzst
fi
. tools/linuxfs/uruntime.env
curl -fsSL --retry 3 -o uruntime "https://github.com/VHSgunzo/uruntime/releases/download/${URUNTIME_VERSION}/${URUNTIME_ASSET}"
echo "${URUNTIME_SHA256} uruntime" | sha256sum -c -
install -Dm644 uruntime "$D/usr/local/lib/droiddeck/uruntime"
install -Dm644 tools/linuxfs/licenses/uruntime-LICENSE "$D/usr/local/share/licenses/uruntime/LICENSE"
rm uruntime
M="$D/usr/local/lib/mangoapp"
mkdir -p "$M" mango-pkgs
grep -v '^#' tools/mangoapp/packages.txt | while read -r sum url; do
[ -n "$url" ] || continue
curl -fsSL --retry 3 -o mango-pkgs/pkg.tar.zst "$url"
echo "$sum mango-pkgs/pkg.tar.zst" | sha256sum -c -
tar --use-compress-program=unzstd -xf mango-pkgs/pkg.tar.zst -C mango-pkgs
rm mango-pkgs/pkg.tar.zst
done
install -m644 mango-pkgs/usr/bin/mangoapp "$M/mangoapp"
for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do
cp -L "mango-pkgs/usr/lib/$so" "$M/$so"
done
# Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c).
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \
-o "$M/libtracefs.so.1" tools/mangoapp/libtracefs-shim.c
aarch64-linux-gnu-strip --strip-unneeded "$M/libtracefs.so.1"
install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp"
rm -rf mango-pkgs
ls -l "$M"
# The Windows components installer's tools (tools/msitools/packages.txt).
T="$D/usr/local/lib/droiddeck-msitools"
mkdir -p "$T" msi-pkgs
grep -v '^#' tools/msitools/packages.txt | while read -r sum url; do
[ -n "$url" ] || continue
curl -fsSL --retry 3 -o msi-pkgs/pkg.tar.xz "$url"
echo "$sum msi-pkgs/pkg.tar.xz" | sha256sum -c -
tar -xJf msi-pkgs/pkg.tar.xz -C msi-pkgs
rm msi-pkgs/pkg.tar.xz
done
install -m644 msi-pkgs/usr/bin/msiinfo "$T/msiinfo"
install -m644 msi-pkgs/usr/bin/cabextract "$T/cabextract"
# 7-Zip (the full build with its format plugin) opens installer .exe wrappers.
install -m644 msi-pkgs/usr/lib/7zip/7z "$T/7z"
install -m644 msi-pkgs/usr/lib/7zip/7z.so "$T/7z.so"
for so in libmsi-1.0.so.0 libgsf-1.so.114 libgcab-1.0.so.0; do
cp -L "msi-pkgs/usr/lib/$so" "$T/$so"
done
for f in NOTICE GPL-2 GPL-3 LGPL-2.1 7zip-License 7zip-unRarLicense; do
install -Dm644 "tools/msitools/licenses/$f" "$D/usr/local/share/licenses/droiddeck-msitools/$f"
done
rm -rf msi-pkgs
ls -l "$T"
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop "$D/usr/local/bin/droiddeck-desktop"
install -Dm644 tools/linuxfs/desktop/droiddeck-gpu "$D/usr/local/bin/droiddeck-gpu"
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop-gpu "$D/usr/local/bin/droiddeck-desktop-gpu"
install -Dm644 tools/linuxfs/desktop/kwin_wayland_wrapper "$D/usr/local/bin/kwin_wayland_wrapper"
install -Dm644 tools/linuxfs/desktop/firefox-droiddeck.js "$D/usr/lib/firefox/defaults/pref/droiddeck.js"
ls -lR "$D"
# The Debian 11 image the preloads are compiled in comes from a release on this repo
# (published once by ci-debian-image.yml, pinned in tools/linuxfs/debian-image.env): the
# public registry mirrors rate-limit the shared runner addresses and failed builds on a
# busy evening. Only if that download fails is the image pulled, with retries.
- name: Fetch the Debian image the preloads build in
if: steps.preloads.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
. tools/linuxfs/debian-image.env
image=public.ecr.aws/docker/library/debian:bullseye
if gh release download "$DEBIAN_IMAGE_TAG" -R "$GITHUB_REPOSITORY" -p "$DEBIAN_IMAGE_ASSET" -O /tmp/debian-image.tar.zst \
&& { [ -z "$DEBIAN_IMAGE_SHA256" ] || echo "$DEBIAN_IMAGE_SHA256 /tmp/debian-image.tar.zst" | sha256sum -c -; }; then
zstd -dc /tmp/debian-image.tar.zst | docker load
rm -f /tmp/debian-image.tar.zst
else
echo "::warning::the hosted Debian image could not be fetched; pulling from the registry instead"
for attempt in 1 2 3 4 5; do
docker pull "$image" && break
[ "$attempt" -lt 5 ] || { echo "::error::could not pull $image after $attempt attempts"; exit 1; }
echo "pull attempt $attempt failed (registry busy?); retrying in $((attempt * 30)) s"
sleep $((attempt * 30))
done
fi
docker image ls "$image"
- name: Build the x86 preloads FEX guests load (glibc 2.31)
if: steps.preloads.outputs.cache-hit != 'true'
run: |
set -euo pipefail
docker run --rm -v "$PWD:/src" -w /src public.ecr.aws/docker/library/debian:bullseye bash -c '
set -euo pipefail
printf "deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian-security bullseye-security main\n" > /etc/apt/sources.list
apt-get -o Acquire::Check-Valid-Until=false update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends gcc g++ gcc-multilib g++-multilib binutils >/dev/null
tools/linuxfs/build-x86-preloads.sh app/src/main/assets/linuxfs
'
sudo chown -R "$(id -u):$(id -g)" app/src/main/assets/linuxfs
ls -l app/src/main/assets/linuxfs/x86_64 app/src/main/assets/linuxfs/i386
- name: Bundle the droiddeck-esync packs
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [ -f tools/droiddeck-esync/release.env ]; then
. tools/droiddeck-esync/release.env
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p "$SYNC_BUNDLE_ASSET" -O sync-bundle.tzst
echo "$SYNC_BUNDLE_SHA256 sync-bundle.tzst" | sha256sum -c -
mkdir -p app/src/main/assets/droiddeck-esync
tar --use-compress-program=unzstd -xf sync-bundle.tzst -C app/src/main/assets/droiddeck-esync
rm sync-bundle.tzst
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p index.json -O sync-index.json
revoked=$(python3 -c 'import json, sys; print(" ".join(p["id"] for p in json.load(open(sys.argv[1]))["packs"] if p.get("revoked") is True))' sync-index.json)
for id in $revoked; do
if [ -e "app/src/main/assets/droiddeck-esync/packs/$id.tzst" ]; then
echo "::warning::$SYNC_BUNDLE_ASSET carries revoked pack $id; it is left out of the APK (point tools/droiddeck-esync/release.env at a newer bundle)"
rm -f "app/src/main/assets/droiddeck-esync/packs/$id.tzst"
fi
done
rm sync-index.json
test -f app/src/main/assets/droiddeck-esync/index.json
test -f app/src/main/assets/droiddeck-esync/index.json.sig
ls -lR app/src/main/assets/droiddeck-esync
fi
- name: Cache PulseAudio 13.0 source
id: pa-src
uses: actions/cache@v4
with:
path: pa13
key: pulseaudio-src-13.0
- name: Fetch PulseAudio 13.0 source
if: steps.pa-src.outputs.cache-hit != 'true'
run: |
set -euo pipefail
curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1 && rm pa.tar.gz
# DirectAudio - the game driver (one set per Wine audio interface), the Android-side helper and
# the Steam client's sinks - comes from a pinned release of The412Banner/directaudio, checksummed,
# exactly like gamescope; nothing compiled for audio is kept in this repo.
- name: Fetch DirectAudio (tools/directaudio/release.env)
run: tools/directaudio/fetch.sh "$PWD" "$PWD/sink-out"
- name: Build the AAudio sink into the audio bundle
run: |
set -euo pipefail
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/sink-out"
rm -rf pa-bundle && mkdir -p pa-bundle
tar --use-compress-program=unzstd -xf app/src/main/assets/pulseaudio.tzst -C pa-bundle
test ! -f pa-bundle/modules/arm64/module-aaudio-sink.so
install -m755 sink-out/module-aaudio-sink.so pa-bundle/modules/arm64/module-aaudio-sink.so
install -m755 sink-out/module-directaudio-native-sink.so pa-bundle/modules/arm64/module-directaudio-native-sink.so
(cd pa-bundle && tar --use-compress-program='zstd -19' -cf ../pulseaudio.tzst .)
mkdir -p app/build/prepared-assets
mv pulseaudio.tzst app/build/prepared-assets/pulseaudio.tzst
tar --use-compress-program=unzstd -tf app/build/prepared-assets/pulseaudio.tzst | grep -c '\.so$'
rm -rf pa-bundle sink-out
- name: Cache proot
id: proot
uses: actions/cache@v4
with:
path: |
app/src/main/jniLibs/arm64-v8a/libproot.so
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
key: proot-${{ env.NDK_VERSION }}-${{ hashFiles('tools/proot/**') }}
- name: Build proot
if: steps.proot.outputs.cache-hit != 'true'
run: |
set -euo pipefail
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
tools/proot/build.sh app/src/main/jniLibs/arm64-v8a
- name: Check the preloads
run: |
set -euo pipefail
bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs
# The store download engines (app/src/main/rust/stores) are Rust, built by Gradle's
# buildRustStores task through cargo-ndk. rustup and cargo are on the runner image; the Android
# target and cargo-ndk are added here. Cache = cargo registry + cargo-ndk + the crate's target
# tree, keyed on the rustc version and Cargo.lock, so a source-only change rebuilds this crate
# on top of the cached dependency objects, and a lockfile change still starts from the
# previous objects (restore-keys).
- name: Rust toolchain for the store engines (aarch64-linux-android)
id: rust
run: |
set -euo pipefail
rustup target add aarch64-linux-android
echo "rustc=$(rustc -V | awk '{print $2}')" >> "$GITHUB_OUTPUT"
rustc -V; cargo -V
- name: Cache cargo registry and the store engines' target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin/cargo-ndk
app/src/main/rust/stores/target
key: stores-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}
restore-keys: |
stores-${{ steps.rust.outputs.rustc }}-
- name: cargo-ndk
run: |
set -euo pipefail
command -v cargo-ndk >/dev/null || cargo install cargo-ndk --locked
cargo ndk --version
- name: Cache the CMake build tree
uses: actions/cache@v4
with:
path: |
app/.cxx
app/build/intermediates/cxx
key: cxx-${{ env.NDK_VERSION }}-${{ hashFiles('app/src/main/cpp/**') }}
restore-keys: |
cxx-${{ env.NDK_VERSION }}-
- name: Build
id: build
env:
DROIDDECK_BUILD_LABEL: ${{ steps.build-label.outputs.label }}
SGDB_API_KEY: ${{ secrets.SGDB_API_KEY }}
run: |
set -o pipefail
python3 tools/release/runtime_inputs.py record
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
python3 tools/release/check_session_assets.py app/build/outputs/apk/release/app-release.apk
python3 tools/release/runtime_inputs.py apk app/build/outputs/apk/release/app-release.apk
- name: Check the store engines' library
# The exports Kotlin binds must be in the apk's libdroiddeckstores.so, or the stores fail on
# the device with an UnsatisfiedLinkError instead of here. The lockfile check is the
# reproducibility guard: cargo must not have had to re-resolve anything.
run: |
set -euo pipefail
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
python3 tools/release/check_android_libraries.py app/build/outputs/apk/release/app-release.apk \
--readelf "$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf"
git diff --stat -- app/src/main/rust/stores/Cargo.lock
git diff --exit-code -- app/src/main/rust/stores/Cargo.lock \
|| { echo "::error::Cargo.lock changed during the build; commit the regenerated lockfile (uploaded as stores-cargo-lock)"; exit 1; }
- name: Store engines host unit tests
# The crate's #[test] functions are pure host-side logic (manifest parsing, chunk plans,
# assembly from a cache, hashes), so a plain `cargo test` on the runner's own architecture
# runs them; the Android build above compiles none of them. Informational on this first
# run - the point is to learn whether they pass - and bounded so a hang can never hold
# the apk. Make it blocking once it is known green.
timeout-minutes: 10
continue-on-error: true
run: |
cd app/src/main/rust/stores
cargo test --locked 2>&1 | tail -60
- name: Upload the regenerated Cargo.lock
if: failure()
uses: actions/upload-artifact@v4
with:
name: stores-cargo-lock
path: app/src/main/rust/stores/Cargo.lock
if-no-files-found: ignore
- name: Steam import and game environment tests
run: python3 -m unittest discover -s tools/tests -p 'test_*.py'
- name: Unit tests
run: ./gradlew testReleaseUnitTest --console=plain -PndkVersion="$NDK_VERSION"
- name: Sign with the testkey (v1 + v2 + v3)
run: |
set -euo pipefail
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
TOOLS=$(find "$ANDROID_HOME/build-tools" -maxdepth 1 -type d | sort -V | tail -1)
"$TOOLS/zipalign" -p -f 4 "$APK" "$APK.aligned"
"$TOOLS/apksigner" sign \
--ks keystore/testkey.p12 --ks-type PKCS12 --ks-pass pass:android \
--ks-key-alias testkey --key-pass pass:android \
--v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true \
--out "$APK" "$APK.aligned"
rm -f "$APK.aligned" "$APK.idsig"
- name: Check the signature
run: |
set -euo pipefail
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
APKSIGNER=$(find "$ANDROID_HOME/build-tools" -name apksigner | sort -V | tail -1)
"$APKSIGNER" verify --min-sdk-version 21 --verbose --print-certs "$APK" | tee /tmp/sig.txt
unzip -l "$APK" | grep -qE 'META-INF/.*\.(SF|RSA|DSA)$' \
|| { echo "::error::apk carries no JAR signature files"; exit 1; }
for scheme in "v1 scheme (JAR signing): true" "v2 scheme (APK Signature Scheme v2): true" \
"v3 scheme (APK Signature Scheme v3): true"; do
grep -qF "$scheme" /tmp/sig.txt || { echo "::error::apk is not signed with $scheme"; exit 1; }
done
grep -qi "CN=Android, OU=Android, O=Android" /tmp/sig.txt \
|| { echo "::error::apk is not signed with the AOSP testkey"; exit 1; }
- name: Report on the pull request
if: always() && github.event_name == 'pull_request'
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
BUILD_OUTCOME: ${{ steps.build.outcome }}
SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
run: |
set -uo pipefail
MARK="<!-- droiddeck-pr-build -->"
{
echo "$MARK"
echo "## Build of this pull request"
echo
[ -f /tmp/pr-merge.md ] && cat /tmp/pr-merge.md
echo
case "$BUILD_OUTCOME" in
success)
echo "### Build passed"
echo
echo "The \`droiddeck-apk\` artifact is [on this run]($RUN_URL), signed with the public test key."
echo
if [ "$SAME_REPO" = true ]; then
echo "A release-signed tester build that installs over Stable or Preview is being published now and will appear in DroidDeck's **Test builds** list. Label the PR \`no-test-build\` to skip that."
else
echo "**Maintainer:** add the \`publish-test\` label to publish a release-signed tester build that installs over Stable or Preview and appears in DroidDeck's **Test builds** list. The label is removed automatically after publishing."
fi
echo
echo "Fallback: [open **PR Release Publish**]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/workflows/pr-release-publish.yml) on \`main\` and enter PR \`$PR\`."
;;
failure)
echo "### ERROR: Build failed"
echo
echo "The compiler's errors ([full log]($RUN_URL)):"
echo
echo '```'
{ grep -E '^e: |^w: .*error|error:|CMake Error|fatal error|ninja: error|FAILED|What went wrong|Execution failed for task|Unresolved reference|Type mismatch|Unresolved' /tmp/build.log 2>/dev/null \
| sed -e 's#file:///home/runner/work/[^/]*/[^/]*/##' -e 's#/home/runner/work/[^/]*/[^/]*/##' \
| grep -v '^w: ' | awk '!seen[$0]++' | head -80; } || true
echo '```'
echo
echo "<details><summary>What went wrong (Gradle)</summary>"
echo
echo '```'
awk '/^\* What went wrong/{p=1} /^\* Try:/{p=0} p{print}' /tmp/build.log 2>/dev/null | head -60
echo '```'
echo "</details>"
;;
skipped|cancelled)
echo "### Not built"
echo
echo "The build did not run (see above, or [the run]($RUN_URL))."
;;
esac
echo
} > /tmp/pr-report.md
cat /tmp/pr-report.md >> "$GITHUB_STEP_SUMMARY"
ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq ".[] | select(.body | startswith(\"$MARK\")) | .id" | head -1 || true)
if [ -n "$ID" ]; then
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${ID}" -F body=@/tmp/pr-report.md >/dev/null
else
gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" -F body=@/tmp/pr-report.md >/dev/null
fi
- uses: actions/upload-artifact@v4
with:
name: droiddeck-apk
path: app/build/outputs/apk/release/*.apk
if-no-files-found: error
retention-days: 30
compression-level: 0
sign:
needs: build
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
permissions:
contents: read
actions: read
uses: ./.github/workflows/sign.yml
with:
environment: signing-main
variants: ${{ needs.build.outputs.variants }}
secrets: inherit
# Each signed main build as a prerelease of the public Droid-Deck/DroidDeck-CI (main-<sha>, the
# last KEEP kept), where anyone can download it without a GitHub account, then a post in the
# Discord's builds channel linking to it. The release is made with a token of the org's
# "DroidDeck CI Publisher" app (installed on DroidDeck-CI only, Contents: write): its ID in the
# DROIDDECK_CI_APP_ID variable, its private key in the DROIDDECK_CI_APP_KEY secret.
# DISCORD_BUILDS_WEBHOOK is the channel's webhook URL.
announce:
needs: [build, sign]
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
env:
CI_REPO: Droid-Deck/DroidDeck-CI
KEEP: 10
steps:
- uses: actions/download-artifact@v4
with:
pattern: droiddeck-signed-*
merge-multiple: true
path: signed
- uses: actions/create-github-app-token@v2
id: ci-token
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
owner: Droid-Deck
repositories: DroidDeck-CI
# What this build is, for the top of the release notes: the merged PR's title and the first
# paragraph of its description, or the commit's subject for a push made without a PR.
- name: Describe the change
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
pr=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \
--jq '[.[] | select(.merged_at != null and .base.ref == "main")] | first // empty')
if [ -n "$pr" ]; then
number=$(jq -r .number <<<"$pr")
title=$(jq -r .title <<<"$pr")
merged=$(jq -r '.merged_at[:10]' <<<"$pr")
summary=$(jq -r '.body // ""' <<<"$pr" | tr -d '\r' | { grep -v '^🤖' || true; } |
awk '!p && /^#/ { next } NF { p = 1; print; next } p { exit }' | head -n 6 | cut -c1-400)
{
echo "**Merged to main** - [PR #$number]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$number): $title (merged $merged)"
if [ -n "$summary" ]; then
echo
echo "$summary"
fi
} > notes-head.md
else
subject=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA" --jq '.commit.message | split("\n")[0]')
echo "**Pushed to main** without a PR - $subject" > notes-head.md
fi
cat notes-head.md
- name: Publish to DroidDeck-CI
id: publish
env:
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
run: |
set -euo pipefail
tag=main-${GITHUB_SHA::7}
cat signed/SHA256SUMS-*.txt > signed/SHA256SUMS.txt
rm signed/SHA256SUMS-*.txt
notes="$(cat notes-head.md)
Signed build of $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA - in main, not yet in a DroidDeck release; for testing.
Commit: $GITHUB_SHA
VersionCode: $(sed -n 's/^ *versionCode \([0-9][0-9]*\)$/\1/p' app/build.gradle)
Each apk installs over a DroidDeck release and keeps its data. \`standard\` is the normal app; the others are the same app under another package name, installed beside it.
SHA-256:
\`\`\`
$(cat signed/SHA256SUMS.txt)
\`\`\`"
gh release delete "$tag" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null || true
gh release create "$tag" -R "$CI_REPO" --target main --prerelease \
--title "main ${GITHUB_SHA::7}" --notes "$notes" signed/*
# Keep the newest main builds by publication time. GitHub App releases can share createdAt.
gh release list -R "$CI_REPO" -L 200 --json tagName,publishedAt \
--jq '[.[] | select(.tagName | startswith("main-"))] | sort_by(.publishedAt) | reverse | .['"$KEEP"':][] | .tagName' |
while IFS= read -r old; do
# Never remove this run's release, even if the API returns an unexpected ordering.
if [ "$old" = "$tag" ]; then
continue
fi
# Another main publish may have removed the same old release concurrently.
if ! gh release delete "$old" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null; then
if gh release view "$old" -R "$CI_REPO" >/dev/null 2>&1; then
echo "::error::Could not delete old release $old"
exit 1
fi
fi
done
published=$(gh release view "$tag" -R "$CI_REPO" --json tagName,assets)
jq -e --arg tag "$tag" '.tagName == $tag and (.assets | length) > 0' <<< "$published" >/dev/null
url="$GITHUB_SERVER_URL/$CI_REPO/releases/tag/$tag"
apk=$(cd signed && ls DroidDeck-*.apk | awk 'length(a) == 0 || length($0) < length(a) { a = $0 } END { print a }')
echo "release=$url" >> "$GITHUB_OUTPUT"
echo "apk=$GITHUB_SERVER_URL/$CI_REPO/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
echo "Published $url"
- name: Post to Discord
env:
WEBHOOK: ${{ secrets.DISCORD_BUILDS_WEBHOOK }}
GH_TOKEN: ${{ github.token }}
RELEASE: ${{ steps.publish.outputs.release }}
APK: ${{ steps.publish.outputs.apk }}
run: |
set -euo pipefail
if [ -z "$WEBHOOK" ]; then
echo "::notice::No DISCORD_BUILDS_WEBHOOK secret; nothing posted"
exit 0
fi
commit=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA")
subject=$(jq -r '.commit.message | split("\n")[0]' <<<"$commit")
# A merge commit's subject is "Merge pull request #N from ..."; its body's first line is
# the PR's title, which says more.
title=$(jq -r '.commit.message | split("\n") | map(select(length > 0)) | .[1] // ""' <<<"$commit")
pr=$(sed -n 's/^Merge pull request #\([0-9]*\).*/\1/p' <<<"$subject")
if [ -n "$pr" ]; then
what="[#$pr]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$pr): ${title:-$subject}"
else
what="[\`${GITHUB_SHA::7}\`]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA): $subject"
fi
get=$(printf 'Download: [standard APK](<%s>) · [all variants](<%s>)' "$APK" "$RELEASE")
content=$(printf '**New signed build** `main-%s`\n%s\n%s' "${GITHUB_SHA::7}" "$what" "$get")
jq -n --arg content "$content" '{content: $content, allowed_mentions: {parse: []}}' |
curl -fsS -H 'Content-Type: application/json' -d @- "$WEBHOOK" -o /dev/null
echo "Posted: $content"
catalog:
needs: announce
if: ${{ always() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && needs.announce.result != 'skipped' }}
uses: ./.github/workflows/update-catalog.yml
secrets: inherit
+55
View File
@@ -0,0 +1,55 @@
name: Publish the CI Debian image
# Pulls the Debian 11 image the x86 preloads build in, once, and publishes it as a release asset
# on this repo so build.yml never depends on a registry mirror's rate limit. Run it again (with a
# new tag) only to refresh the image; tools/linuxfs/debian-image.env names the one builds use.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish to (a new -rN tag for a refresh; builds pin the one in tools/linuxfs/debian-image.env)'
required: true
default: 'ci-debian-bullseye-r2'
replace:
description: 'Overwrite the asset if the release already has one (changes its checksum: re-pin it in debian-image.env)'
type: boolean
default: false
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Pull, save and publish
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
REPLACE: ${{ inputs.replace }}
run: |
set -euo pipefail
image=public.ecr.aws/docker/library/debian:bullseye
# A published image is pinned by checksum in the build; never replace one by accident.
if [ "$REPLACE" != true ] && gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json assets -q '.assets[].name' 2>/dev/null | grep -qx debian-bullseye.tar.zst; then
echo "::error::$TAG already carries debian-bullseye.tar.zst; publish to a new tag, or set replace and re-pin tools/linuxfs/debian-image.env"
exit 1
fi
for attempt in 1 2 3 4 5 6; do
docker pull "$image" && break
[ "$attempt" -lt 6 ] || { echo "::error::could not pull $image"; exit 1; }
echo "pull attempt $attempt failed (registry busy?); retrying in $((attempt * 30)) s"
sleep $((attempt * 30))
done
docker image inspect "$image" --format 'image id {{.Id}} created {{.Created}}'
docker save "$image" | zstd -19 -T0 -o debian-bullseye.tar.zst
sha256sum debian-bullseye.tar.zst | tee SHA256SUMS
ls -l debian-bullseye.tar.zst
if ! gh release view "$TAG" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release create "$TAG" -R "$GITHUB_REPOSITORY" --title "CI Debian 11 image ($TAG)" --latest=false \
--notes "The debian:bullseye image build.yml compiles the x86 preloads in (glibc 2.31), saved with docker save and compressed with zstd. Pulled from public.ecr.aws/docker/library/debian:bullseye on $(date -u +%Y-%m-%d); image id $(docker image inspect "$image" --format '{{.Id}}'). Pinned in tools/linuxfs/debian-image.env."
fi
gh release upload "$TAG" -R "$GITHUB_REPOSITORY" debian-bullseye.tar.zst SHA256SUMS --clobber
{ echo "Published \`$TAG\`:"; echo; echo '```'; cat SHA256SUMS; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
+78
View File
@@ -0,0 +1,78 @@
name: PR Release Cleanup
run-name: remove PR #${{ github.event.pull_request.number }}'s test build
# A merged or closed PR's test build leaves DroidDeck-CI, so the app's Updates page stops offering it
# (a merged fix is in the next Preview). pull_request_target for the publisher app's secrets; nothing
# from the PR is checked out or run.
on:
pull_request_target:
types: [ closed ]
branches: [ main ]
permissions:
contents: read
pull-requests: read
concurrency:
group: pr-release-${{ github.event.pull_request.number }}
cancel-in-progress: false
queue: max
jobs:
cleanup:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Check the current PR state
id: state
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
state=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .state)
echo "closed=$([ "$state" = closed ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
- uses: actions/create-github-app-token@v2
if: ${{ steps.state.outputs.closed == 'true' }}
id: ci-token
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
owner: Droid-Deck
repositories: DroidDeck-CI
- name: Delete the test build
if: ${{ steps.state.outputs.closed == 'true' }}
env:
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
found=0
for tag in "pr-$PR_NUMBER" "pr-$PR_NUMBER.0"; do
if gh release view "$tag" -R Droid-Deck/DroidDeck-CI >/dev/null 2>&1; then
gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
echo "Deleted $tag"
found=1
fi
done
[ "$found" = 1 ] || echo "No test build for PR #$PR_NUMBER"
- uses: actions/checkout@v4
if: ${{ steps.state.outputs.closed == 'true' }}
with:
ref: main
persist-credentials: false
- name: Retire the Discord PR announcement
if: ${{ steps.state.outputs.closed == 'true' }}
env:
WEBHOOK: ${{ secrets.DISCORD_PR_BUILDS_WEBHOOK }}
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: python3 tools/release/pr_discord.py retire
catalog:
needs: cleanup
if: ${{ always() }}
uses: ./.github/workflows/update-catalog.yml
secrets: inherit
+341
View File
@@ -0,0 +1,341 @@
name: PR Release Publish
run-name: "sign PR #${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }} with the release key"
on:
workflow_dispatch:
inputs:
pr_number:
description: Open pull request number
required: true
type: string
pull_request_target:
types: [labeled]
branches: [main]
# A PR from a branch of this repo is signed on its own after each green build: only people who
# can push here can change it. A fork's PR waits for a maintainer's publish-test label.
workflow_run:
workflows: [Build APK]
types: [completed]
permissions:
actions: read
contents: read
issues: write
pull-requests: write
concurrency:
group: pr-release-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
cancel-in-progress: false
queue: max
jobs:
authorize:
if: >-
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request_target' && github.event.label.name == 'publish-test') ||
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name == github.repository &&
github.event.workflow_run.pull_requests[0] != null)
runs-on: ubuntu-24.04
timeout-minutes: 45
outputs:
pr_number: ${{ steps.gate.outputs.pr_number }}
run_id: ${{ steps.gate.outputs.run_id }}
head_sha: ${{ steps.gate.outputs.head_sha }}
base_sha: ${{ steps.gate.outputs.base_sha }}
version_code: ${{ steps.gate.outputs.version_code }}
label: ${{ steps.gate.outputs.label }}
skip: ${{ steps.gate.outputs.skip }}
automatic: ${{ steps.gate.outputs.automatic }}
variants: ${{ steps.variants.outputs.list }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# Every variant a main build ships, so a PR build can be tried under any of them.
- name: The packages a signed build ships as
id: variants
run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT"
- name: Check the requester, PR, and build
id: gate
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
REQUESTER: ${{ github.actor }}
PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
BUILT_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number '$PR_NUMBER'"; exit 1; }
# The automatic path stands down quietly, never failing: a draft, a PR labelled
# no-test-build, or a build that a later push has already overtaken.
skip() { echo "::notice::Not publishing PR #$PR_NUMBER: $1"; echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
if [ "$EVENT_NAME" = workflow_run ]; then
echo "automatic=true" >> "$GITHUB_OUTPUT"
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
[ "$(jq -r .state <<<"$pr")" = open ] || skip "it is not open"
[ "$(jq -r .draft <<<"$pr")" = false ] || skip "it is a draft"
[ "$(jq -r .base.ref <<<"$pr")" = main ] || skip "it does not target main"
[ "$(jq -r .head.repo.full_name <<<"$pr")" = "$GITHUB_REPOSITORY" ] || skip "it comes from a fork"
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$pr" >/dev/null && skip "it is labelled no-test-build"
[ "$(jq -r .head.sha <<<"$pr")" = "$BUILT_SHA" ] || skip "a newer push is building"
fi
if [ "$EVENT_NAME" = pull_request_target ]; then
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$REQUESTER/permission" --jq .permission)
case "$permission" in
admin|maintain|write) ;;
*) echo "::error::@$REQUESTER does not have permission to publish release-signed test builds"; exit 1 ;;
esac
fi
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null ||
{ echo "::error::PR #$PR_NUMBER must be open, ready for review, and target main"; exit 1; }
head=$(jq -r '.head.sha' <<<"$pr")
base=$(jq -r '.base.sha' <<<"$pr")
# The label is often added while the PR's build is still running, or just after a push
# before it has started: wait for the latest build of the head commit to finish (a PR
# build takes about 4 minutes) rather than fail and need the label added again.
run_id=
for attempt in $(seq 1 60); do
latest=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \
--event pull_request --commit "$head" \
--json databaseId,headSha,status,conclusion,createdAt,attempt -L 100 |
jq -c --arg head "$head" '[.[] | select(.headSha == $head)] |
sort_by([.createdAt, .attempt]) | last // empty')
if [[ -n "$latest" && $(jq -r .status <<<"$latest") == completed ]]; then
conclusion=$(jq -r .conclusion <<<"$latest")
[[ "$conclusion" == success ]] || { echo "::error::The latest PR build is not green ($conclusion)"; exit 1; }
run_id=$(jq -r .databaseId <<<"$latest")
break
fi
status=not-started
[[ -z "$latest" ]] || status=$(jq -r .status <<<"$latest")
echo "Waiting for the PR build of ${head:0:7} to finish ($status)"
sleep 30
done
[[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; }
# GitHub leaves a run's pull_requests empty when its head is in a fork, so the build is
# tied to this PR by its head repository, branch and commit instead.
run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id")
jq -e --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" '
.event == "pull_request" and .head_sha == $head and .conclusion == "success" and
.head_repository.full_name == $repo and .head_branch == $ref
' <<<"$run" >/dev/null ||
{ echo "::error::Build run $run_id is not a green build of PR #$PR_NUMBER at ${head:0:7}"; exit 1; }
sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}
tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1)
# A PR build is made from the PR merged onto main, so it must install over main's own
# build. main is what this job checked out; no release has to exist or be marked latest.
main_code=$(sed -n 's/^[[:space:]]*versionCode[[:space:]][[:space:]]*\([0-9][0-9]*\)[[:space:]]*$/\1/p' app/build.gradle)
[[ "$main_code" =~ ^[0-9]+$ ]] || { echo "::error::No numeric versionCode in main's app/build.gradle"; exit 1; }
mkdir -p "$RUNNER_TEMP/pr"
gh run download "$run_id" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D "$RUNNER_TEMP/pr"
mapfile -t apks < <(find "$RUNNER_TEMP/pr" -type f -name '*.apk')
[[ ${#apks[@]} == 1 ]] || { echo '::error::Expected one PR APK'; exit 1; }
package_line() {
"$tools/aapt" dump badging "$1" | sed -n "s/^package: name='\\([^']*\\)' versionCode='\\([^']*\\)'.*/\\1 \\2/p"
}
candidate=$(package_line "${apks[0]}")
[[ "$candidate" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && ${candidate##* } -ge $main_code ]] || {
echo "::error::PR package/versionCode '$candidate' cannot install over main (versionCode $main_code)"; exit 1;
}
current=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
jq -e --arg head "$head" --arg base "$base" '
.state == "open" and .draft == false and .base.ref == "main" and
.head.sha == $head and .base.sha == $base
' <<<"$current" >/dev/null ||
{ echo "::error::PR #$PR_NUMBER moved on while its build was checked; publish again"; exit 1; }
{
echo "pr_number=$PR_NUMBER"
echo "run_id=$run_id"
echo "head_sha=$head"
echo "base_sha=$base"
echo "version_code=${candidate##* }"
echo "label=PR.$PR_NUMBER-${head:0:7}"
} >> "$GITHUB_OUTPUT"
sign:
needs: authorize
uses: ./.github/workflows/sign.yml
with:
environment: signing-main
variants: ${{ needs.authorize.outputs.variants }}
run_id: ${{ needs.authorize.outputs.run_id }}
label: ${{ needs.authorize.outputs.label }}
artifact: droiddeck-pr-signed
secrets: inherit
if: ${{ needs.authorize.outputs.skip != 'true' }}
publish:
needs: [authorize, sign]
runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
- uses: actions/download-artifact@v4
with:
pattern: droiddeck-pr-signed-*
merge-multiple: true
path: signed
# What this build is, for the top of the release notes: the PR's title and the first
# paragraph of its description.
- name: Describe the change
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
run: |
set -euo pipefail
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
title=$(jq -r .title <<<"$pr")
summary=$(jq -r '.body // ""' <<<"$pr" | tr -d '\r' | { grep -v '^🤖' || true; } |
awk '!p && /^#/ { next } NF { p = 1; print; next } p { exit }' | head -n 6 | cut -c1-400)
{
echo "**Not merged** - a test build of open [PR #$PR_NUMBER]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$PR_NUMBER): $title"
if [ -n "$summary" ]; then
echo
echo "$summary"
fi
} > notes-head.md
cat notes-head.md
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
- uses: actions/create-github-app-token@v2
id: ci-token
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
owner: Droid-Deck
repositories: DroidDeck-CI
- name: Publish to DroidDeck-CI
id: publish
env:
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
BASE_SHA: ${{ needs.authorize.outputs.base_sha }}
VERSION_CODE: ${{ needs.authorize.outputs.version_code }}
run: |
set -euo pipefail
repo=Droid-Deck/DroidDeck-CI
tag="pr-$PR_NUMBER"
cat signed/SHA256SUMS-*.txt > signed/SHA256SUMS.txt
rm signed/SHA256SUMS-*.txt
notes="$(cat notes-head.md)
Source: https://github.com/$GITHUB_REPOSITORY/pull/$PR_NUMBER
Commit: $HEAD_SHA
Base: $BASE_SHA
VersionCode: $VERSION_CODE
Each apk installs over a DroidDeck release and keeps its data. \`standard\` is the normal app; the others are the same app under another package name, installed beside it.
SHA-256:
\`\`\`
$(cat signed/SHA256SUMS.txt)
\`\`\`"
first=false
if ! gh release view "$tag" -R "$repo" >/dev/null 2>&1; then
gh release create "$tag" -R "$repo" --target main --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
first=true
fi
echo "first=$first" >> "$GITHUB_OUTPUT"
gh release upload "$tag" signed/* -R "$repo" --clobber
gh release edit "$tag" -R "$repo" --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
# Drop the previous build's files: anything this build did not just upload.
keep=$(cd signed && ls | jq -R . | jq -sc .)
gh api "repos/$repo/releases/tags/$tag" |
jq -r --argjson keep "$keep" '.assets[] | select(.name as $n | $keep | index($n) | not) | .id' |
while IFS= read -r id; do
gh api --method DELETE "repos/$repo/releases/assets/$id" >/dev/null
done
published=$(gh release view "$tag" -R "$repo" --json tagName,assets)
jq -e --arg tag "$tag" '.tagName == $tag and (.assets | length) > 0' <<<"$published" >/dev/null
release="$GITHUB_SERVER_URL/$repo/releases/tag/$tag"
apk=$(cd signed && ls DroidDeck-*.apk | awk 'length(a) == 0 || length($0) < length(a) { a = $0 } END { print a }')
echo "release=$release" >> "$GITHUB_OUTPUT"
echo "apk=$GITHUB_SERVER_URL/$repo/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
- name: Whether the PR's branch is in this repo
id: same-repo
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
run: |
head_repo=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .head.repo.full_name)
echo "same=$([ "$head_repo" = "$GITHUB_REPOSITORY" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
- name: Clear the publish request label
if: ${{ github.event_name == 'pull_request_target' && steps.publish.outcome == 'success' }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
run: |
gh api --method DELETE "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels/publish-test"
- name: Report the signed build on the PR
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
RELEASE: ${{ steps.publish.outputs.release }}
APK: ${{ steps.publish.outputs.apk }}
SAME_REPO: ${{ steps.same-repo.outputs.same }}
run: |
set -euo pipefail
short=${HEAD_SHA:0:7}
mark='<!-- droiddeck-pr-release -->'
if [ "$SAME_REPO" = true ]; then
NEXT='Each green build of a new push is signed and published here on its own. Label the PR `no-test-build` to stop that.'
else
NEXT='Pushes to this PR do not re-sign automatically. After a new green build, a maintainer can add the `publish-test` label again.'
fi
body=$(cat <<EOF
$mark
## Signed test build
A release-signed build of this PR at \`$short\` is ready. It installs over Stable or Preview and keeps app data.
[Download the standard APK]($APK) · [All variants and checksums]($RELEASE)
It is also available in **DroidDeck → Updates → Test builds → PR #$PR_NUMBER**.
$NEXT
EOF
)
comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate --slurp | \
jq "[.[][] | select(.user.login == \"github-actions[bot]\") | select(.body | startswith(\"$mark\"))] | first // {}")
id=$(jq -r '.id // empty' <<<"$comment")
if [ -n "$id" ]; then
state=$(jq -r '.body | split("\n")[] | select(startswith("<!-- droiddeck-discord: ") and endswith(" -->"))' <<<"$comment")
if [ -n "$state" ]; then body=$(printf '%s\n%s' "$body" "$state"); fi
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -f body="$body" >/dev/null
else
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null
fi
- name: Update the Discord PR announcement
if: ${{ always() && steps.publish.outcome == 'success' }}
env:
WEBHOOK: ${{ secrets.DISCORD_PR_BUILDS_WEBHOOK }}
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
RELEASE: ${{ steps.publish.outputs.release }}
APK: ${{ steps.publish.outputs.apk }}
run: python3 tools/release/pr_discord.py publish
catalog:
needs: publish
if: ${{ always() && needs.publish.result != 'skipped' }}
uses: ./.github/workflows/update-catalog.yml
secrets: inherit
+120
View File
@@ -0,0 +1,120 @@
name: Release APKs
run-name: release ${{ inputs.version }} from main ${{ github.sha }}
on:
workflow_dispatch:
inputs:
version:
description: Version for the file names, e.g. 0.1.7
required: true
permissions:
contents: read
actions: read
concurrency:
group: release
cancel-in-progress: false
jobs:
find:
name: find the main build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
outputs:
run_id: ${{ steps.find.outputs.run_id }}
variants: ${{ steps.find.outputs.variants }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- id: find
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
[[ $VERSION =~ ^[0-9A-Za-z._-]+$ ]] || { echo "::error::version '$VERSION': letters, digits, . _ - only"; exit 1; }
name=$(sed -n 's/^ *versionName "\(.*\)"$/\1/p' app/build.gradle)
[[ $VERSION == "$name" || $VERSION == "$name"-* ]] || {
echo "::error::version '$VERSION' does not match versionName '$name' in app/build.gradle: bump versionName and versionCode first"; exit 1;
}
runs=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml --branch main --commit "$GITHUB_SHA" \
--status success --json databaseId,event,headSha -L 20)
id=$(jq -r --arg sha "$GITHUB_SHA" \
'[.[] | select(.headSha == $sha and (.event == "push" or .event == "workflow_dispatch"))][0].databaseId // empty' <<<"$runs")
if [ -z "$id" ]; then
echo "::error::main ${GITHUB_SHA::7} has no successful Build APK run yet: wait for it, then release"
exit 1
fi
variants=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)
echo "run_id=$id" >> "$GITHUB_OUTPUT"
echo "variants=$variants" >> "$GITHUB_OUTPUT"
echo "Signing Build APK run $id (main ${GITHUB_SHA::7}) as $VERSION: $variants" >> "$GITHUB_STEP_SUMMARY"
sign:
needs: find
uses: ./.github/workflows/sign.yml
with:
environment: release
variants: ${{ needs.find.outputs.variants }}
run_id: ${{ needs.find.outputs.run_id }}
label: ${{ inputs.version }}
artifact: droiddeck-release
secrets: inherit
# The release is made here, from the apks this run signed, so it traces back to its Build APK run.
publish:
name: publish the release
needs: [find, sign]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
pattern: droiddeck-release-*
merge-multiple: true
path: out
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
BUILD_RUN: ${{ needs.find.outputs.run_id }}
run: |
set -euo pipefail
notes="docs/releases/$VERSION.md"
[ -f "$notes" ] || { echo "::error::$notes is missing: add the release notes first"; exit 1; }
if gh release view "$VERSION" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::error::release $VERSION already exists"; exit 1
fi
# The notes' first line is the title; media paths become raw URLs at this release's tag.
title=$(sed -n '1s/^# *//p' "$notes")
[ -n "$title" ] || title="DroidDeck $VERSION"
raw="https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$VERSION/docs/releases/media/"
{
sed '1{/^# /d;}' "$notes" | sed -E "s#(\]\(|src=\")media/#\1$raw#g"
echo
echo "---"
echo
echo "Built by [Build APK run $BUILD_RUN]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN) from \`$GITHUB_SHA\`, signed and published by [Release APKs run $GITHUB_RUN_ID]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
} > "$RUNNER_TEMP/body.md"
( cd out && cat SHA256SUMS-*.txt | sort -k2 > SHA256SUMS.txt && rm SHA256SUMS-*.txt && cat SHA256SUMS.txt )
gh release create "$VERSION" -R "$GITHUB_REPOSITORY" out/*.apk out/SHA256SUMS.txt \
--target "$GITHUB_SHA" --title "$title" --notes-file "$RUNNER_TEMP/body.md" --latest
echo "Published [$title]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$VERSION)" >> "$GITHUB_STEP_SUMMARY"
# A release made with this run's token fires no release event, so the in-app catalog is refreshed here.
catalog:
needs: publish
uses: ./.github/workflows/update-catalog.yml
permissions:
contents: read
pull-requests: read
secrets: inherit
+95
View File
@@ -0,0 +1,95 @@
name: Sign APKs
on:
workflow_call:
inputs:
environment:
type: string
required: true
variants:
description: JSON array of names from tools/release/variants.txt
type: string
required: true
run_id:
description: The Build APK run whose apk to sign; empty for the calling run's own
type: string
default: ''
label:
description: Goes in the file names (a version); empty for <branch>-<short sha>
type: string
default: ''
artifact:
type: string
default: droiddeck-signed
permissions:
contents: read
actions: read
jobs:
sign:
name: sign ${{ matrix.variant }}
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: ${{ inputs.environment }}
strategy:
fail-fast: false
matrix:
variant: ${{ fromJSON(inputs.variants) }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: The apk (this run's build)
if: inputs.run_id == ''
uses: actions/download-artifact@v4
with:
name: droiddeck-apk
path: in
- name: The apk (a main build)
if: inputs.run_id != ''
env:
GH_TOKEN: ${{ github.token }}
RUN: ${{ inputs.run_id }}
run: gh run download "$RUN" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D in
- name: Sign
env:
RELEASE_KEYSTORE_B64: ${{ secrets.RELEASE_KEYSTORE_B64 }}
RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}
RELEASE_KEY_ALIAS: droiddeck
ENVIRONMENT: ${{ inputs.environment }}
VARIANT: ${{ matrix.variant }}
LABEL: ${{ inputs.label }}
run: |
set -euo pipefail
if [ -z "$RELEASE_KEYSTORE_B64" ]; then
echo "::error::environment '$ENVIRONMENT' has no signing key: run tools/release/make-release-key.sh once"
exit 1
fi
export RELEASE_KEYSTORE="$RUNNER_TEMP/release.p12"
( umask 077; base64 -d <<<"$RELEASE_KEYSTORE_B64" > "$RELEASE_KEYSTORE" )
BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -n1)
export BUILD_TOOLS
[ -n "$LABEL" ] || LABEL="${GITHUB_REF_NAME//\//-}-${GITHUB_SHA::7}"
suffix=$(awk -v v="$VARIANT" '!/^#/ && $1 == v { print $3 }' tools/release/variants.txt)
[ "$suffix" = - ] && suffix=
apks=(in/*.apk)
[ ${#apks[@]} = 1 ] && [ -f "${apks[0]}" ] || { echo "::error::expected one apk in the build"; exit 1; }
mkdir -p out
out="out/DroidDeck-$LABEL$suffix.apk"
tools/release/sign-apk.sh "${apks[0]}" "$VARIANT" "$out"
( cd out && sha256sum -- *.apk > "SHA256SUMS-$VARIANT.txt" && cat "SHA256SUMS-$VARIANT.txt" )
echo "- \`$(basename "$out")\` - $(cut -c1-16 "out/SHA256SUMS-$VARIANT.txt")…" >> "$GITHUB_STEP_SUMMARY"
- name: Forget the key
if: always()
run: rm -f "$RUNNER_TEMP/release.p12"
- uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact }}-${{ matrix.variant }}
path: out/*
if-no-files-found: error
+43
View File
@@ -0,0 +1,43 @@
name: Update app catalog
on:
workflow_call:
workflow_dispatch:
release:
types: [published, edited, deleted, unpublished]
permissions:
contents: read
pull-requests: read
concurrency:
group: update-app-catalog
cancel-in-progress: false
queue: max
jobs:
catalog:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
# Always use the current trusted generator, even when a release event points at an older tag.
- uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
- uses: actions/create-github-app-token@v2
id: ci-token
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
owner: Droid-Deck
repositories: DroidDeck-CI
- name: Regenerate catalog.json
env:
SOURCE_GITHUB_TOKEN: ${{ github.token }}
CI_GITHUB_TOKEN: ${{ steps.ci-token.outputs.token }}
run: |
set -euo pipefail
python3 tools/release/update_catalog.py
+42
View File
@@ -0,0 +1,42 @@
*.iml
.gradle/
local.properties
.idea/
.DS_Store
build/
captures/
.externalNativeBuild/
.cxx/
.kotlin/
# Built in CI from tools/linuxfs and app/src/main/cpp/fakeinput.cpp, never committed, so the two
# copies cannot drift.
app/src/main/assets/linuxfs/
app/src/main/jniLibs/arm64-v8a/libproot.so
app/src/main/jniLibs/arm64-v8a/.proot-inputs
app/src/main/jniLibs/arm64-v8a/.proot-outputs
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so
# The store download engines (app/src/main/rust/stores) are cross-compiled by Gradle into
# app/build/rustJniLibs; cargo's own build tree stays next to the crate. Neither is source.
app/src/main/rust/stores/target/
app/src/main/assets/droiddeck-esync/
# Kotlin compiler session files (build-tool state, never source).
.kotlin/
# Signing keys never enter the repo (DroidDeck's lives in GitHub environment secrets; see
# tools/release/make-release-key.sh). keystore/testkey.p12 is the public AOSP testkey.
*.jks
*.keystore
*.p12
*.pfx
*.pk8
*.pem
!keystore/testkey.p12
droiddeck-release-key/
password.txt
__pycache__/
# Local signing with the release key (tools/build_local.sh); holds a password.
.signing.env
app/src/main/assets/directaudio/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Pluvia is an unofficial Steam client app for Android
Copyright (C) 2024 Oxters Wyzgowski
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Pluvia Copyright (C) 2024 Oxters Wyzgowski
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+40
View File
@@ -0,0 +1,40 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="artwork/droiddeck-banner-dark.svg">
<img alt="DroidDeck" src="artwork/droiddeck-banner-light.svg" width="100%">
</picture>
</p>
DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big Picture on your Adreno handheld, with Windows games through Valve's ARM64 Proton.
<p align="center"><a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a></p>
<p align="center"><img src="docs/releases/media/0.2.0/launch-into-steam.gif" width="80%" alt="Tapping DroidDeck on the Android home screen and landing in Steam Big Picture"></p>
> Note: DroidDeck does not have a stand-alone website. Do not click on any download links from websites claiming to be the DroidDeck team.
## Requirements and install
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts. **Stores** (Setup › Stores › "Show Stores in the rail") signs in to GOG, Epic Games and Amazon Games, browses their catalogs and libraries, downloads games into the Games storage and adds each one to the Steam client as a non-Steam game, so it launches through Proton like everything else; the cog on its chip row sets which tab a store opens on and the download speed tier.
Before Steam launches, you must turn off **Restrict child processes** in Developer options. If this option is not available in developer settings (Android 12 and 13 devices), first launch of Steam will present a "Fix it for me" button, which will help automate the setup process.
## Community
Join the [DroidDeck Discord](https://discord.gg/JRGAvawjsm) for help, Preview builds, and device reports. Bug reports go in its **#bug-reports** forum; attach the zip from **Share logs** (Setup, or the session drawer) so the logs come with it.
## Build
Run `tools/build_local.sh` with Docker, Java 17, the Android SDK/NDK, Rust through rustup, GitHub CLI, Python 3, and `zstd` installed. It prepares the same native libraries, pinned components and audio sinks as CI, then checks the APK's assets, JNI exports and dependencies. The APK is at `app/build/outputs/apk/release/app-release.apk`. Set `DROIDDECK_PA13_SOURCE_DIR` to an existing PulseAudio 13.0 source directory to skip downloading it. Set `DROIDDECK_SIGNING_ENV` to a signing environment file to install over a release build; relative keystore paths resolve beside that file. To install on an attached device, run `tools/deploy_local.sh`.
After preparation, ordinary `./gradlew :app:assembleRelease` builds can reuse the verified inputs. If native sources, component pins or staged files change, packaging stops with instructions to rerun the helper. Generated audio stays under `app/build/`; the tracked base bundle is never modified. `./gradlew -PskipRust=true :app:testDebugUnitTest` needs no native preparation. For an APK without store engines, use `DROIDDECK_SKIP_RUST=1 tools/build_local.sh`; it omits cached engine libraries too.
Run the helper and preload regression suite with `bash tools/test_local.sh`. On macOS it runs in Linux through Docker, matching the helpers' runtime and CI instead of compiling Linux preloads against macOS headers.
## Limits
Compatibility and performance vary by device; hardware validation is limited. Desktop compositing uses software rendering. Firefox sandboxing is reduced under proot. Logs are kept app-private (`files/logs/`, the last 30 sessions); **Share logs** packs the newest session, with the stores and tool logs, into one scrubbed zip.
## Credits and licence
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1000 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 KiB

+305
View File
@@ -0,0 +1,305 @@
plugins {
id 'com.android.application'
}
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
def gitValue = { List<String> arguments ->
try {
def output = providers.exec {
commandLine(['git'] + arguments)
workingDir rootProject.projectDir
ignoreExitValue = true
}
output.result.get().exitValue == 0 ? output.standardOutput.asText.get().trim() : null
} catch (Exception ignored) {
null
}
}
def buildLabel = providers.environmentVariable('DROIDDECK_BUILD_LABEL').getOrNull()
if (buildLabel == null || buildLabel.trim().isEmpty()) {
def eventName = providers.environmentVariable('GITHUB_EVENT_NAME').getOrNull()
def eventPath = providers.environmentVariable('GITHUB_EVENT_PATH').getOrNull()
if (eventName == 'pull_request_target' && eventPath) {
try {
def eventFile = layout.file(providers.provider { new File(eventPath) })
def eventText = providers.fileContents(eventFile).asText.get()
def pullRequest = new groovy.json.JsonSlurper().parseText(eventText).pull_request
if (pullRequest?.number && pullRequest?.head?.sha) {
buildLabel = "pr${pullRequest.number} at ${pullRequest.head.sha.take(7)}"
}
} catch (Exception ignored) {
}
}
if (buildLabel == null || buildLabel.trim().isEmpty()) {
def branch = providers.environmentVariable('GITHUB_REF_NAME').getOrNull()
?: gitValue(['branch', '--show-current']) ?: 'local'
def commit = providers.environmentVariable('GITHUB_SHA').getOrNull()
?.take(7) ?: gitValue(['rev-parse', '--short=7', 'HEAD']) ?: 'unknown'
buildLabel = "${branch} at ${commit}"
}
}
def buildOrigin = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true' ? 'remote' : 'local'
def treeState = providers.environmentVariable('DROIDDECK_BUILD_TREE_STATE').getOrNull()
if (!(treeState in ['clean', 'dirty'])) {
def status = gitValue(['status', '--porcelain'])
treeState = status == null ? 'unknown' : (status.isEmpty() ? 'clean' : 'dirty')
}
buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}"
def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"')
// What the in-app updater compares against the published builds: the commit this build is of (a PR
// build's head, not the merge CI makes of it), its PR, and whether CI made it at all.
def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull()
?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: ''
def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' }
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
// The release key's certificate: a build signed with it, from CI or a computer, updates in place.
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
// When the source was committed, for "built 3 hours ago": fixed per commit, so it costs no rebuilds.
def commitTime = (gitValue(['show', '-s', '--format=%ct', 'HEAD']) ?: '0').with { it.isLong() ? it : '0' }
// The desktop package this apk installs (tools/desktop-kde/release.env): published once per tag in
// DroidDeck-Components, pinned here with its checksum like gamescope and the esync packs, so the
// desktop scripts in the apk always get the package they were made for.
def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
.findAll { it.contains('=') && !it.startsWith('#') }
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
def desktopRemovalSources = new groovy.json.JsonSlurper().parse(file('src/main/assets/desktop-removal/sources.json'))
if (desktopRemovalSources.archives[desktopKde.DESKTOP_KDE_TAG] != desktopKde.DESKTOP_KDE_SHA256) {
throw new GradleException('Desktop removal inventories do not match the pinned archive')
}
if (!file('src/main/assets/desktop-removal/versions.txt').readLines().contains(desktopKde.DESKTOP_KDE_TAG)) {
throw new GradleException('Regenerate desktop removal inventories for ' + desktopKde.DESKTOP_KDE_TAG)
}
// tools/release/variants.txt as "package=suffix;...", so the updater picks the apk of the package it runs as.
def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines()
.findAll { it.trim() && !it.startsWith('#') }
.collect { def f = it.trim().split(/\s+/); "${f[1]}=${f[2] == '-' ? '' : f[2]}" }
.join(';')
android {
namespace 'com.droiddeck.launcher'
compileSdk 34
// The NDK is pinned by property so CI can hand in whatever the runner already has rather than
// downloading a second copy; see gradle.properties.
ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724'
defaultConfig {
applicationId "com.droiddeck.launcher"
minSdkVersion 26
// 28 is not a leftover. An app on targetSdk 29+ runs in a domain that may not execute a
// file out of its own data directory, and this app's entire job is to exec a rootfs:
// proot, gamescope, the Steam client and everything it launches all live under files/.
targetSdkVersion 28
versionCode 12
versionName "0.3.2"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
buildConfigField "boolean", "CI_BUILD", "${ciBuild}"
buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\""
buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L"
buildConfigField "String", "VARIANT_SUFFIXES", "\"${variantSuffixes}\""
buildConfigField "String", "SGDB_API_KEY", "\"${System.getenv("SGDB_API_KEY") ?: project.findProperty("sgdbApiKey") ?: ""}\""
buildConfigField "String", "DESKTOP_KDE_TAG", "\"${desktopKde.DESKTOP_KDE_TAG}\""
buildConfigField "String", "DESKTOP_KDE_URL", "\"https://github.com/${desktopKde.DESKTOP_KDE_REPO}/releases/download/${desktopKde.DESKTOP_KDE_TAG}/desktop-kde.tar.zst\""
buildConfigField "String", "DESKTOP_KDE_SHA256", "\"${desktopKde.DESKTOP_KDE_SHA256}\""
buildConfigField "long", "DESKTOP_KDE_SIZE", "${desktopKde.DESKTOP_KDE_SIZE}L"
ndk {
abiFilters 'arm64-v8a'
}
externalNativeBuild {
cmake {
arguments '-DANDROID_STL=c++_shared'
}
}
}
externalNativeBuild {
cmake {
path 'src/main/cpp/CMakeLists.txt'
version '3.22.1'
}
}
signingConfigs {
// AOSP testkey ([email protected]), the same key Bannerlator ships with. It is publicly
// known, so it proves nothing about who built the apk - what it buys is ONE stable
// signature: every build installs over the last one instead of asking the user to
// uninstall first. v1 (JAR) + v2 + v3 so every installer and Android version accepts it.
testkey {
storeFile file("${rootDir}/keystore/testkey.p12")
storeType "PKCS12"
storePassword "android"
keyAlias "testkey"
keyPassword "android"
enableV1Signing = true
enableV2Signing = true
enableV3Signing = true
}
}
buildTypes {
debug {
minifyEnabled false
signingConfig signingConfigs.testkey
}
release {
minifyEnabled false
signingConfig signingConfigs.testkey
}
}
lint {
// targetSdk 28 is deliberate and structural: an app on 29+ may not execute a file out of
// its own data directory, and executing a rootfs is the whole of what this app does. Lint
// fails a release build over it because Google Play will not accept an sdk that old -
// true, and not something this app can trade away. Every other check stays on.
disable 'ExpiredTargetSdkVersion'
}
packagingOptions {
resources {
// Kadb's Bouncy Castle artifacts repeat the same multi-release JAR manifest.
excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF']
}
jniLibs {
// The PulseAudio daemon is executed from the native library directory and its
// libraries are read out of the apk by the class loader: both want the old,
// uncompressed-and-extracted layout.
useLegacyPackaging = true
// Prefer the PTY helper built by this app's NDK target over Termux's prebuilt copy.
pickFirsts += ['**/libtermux.so']
}
}
buildFeatures {
compose true
buildConfig true
}
testOptions {
// Plain JVM tests: a stray android.util.Log call returns a default instead of throwing.
unitTests.returnDefaultValues = true
// Robolectric tests start activities whose text is now all string resources.
unitTests.includeAndroidResources = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
// Session scripts must follow this checkout even when the native asset bundle is cached.
// A stale local bundle can omit a newly required helper on a fresh runtime install.
def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
from(rootProject.file('tools/linuxfs/overlay')) {
include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**'
}
into(layout.projectDirectory.dir('src/main/assets/linuxfs'))
}
tasks.named('preBuild').configure { dependsOn(stageSessionScripts) }
// Prepared audio lives under build/, so native builds never rewrite the tracked base bundle.
// Copying into one asset root also prevents an old base bundle winning a duplicate asset merge.
def runtimeAssetsDir = layout.buildDirectory.dir('runtimeAssets')
def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
dependsOn(stageSessionScripts)
from(layout.projectDirectory.dir('src/main/assets')) { exclude 'pulseaudio.tzst' }
from(layout.buildDirectory.dir('prepared-assets'))
into(runtimeAssetsDir)
}
android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir])
tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) }
// Unit tests and previews remain usable without Docker. Packaging an APK requires the complete
// runtime payload prepared by the local helper or CI, with matching sources and output hashes.
def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) {
// The session scripts are re-copied into the asset tree by stageSessionScripts; with parallel
// task execution this check once hashed a file mid-copy and failed a main build. Hash only
// once the staging has finished.
mustRunAfter(stageSessionScripts, stageRuntimeAssets)
workingDir rootProject.projectDir
commandLine 'python3', 'tools/release/runtime_inputs.py', 'verify'
}
tasks.configureEach { task ->
if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs)
}
// The GOG / Epic / Amazon download engines are Rust (app/src/main/rust/stores, ported from
// Bannerlator's bl-steam-client crate). cargo-ndk cross-compiles them for arm64-v8a into the build
// directory, which is one more jniLibs source below: nothing compiled is committed, the same rule
// the preloads, proot and the DirectAudio relay follow. cargo-ndk exports the NDK clang/llvm-ar as
// the C compiler for the one C-compiling crate (ring, under rustls) - the step Bannerlator's CI
// does by hand - and reads the NDK from ANDROID_NDK_HOME, which is set to the NDK this build pins.
// -PskipRust=true builds without the library, for a checkout with no Rust toolchain: the stores
// then report themselves unavailable instead of the build failing.
def rustCrateDir = layout.projectDirectory.dir('src/main/rust/stores')
def rustJniLibsDir = layout.buildDirectory.dir('rustJniLibs')
def skipRust = (project.findProperty('skipRust') ?: 'false').toString().toBoolean()
def buildRustStores = tasks.register('buildRustStores', Exec) {
description = 'Cross-compiles the store download engines (libdroiddeckstores.so) with cargo-ndk.'
onlyIf { !skipRust }
inputs.dir(rustCrateDir.dir('src'))
inputs.files(rustCrateDir.file('Cargo.toml'), rustCrateDir.file('Cargo.lock'))
inputs.property('ndkVersion', android.ndkVersion)
if (!skipRust) inputs.property('rustToolchain', providers.exec {
environment 'PATH', "${System.getProperty('user.home')}/.cargo/bin${File.pathSeparator}${System.getenv('PATH')}"
commandLine 'rustup', 'run', 'stable', 'rustc', '-Vv'
}.standardOutput.asText)
outputs.dir(rustJniLibsDir)
workingDir rustCrateDir
// The configuration cache serialises the task, so everything it needs is resolved here, not
// in doFirst. ~/.cargo/bin is where rustup puts cargo and cargo-ndk; an IDE-launched Gradle
// does not always have it on PATH.
def cargoBin = "${System.getProperty('user.home')}/.cargo/bin"
environment 'PATH', "${cargoBin}${File.pathSeparator}${System.getenv('PATH')}"
environment 'ANDROID_NDK_HOME', android.ndkDirectory.absolutePath
environment 'CARGO_TERM_COLOR', 'never'
if (!skipRust) environment 'RUSTC', providers.exec {
commandLine 'rustup', 'which', '--toolchain', 'stable', 'rustc'
}.standardOutput.asText.get().trim()
// --platform 26 = minSdkVersion: the engines link against the API 26 libc, like the C++ code.
// Homebrew's cargo/rustc can precede rustup on macOS and use a different sysroot, despite
// rustup reporting the Android target installed. Run both through the selected toolchain.
commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26',
'--output-dir', rustJniLibsDir.get().asFile.absolutePath,
'build', '--release', '--locked'
}
// Skipping Rust must omit its library even if another build left a cached .so behind.
if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
dependencies {
// The runtime tarball is a zstd-compressed tar.
implementation 'com.github.luben:zstd-jni:1.5.2-3@aar'
implementation 'org.apache.commons:commons-compress:1.27.1'
// Bannerlator's Windows components come as .tar.xz (WinComponents).
implementation 'org.tukaani:xz:1.10'
// The UI: every menu, dialog and overlay is Compose. The session's SurfaceView and the
// on-screen pad are Views underneath it, which is what they are in every Compose app.
implementation platform('androidx.compose:compose-bom:2024.02.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:1.8.2'
// The File Manager, Bannerlator's: its icon set, image thumbnails and the coroutines it
// copies on. Same versions as Bannerlator so the screen stays a straight copy.
implementation 'androidx.compose.material:material-icons-extended'
implementation 'io.coil-kt:coil-compose:2.6.0'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// ADB-over-Wi-Fi pairing and authenticated shell client. Users still approve pairing from
// Android's Wireless debugging screen; DroidDeck only runs its one fixed setting command.
implementation 'com.flyfishxu:kadb-android:1.2.1'
// Android terminal view/emulator used by the in-session secondary-display Linux shell.
implementation 'com.termux.termux-app:terminal-view:0.118.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.16.1'
}
+212
View File
@@ -0,0 +1,212 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Session logs go to Downloads so a failed run can be handed over as one folder. -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Store download progress and "installed" / "failed" notices (Android 13+). -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- The session keeps running while the app is not on screen: a foreground service holds the
process at perceptible priority, and a high-performance WiFi lock keeps a backgrounded
download from being throttled to nothing by the radio's power-save. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Optional Wi-Fi discovery: requested only from Steam settings after an explanation. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Ends background processes of another app that holds the Steam login. GameHub starts
itself with the phone via boot receivers and signs this client out seconds after every
login; a normal permission, and it cannot touch anything in the foreground. -->
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<!-- The microphone for the Linux session, only when the user turns it on: the DirectAudio
relay helper opens the input stream under this uid, and PulseAudio exposes it to the
Steam client as DirectAudioMic. -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Installs DroidDeck's own updates (Updates page) over itself, after the user allows it. -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- A paired Steam Controller, read over Bluetooth LE out of lizard mode (SteamControllerBle).
Granted at install below targetSdk 31; no scan, so no location. -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<application
android:name=".App"
android:localeConfig="@xml/locales_config"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:hardwareAccelerated="true"
android:largeHeap="true"
android:extractNativeLibs="true"
android:appCategory="game"
android:isGame="true">
<!-- A game, to the OS: Android 12+ routes the category through GameManager and the OEM
performance frameworks (Game Booster, Game Turbo, Game Space) key off it. Without it
a regular flagship runs the session under its "app" power profile - a conservative
governor and GPU DVFS - which is why the same chipset feels slower here than in a
gaming phone whose mode is on for everything. The config says what the OS may do. -->
<meta-data android:name="android.game_mode_config" android:resource="@xml/game_mode_config" />
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="sensorLandscape"
android:configChanges="keyboard|keyboardHidden|navigation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="droiddeck" android:host="game" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.droiddeck.launcher.LAUNCH_GAME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Android owns the Home key. This small translucent Home handler routes the resulting
Home launch either back into a running Steam session or to the regular front end.
Off until the user turns on "Use as a Home screen" (Setup): while it is disabled the
app is not a Home app at all - not offered, not in the "choose Home" list. -->
<activity
android:name=".HomeActivity"
android:enabled="false"
android:exported="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:taskAffinity="${applicationId}.home"
android:screenOrientation="sensorLandscape"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.HOME" />
</intent-filter>
</activity>
<!--
A session keeps a Surface, a compositor thread and a proot tree alive; a configuration
change must not tear that down and rebuild it, so the activity handles them itself.
Landscape only: gamescope is told one output size when it starts.
Resizable, deliberately: a foldable opening or closing changes the window, and the
compositor re-fits the picture with bars. Declared non-resizable, the system keeps a
fixed-shape window instead and STRETCHES it onto the other panel - the app never sees a
size change, so nothing it does can help.
-->
<activity
android:name=".input.ControllerEditorActivity"
android:exported="false"
android:theme="@style/SessionTheme"
android:screenOrientation="sensorLandscape"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".files.FileManagerActivity"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:theme="@style/AppTheme" />
<activity
android:name=".files.FilePickerActivity"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:theme="@style/AppTheme" />
<!-- A store's own sign-in page (GOG, Epic Games, Amazon Games) in a WebView. -->
<activity
android:name=".stores.StoreLoginActivity"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:theme="@style/AppTheme" />
<activity
android:name=".agent.AgentStartActivity"
android:exported="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:permission="android.permission.DUMP"
android:theme="@style/SessionTheme" />
<activity
android:name=".SessionActivity"
android:exported="false"
android:supportsPictureInPicture="true"
android:launchMode="singleTask"
android:theme="@style/SessionTheme"
android:preferMinimalPostProcessing="true"
android:screenOrientation="sensorLandscape"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<!--
Owns the session - gamescope, the Steam client, audio - so it survives the activity
going away. Without it Android demotes the process as soon as the last visible
activity goes, and the low-memory killer reaps the guest: leaving Big Picture to
answer a message would come back to a dead Steam and a half-finished download.
-->
<service
android:name=".session.SessionService"
android:exported="false"
android:stopWithTask="false" />
<!-- Holds the process in the foreground while the Linux runtime downloads and unpacks. -->
<service
android:name=".runtime.RuntimeInstallService"
android:exported="false" />
<service
android:name=".core.WirelessAdbPairingService"
android:exported="false" />
<!-- Keeps the process alive while a store (GOG, Epic, Amazon) game downloads. -->
<service
android:name=".stores.download.StoreDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<!-- Hands a zipped session folder to the share sheet; nothing else is exposed. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.logs"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/log_share_paths" />
</provider>
<!-- The app's data folder in Android's file picker ("Open from" > DroidDeck). Only the
system's documents UI may bind it; apps get a file only when the user picks it. -->
<provider
android:name=".files.AppFilesProvider"
android:authorities="${applicationId}.documents"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<provider
android:name=".agent.AgentBridgeProvider"
android:authorities="${applicationId}.agent"
android:exported="true"
android:permission="android.permission.DUMP"
android:grantUriPermissions="false" />
</application>
</manifest>
Binary file not shown.
@@ -0,0 +1,49 @@
{
"archives": {
"lxqt-r1": "c2bb860b3b1688287451e2495be5db313ec07f8c8af501dfac2768a9f417bc86",
"desktop-kde-r1": "55747e75d0ec055ab7ea1e92a757f3be227cc3205bb9101f5b53cadef6318579",
"desktop-kde-r2": "9ba832677b498654136c69d8c8330434d17e4d53cd099acac9bde3ba429b35bf",
"emulators-r1": "a1f4806a9663d28f424b4cab1277e0ede80a92e69de5bc063cdc8b45af220164"
},
"desktopApplications": {
"ark-26.08.1-1-aarch64.pkg.tar.xz": "93e2c2b75ff20cd29b4f312ed1979ef6f27255b93232b93bfcdb31239903c1d4",
"breeze-6.7.5-1-aarch64.pkg.tar.xz": "1d7f4eb9d8d234caaea23621909f5233d19462c2f058afeeb54bed7b81bd03bd",
"breeze-gtk-6.7.5-1-any.pkg.tar.xz": "a0f59bc4fc426879c8f8a97f6dcc707fb7d1b52913da5eb7e9db83777b6957b7",
"breeze-icons-6.30.0-1-aarch64.pkg.tar.xz": "4bae4689032501156687a227024e56554820747644953c67fc8d2e2b6b0b045a",
"dolphin-26.08.1-1-aarch64.pkg.tar.xz": "fef738ab8132d3eba1156b98c116699ce0a5efcb4cb462efbacdcf4c47f79a23",
"firefox-157.0.1-1-aarch64.pkg.tar.xz": "d142fdb8a339a65eca7cea6a2bba5b29202386166abd9d59851d6757c9eb2163",
"gwenview-26.08.1-1-aarch64.pkg.tar.xz": "a9dc7d5ab73fa677905d5a57248b1b9b404b6b16e862958cc7443d387f684a47",
"kate-26.08.1-1-aarch64.pkg.tar.xz": "acf50710042746f243a3b219a1ae0f788e338f3dd9d75093e8bb671a618b83b6",
"kde-cli-tools-6.7.5-1-aarch64.pkg.tar.xz": "af466398a912a994a97fadfe46aff401d88a90d7fdce7e843dc4218098fa38ac",
"kde-gtk-config-6.7.5-1-aarch64.pkg.tar.xz": "ba6cb4cb01dbb3f2c247f2f6973ede3b64c0849f89340217b51dfbf11cb8cb5c",
"kinfocenter-6.7.5-1-aarch64.pkg.tar.xz": "f3d5416db96b50f9a5b8a284d82ae21d834a8cce646010abdd7087491c399b0b",
"konsole-26.08.1-1-aarch64.pkg.tar.xz": "1ecf87c0d11d620ebe4aeafb02788e0d5f4fe9019ad9c5d9f1011c43237dd5ec",
"kscreen-6.7.5-1-aarch64.pkg.tar.xz": "8fbaa6fedf93edeba021a5416b551663654919b27c2ee237d696ec0b0ce7d5b7",
"kwin-6.7.5-1-aarch64.pkg.tar.xz": "a7535710c74b90359f80306134b2b7581132cbac508dc84b7e07ef275cc8c3fe",
"mpv-1:0.41.0-6-aarch64.pkg.tar.xz": "6c89d00736820210a3c7f32af4cf95d155191d463f9df8bd22137c45f30a554a",
"plasma-desktop-6.7.5-1-aarch64.pkg.tar.xz": "b266dd42cea965b5ab1cc1ae600877e9d3bc83ec642c61fe1669c4457e641a55",
"plasma-integration-6.7.5-1-aarch64.pkg.tar.xz": "daa96da14d9a4d12c0d9c0341e1be6836a9a437c469d61da5e9844d51e2606cf",
"plasma-pa-6.7.5-1-aarch64.pkg.tar.xz": "02dbefe9d3e0069b2bb37cac2a6be18b8f0a1887c87704a0716e7a9268eadcd8",
"plasma-workspace-6.7.5-1-aarch64.pkg.tar.xz": "3a1455857afaa9ccf342ab6167f0f15794ee9d248b0925398ea6f62f12730a86",
"spectacle-1:6.7.5-1-aarch64.pkg.tar.xz": "0c9837412ea6119edef18bcb0a8fceb74634cb35be0ca78b3e1dd1ff2443866e",
"systemsettings-6.7.5-1-aarch64.pkg.tar.xz": "b02b6a726857d5125c92d1ec95379216c7a3bc1f187a3b46e88c0d056b29ce70",
"xdg-desktop-portal-kde-6.7.5-1-aarch64.pkg.tar.xz": "2c57d17d34d577023eb9cd7b0fca1ba9163549afe60450f6271aa148af099ef2"
},
"legacyApplications": {
"featherpad-1.6.3-1-aarch64.pkg.tar.xz": "fcac55215815c0b712453f9bcc09abfe4b518e36025a111a7a7598ef04913f3d",
"labwc-0.20.2-1-aarch64.pkg.tar.xz": "bcd4cc3945165b0fdd33c0117cd7b9dfd05cf8c327326c37f3c3a27ef23ee5ca",
"lximage-qt-2.4.0-1-aarch64.pkg.tar.xz": "5d6bdd5472ea6bcb4b993b36df3ab7010bb2e62b36a78a649708fc9a52cb0928",
"lxqt-config-2.4.0-1-aarch64.pkg.tar.xz": "4b601d798fc7689ee76787dbef8c8958c02429ee60248cd99d950c919dbefba5",
"lxqt-panel-2.4.1-1-aarch64.pkg.tar.xz": "a141e0f8049e73f859d843510c6f45d6ed9087c990bcebf44f5f2430621024c4",
"lxqt-qtplugin-2.4.0-3-aarch64.pkg.tar.xz": "61c8f828be73111ee0bd5e3163b0d64c2098dd0189710a190f42be11137695cf",
"lxqt-runner-2.4.0-1-aarch64.pkg.tar.xz": "5afa74652a37366ea4e7774cf33379977582fbdca599cef1005ebf637cbcae4b",
"lxqt-session-2.4.0-2-aarch64.pkg.tar.xz": "e9c7c8a0011859029622ad202e0d3826e4e5bc8d64659031f6efa211456ff6eb",
"lxqt-themes-2.4.0-1-any.pkg.tar.xz": "7ea16c62a1fa70cdac13cb3f8fdcdaad299ae1f5bd514acfda671484755787bf",
"lxqt-wayland-session-0.4.1-1-any.pkg.tar.xz": "cd8dc11f5035469d23b6bbd937a90593402428c43de37517412aa43fe1fc6b10",
"papirus-icon-theme-20260801-1-any.pkg.tar.xz": "4aae3f47b800a60d6fd360e55adc581408ceaafc481c5a420a681741a3966510",
"pcmanfm-qt-2.4.1-1-aarch64.pkg.tar.xz": "7ec1a6b39dd0e4055b39f0e2c0549ea263d83004e1b821fb9e42f1d7f5fff0f9",
"qterminal-2.4.0-1-aarch64.pkg.tar.xz": "aa4adc9c9d47641b9452ec8d879038a9cb4762c3b6b5299045fac968fd0ed22d",
"swaybg-1.2.2-1-aarch64.pkg.tar.xz": "a26caa16ca7d581b5ca05758677058b4b60af10dc8d83e447ad6824b762b48b8",
"wlr-randr-0.5.0-1-aarch64.pkg.tar.xz": "58c3651c6a15dc8f95a310c7e225f759aa766be2fb3c0981a9a8b567854675b3"
}
}
@@ -0,0 +1,2 @@
desktop-kde-r1
desktop-kde-r2
Binary file not shown.
File diff suppressed because it is too large. Load diff
+54
View File
@@ -0,0 +1,54 @@
# The app's native side: the Wayland compositor the session presents into, the adrenotools loader
# it opens Turnip with, and one small library for the fake-input ring's memory fence.
cmake_minimum_required(VERSION 3.22.1)
project(droiddeck C CXX)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2 -Wno-unused-function -Wimplicit-function-declaration")
# Every library the app builds is linked with 16 KB-aligned LOAD segments. A 16 KB-page kernel
# (Android 15+ devices) refuses to load a 4 KB-only ELF; a 4 KB kernel loads a 16 KB-aligned one
# unchanged (the alignment is a multiple of its page), so this costs nothing on today's devices
# but a little padding in the file. Prebuilt libraries under jniLibs/ carry their own alignment.
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,max-page-size=16384")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,max-page-size=16384")
add_subdirectory(adrenotools)
# DXVK's DXBC->SPIR-V translator (zlib licence), vendored under thirdparty/dxbc from the same
# subset upstream lsfg-vk uses. Native LSFG frame generation needs it: the Lossless Scaling
# shader chain is DXBC in every build downloadable from Steam.
add_subdirectory(thirdparty/dxbc)
# The Win-FG chain self-identifies by two halves: the upstream tag it was copied from (hand-set,
# UPDATE IT when re-copying) and a hash over the sources themselves, computed here, which cannot
# rot. The compositor's frame-generation leg (waylandcomp/CMakeLists.txt) stamps both in.
set(WINFG_UPSTREAM "v0.3.0")
set(WINFG_CHAIN_SRC
framegen/winfg/framegen.cpp
framegen/winfg/framegen.hpp
framegen/winfg/record_impl.inc
framegen/winfg/config.hpp
framegen/winfg/embedded_shaders.hpp
framegen/winfg/winfg_engine.cpp
)
set(WINFG_HASH_INPUT "")
foreach(f ${WINFG_CHAIN_SRC})
file(SHA256 "${CMAKE_CURRENT_SOURCE_DIR}/${f}" _h)
string(APPEND WINFG_HASH_INPUT "${_h}")
endforeach()
string(SHA256 WINFG_CHAIN_HASH_FULL "${WINFG_HASH_INPUT}")
string(SUBSTRING "${WINFG_CHAIN_HASH_FULL}" 0 12 WINFG_CHAIN_HASH)
message(STATUS "win-fg chain: ${WINFG_UPSTREAM} (src ${WINFG_CHAIN_HASH})")
add_subdirectory(waylandcomp)
add_library(sdnative SHARED sdnative/sdnative.c)
target_link_libraries(sdnative log)
# The device info window's Vulkan section: the phone's own driver, asked once through the system
# loader (opened with dlopen, so a phone without Vulkan still loads the app).
add_library(deviceinfo SHARED deviceinfo/vkinfo.c)
target_link_libraries(deviceinfo dl)
# Rebuild Termux terminal-emulator's PTY JNI helper with the app's NDK instead of carrying its
# prebuilt libtermux.so with a redundant libstdc++.so dependency.
add_library(termux SHARED termux/termux.c)
+4
View File
@@ -0,0 +1,4 @@
[submodule "lib/linkernsbypass"]
path = lib/linkernsbypass
url = https://github.com/Pipetto-crypto/liblinkernsbypass.git
branch = master
@@ -0,0 +1,37 @@
cmake_minimum_required(VERSION 3.14)
if(NOT ${CMAKE_ANDROID_ARCH_ABI} STREQUAL arm64-v8a)
message(FATAL_ERROR "Unsupported target architecture: ${CMAKE_ANDROID_ARCH_ABI}. Please make an issue on the repo!")
endif()
project(adrenotools LANGUAGES CXX C)
set(GEN_INSTALL_TARGET OFF CACHE BOOL "")
add_subdirectory(lib/linkernsbypass)
set(LIB_SOURCES src/bcenabler.cpp
src/driver.cpp
include/adrenotools/bcenabler.h
include/adrenotools/driver.h
include/adrenotools/priv.h)
add_library(adrenotools ${LIB_SOURCES})
target_include_directories(adrenotools PUBLIC include)
target_include_directories(adrenotools PRIVATE .)
target_compile_options(adrenotools PRIVATE -Wall -Wextra)
target_link_libraries(adrenotools android linkernsbypass)
add_subdirectory(src/hook)
if (${BUILD_SHARED_LIBS})
target_link_options(adrenotools PRIVATE "-Wl,-s,--exclude-libs,liblinkernsbypass.a")
endif()
if (${GEN_INSTALL_TARGET})
configure_file(adrenotools.pc.in adrenotools.pc @ONLY)
install(FILES ${CMAKE_BINARY_DIR}/adrenotools.pc DESTINATION lib/pkgconfig)
install(TARGETS adrenotools main_hook file_redirect_hook gsl_alloc_hook hook_impl)
install(DIRECTORY include/
DESTINATION include
FILES_MATCHING PATTERN "*.h*")
endif()
+24
View File
@@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2021, Billy Laws
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+30
View File
@@ -0,0 +1,30 @@
### Adreno Tools
A library for applying rootless Adreno GPU driver modifications/replacements. Currently supports loading custom GPU drivers such as [turnip](https://docs.mesa3d.org/android.html#building-using-the-android-ndk), enabling BCn textures and redirecting file operations to allow accessing shader dumps and modification of the [driver config file](https://gist.github.com/bylaws/04130932e2634d1c6a2a9729e3940d60) without root.
#### Documentation
API is documented in the `include/adrenotools` headers.
#### Support
Android 9+
Arm64
Please create an issue if support for anything else is desired.
### FAQ
#### Is there an example project?
There is a simple bare-bones project [AdrenoToolsTest](https://github.com/darksylinc/AdrenoToolsTest) demonstrating how to get libadrenotools working.
#### How do I use this to update the drivers on my phone? Where's the apk?
You don't. This library is **not** for installing into Android and is **not** for end users.
This library is aimed at other developers.
Each individual app must explicitly make use of libadrenotools in order to load custom drivers into an app / game.
#### How do I use this library to make \<favourite game\> use newer drivers?
See previous question. It's up to the game developer to add support & use this library.
You could contact them to so they add support for it; but that's out of our power.
@@ -0,0 +1,13 @@
prefix="@CMAKE_INSTALL_PREFIX@"
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: adrenotools
Description:
URL:
Version: 1.0.0
Libs: -L${libdir} -ladrenotools
Libs.private:
Requires.private:
Cflags: -I${includedir}
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
aarch64-linux-gnu-gcc -c src/bcenabler_patch.s -o /tmp/patch.o
aarch64-linux-gnu-objcopy -O binary -j .text /tmp/patch.o /tmp/patch.bin
# Must be smaller than a single page
bin2c -o gen/bcenabler_patch.h -n PatchRawData /tmp/patch.bin
rm /tmp/patch.o
rm /tmp/patch.bin
@@ -0,0 +1,23 @@
/* Generated by bin2c, do not edit manually */
/* Contents of file /tmp/patch.bin */
const long int PatchRawData_size = 264;
const unsigned char PatchRawData[264] = {
0x1F, 0x0C, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0xE0, 0x08, 0x80, 0x52, 0x3E, 0x00, 0x00, 0x14,
0x1F, 0x14, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0xE0, 0x08, 0x80, 0x52, 0x3A, 0x00, 0x00, 0x14,
0x1F, 0x10, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x00, 0x09, 0x80, 0x52, 0x36, 0x00, 0x00, 0x14,
0x1F, 0x18, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x00, 0x09, 0x80, 0x52, 0x32, 0x00, 0x00, 0x14,
0x1F, 0x1C, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x40, 0x09, 0x80, 0x52, 0x2E, 0x00, 0x00, 0x14,
0x1F, 0x20, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x60, 0x09, 0x80, 0x52, 0x2A, 0x00, 0x00, 0x14,
0x1F, 0x24, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0xA0, 0x09, 0x80, 0x52, 0x26, 0x00, 0x00, 0x14,
0x1F, 0x28, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0xC0, 0x09, 0x80, 0x52, 0x22, 0x00, 0x00, 0x14,
0x1F, 0x2C, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x00, 0x0A, 0x80, 0x52, 0x1E, 0x00, 0x00, 0x14,
0x1F, 0x30, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x20, 0x0A, 0x80, 0x52, 0x1A, 0x00, 0x00, 0x14,
0x1F, 0x34, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x60, 0x0A, 0x80, 0x52, 0x16, 0x00, 0x00, 0x14,
0x1F, 0x38, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x80, 0x0A, 0x80, 0x52, 0x12, 0x00, 0x00, 0x14,
0x1F, 0x3C, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0xE0, 0x0B, 0x80, 0x52, 0x0E, 0x00, 0x00, 0x14,
0x1F, 0x40, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x00, 0x0C, 0x80, 0x52, 0x0A, 0x00, 0x00, 0x14,
0x1F, 0x44, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x40, 0x0C, 0x80, 0x52, 0x06, 0x00, 0x00, 0x14,
0x1F, 0x48, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0x60, 0x0C, 0x80, 0x52, 0x02, 0x00, 0x00, 0x14,
0x00, 0x00, 0x80, 0x52, 0xFF, 0xFF, 0xFF, 0xFF
};
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
extern "C" {
#else
#include <stdbool.h>
#endif
#include <stdint.h>
/**
* @brief Describes the level of BCeNabler support for a driver
*/
enum adrenotools_bcn_type {
ADRENOTOOLS_BCN_INCOMPATIBLE, //!< Driver isn't supported by BCeNabler
ADRENOTOOLS_BCN_BLOB, //!< Driver already supports BCn textures so BCeNabler isn't necessary
ADRENOTOOLS_BCN_PATCH //!< Driver can be patched with BCeNabler to support BCn textures
};
/**
* @brief Checks the status of BCn support in the supplied driver
* @param major The major part of VkPhysicalDeviceProperties::driverVersion
* @param minor The minor part of VkPhysicalDeviceProperties::driverVersion
* @param vendorId VkPhysicalDeviceProperties::vendorId
*/
enum adrenotools_bcn_type adrenotools_get_bcn_type(uint32_t major, uint32_t minor, uint32_t vendorId);
/**
* @brief Patches the Adreno graphics driver to enable support for BCn compressed formats
* @note adrenotools_get_bcn_type MUST be checked to equal ADRENOTOOLS_BCN_PATCH before calling this
* @param vkGetPhysicalDeviceFormatPropertiesFn A pointer to vkGetPhysicalDeviceFormatProperties obtained through vkGetInstanceProcAddr. This is used to find the correct function to patch
* @return If the patching succeeded, if false the driver will be in an undefined state
*/
bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include "priv.h"
/**
* @brief Opens a new libvulkan.so instance according to `flags`
* @remark IMPORTANT: The app MUST be packaged w/ useLegacyPackaging = true. The reason for this is
* that we install the hook in hookLibDir; but this folder won't be set correctly when useLegacyPackaging == false
* because Android expects to read the *.so directly from the apk (instead of decompressing them into nativeLibraryDir).
* @param dlopenMode The dlopen mode to use when opening libvulkan
* @param featureFlags Which adrenotools driver features to enable
* @param tmpLibDir A writable directory to hold patched libraries, only used on api < 29 due to the lack of memfd support. If nullptr is passed and the API version is < 29 memfd usage will be attempted and if unsupported nullptr will be returned
* @param hookLibDir The directory holding the built hooks.
* IMPORTANT: This path MUST point to whatever `getApplicationInfo().nativeLibraryDir` (Kotlin code) returns.
* Failing to do so will result in this function returning a valid ptr but then the hook will fail and either the original driver is loaded as fallback, or vkEnumeratePhysicalDevices returns 0 devices.
* @param customDriverDir The directory to load a custom GPU driver named according to `customDriverName` from. Only used if ADRENOTOOLS_DRIVER_CUSTOM is set in `featureFlags`.
* IMPORTANT: This path MUST NOT be on sdcard/storage. i.e. use ANativeActivity::internalDataPath and often looks like "/data/user/0/com.example.app/files/".
* Otherwise you will get permission denied because dlopen() can't be done on *.so libraries that any random user or application can manipulate (which would be a security risk).
* @param customDriverName The soname of the custom driver to load. Only used if ADRENOTOOLS_DRIVER_CUSTOM is set in `featureFlags`
* @param fileRedirectDir The directory which to redirect all file accesses performed by the driver to. Only used if ADRENOTOOLS_DRIVER_FILE_REDIRECT is set in `featureFlags`
* @param userMappingHandle A pointer to a void* which will be set to the mapping handle if ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT is set in `featureFlags`
*/
void *adrenotools_open_libvulkan(int dlopenMode, int featureFlags, const char *tmpLibDir, const char *hookLibDir, const char *customDriverDir, const char *customDriverName, const char *fileRedirectDir, void **userMappingHandle);
/**
* @brief Imports the given CPU mapped memory range into the GSL allocator. This should then be followed by a call to vkAllocateMemory with a matching size which will return a VkDeviceMemory view over the input region
* @param handle Mapping handle that was returned by adrenotools_open_libvulkan
* @param hostPtr The host pointer to import
* @param size The size of the region to import
* @return True on success
*/
bool adrenotools_import_user_mem(void *handle, void *hostPtr, uint64_t size);
/**
* @brief Maps GPU memory and imports it into the GSL allocator. This should then be followed by a call to adrenotools_mem_cpu_map to map the memory on the CPU, then finally vkAllocateMemory with a matching size
* @param handle Mapping handle that was returned by adrenotools_open_libvulkan
* @param size Pointer to a variable containing the size of the region to import, will be updated to contain the required size of the region to allocate CPU side
* @return true on success
*/
bool adrenotools_mem_gpu_allocate(void *handle, uint64_t *size);
/**
* @brief Maps the last mapping allocated using adrenotools_mem_gpu_allocate into the given host memory region, such that vkAllocateMemory can then be called
* @param handle Mapping handle that was returned by adrenotools_open_libvulkan
* @param hostPtr A pointer to where the mapping should be mapped
* @param size The size of the mapping. MUST be equal to the size returned by adrenotools_mem_gpu_allocate
*/
bool adrenotools_mem_cpu_map(void *handle, void *hostPtr, uint64_t size);
/**
* @note This function should be called after adrenotools_open_libvulkan and Vulkan driver init to check if the mapping import hook loaded successfully
* @return True if the mapping was successfully imported (or initialization succeeded)
*/
bool adrenotools_validate_gpu_mapping(void *handle);
/**
* @brief Provides a way to force the GPU to run at the maximum possible clocks (thermal constraints will still be applied)
*/
void adrenotools_set_turbo(bool turbo);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#include <stdint.h>
/**
* @brief Bitfield enum of additional driver features that can be used with adrenotools_open_libvulkan
*/
enum {
ADRENOTOOLS_DRIVER_CUSTOM = 1 << 0,
ADRENOTOOLS_DRIVER_FILE_REDIRECT = 1 << 1,
ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT = 1 << 2,
};
#define ADRENOTOOLS_GPU_MAPPING_SUCCEEDED_MAGIC 0xDEADBEEF
/**
* @brief A replacement GPU memory mapping for use with ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT
*/
struct adrenotools_gpu_mapping {
void *host_ptr;
uint64_t gpu_addr; //!< The GPU address of the mapping to import, if mapping import/init succeeds this will be set to ADRENOTOOLS_GPU_MAPPING_SUCCEEDED_MAGIC
uint64_t size;
uint64_t flags;
};
@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.14)
if(NOT ${CMAKE_ANDROID_ARCH_ABI} STREQUAL arm64-v8a)
message(FATAL_ERROR "Unsupported target architecture: ${CMAKE_ANDROID_ARCH_ABI}. Please make an issue on the repo!")
endif()
project(linkernsbypass LANGUAGES CXX)
set(SOURCES android_linker_ns.cpp
android_linker_ns.h
elf_soname_patcher.cpp
elf_soname_patcher.h)
add_library(linkernsbypass STATIC ${SOURCES})
target_compile_options(linkernsbypass PRIVATE -Wall -Wextra)
target_link_libraries(linkernsbypass android dl)
target_include_directories(linkernsbypass PUBLIC .)
@@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2021, Billy Laws
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,9 @@
### Android Linker Namespace Bypass Library
Provides access to the hidden linker namespace functionality in Android 9+ and exposes an interface for hooking libraries similar to a runtime equivalent of `LD_PRELOAD`.
See the `android_linker_ns.h` header for an API reference.
#### Support
Android 9+
Arm64
Android 8 and arm32 could be supported with some trivial changes, feel free to open an issue if you have a use for this library on either of them.
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <array>
#include <cstdio>
#include <cstdlib>
#include <errno.h>
#include <dlfcn.h>
#include <unistd.h>
#include <fcntl.h>
#include <android/dlext.h>
#include <android/log.h>
#include <android/api-level.h>
#include <sys/mman.h>
#include "elf_soname_patcher.h"
#include "android_linker_ns.h"
using loader_android_create_namespace_t = android_namespace_t *(*)(const char *, const char *, const char *, uint64_t, const char *, android_namespace_t *, const void *);
static loader_android_create_namespace_t loader_android_create_namespace;
static bool lib_loaded;
/* Public API */
bool linkernsbypass_load_status() {
return lib_loaded;
}
struct android_namespace_t *android_create_namespace(const char *name,
const char *ld_library_path,
const char *default_library_path,
uint64_t type,
const char *permitted_when_isolated_path,
android_namespace_t *parent_namespace) {
auto caller{__builtin_return_address(0)};
return loader_android_create_namespace(name, ld_library_path, default_library_path, type,
permitted_when_isolated_path, parent_namespace, caller);
}
struct android_namespace_t *android_create_namespace_escape(const char *name,
const char *ld_library_path,
const char *default_library_path,
uint64_t type,
const char *permitted_when_isolated_path,
android_namespace_t *parent_namespace) {
auto caller{reinterpret_cast<void *>(&dlopen)};
return loader_android_create_namespace(name, ld_library_path, default_library_path, type,
permitted_when_isolated_path, parent_namespace, caller);
}
android_get_exported_namespace_t android_get_exported_namespace;
android_link_namespaces_all_libs_t android_link_namespaces_all_libs;
android_link_namespaces_t android_link_namespaces;
bool linkernsbypass_link_namespace_to_default_all_libs(android_namespace_t *to) {
// Creating a shared namespace with the default parent will give a copy of the default namespace that we can actually access
// This is needed since there is no way to access a direct handle to the default namespace as it's not exported
static auto defaultNs{android_create_namespace_escape("default_copy", nullptr, nullptr, ANDROID_NAMESPACE_TYPE_SHARED, nullptr, nullptr)};
return android_link_namespaces_all_libs(to, defaultNs);
}
void *linkernsbypass_namespace_dlopen(const char *filename, int flags, android_namespace_t *ns) {
android_dlextinfo extInfo{
.flags = ANDROID_DLEXT_USE_NAMESPACE,
.library_namespace = ns
};
return android_dlopen_ext(filename, flags, &extInfo);
}
#ifndef __NR_memfd_create
#if defined(__aarch64__)
#define __NR_memfd_create 279
#else
#error Unsupported target architecture!
#endif
#endif
void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *libTargetDir, int flags, android_namespace_t *ns) {
static std::array<char, PATH_MAX> PathBuf{};
// Used as a unique ID for overwriting soname and creating target lib files
static uint16_t TargetId{};
int libTargetFd{[&] () {
if (libTargetDir) {
snprintf(PathBuf.data(), PathBuf.size(), "%s/%d_patched.so", libTargetDir, TargetId);
return open(PathBuf.data(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
} else {
// If memfd isn't supported errno will contain ENOSYS after calling
errno = 0;
int fd{static_cast<int>(syscall(__NR_memfd_create, libPath, 0))};
if (errno == ENOSYS || fd < 0)
return -1;
else
return fd;
}
}()};
if (libTargetFd == -1)
return nullptr;
// Partially overwrite soname with 3 digits (replacing lib...) with to make sure a cached so isn't loaded
std::array<char, 3> sonameOverwrite{};
snprintf(sonameOverwrite.data(), sonameOverwrite.size(), "%03u", TargetId++);
if (!elf_soname_patch(libPath, libTargetFd, sonameOverwrite.data()))
return nullptr;
// Load our patched library into the hook namespace
android_dlextinfo hookExtInfo{
.flags = ANDROID_DLEXT_USE_NAMESPACE | ANDROID_DLEXT_USE_LIBRARY_FD,
.library_fd = libTargetFd,
.library_namespace = ns
};
// Make a path that looks about right
snprintf(PathBuf.data(), PathBuf.size(), "/proc/self/fd/%d", libTargetFd);
return android_dlopen_ext(PathBuf.data(), flags, &hookExtInfo);
}
static void *align_ptr(void *ptr) {
return reinterpret_cast<void *>(reinterpret_cast<uintptr_t>(ptr) & ~(getpagesize() - 1));
}
/* Private */
__attribute__((constructor)) static void resolve_linker_symbols() {
using loader_dlopen_t = void *(*)(const char *, int, const void *);
if (android_get_device_api_level() < 28)
return;
// ARM64 specific function walking to locate the internal dlopen handler
auto loader_dlopen{[]() {
union BranchLinked {
uint32_t raw;
struct {
int32_t offset : 26; //!< 26-bit branch offset
uint8_t sig : 6; //!< 6-bit signature
};
bool Verify() {
return sig == 0x25;
}
};
static_assert(sizeof(BranchLinked) == 4, "BranchLinked is wrong size");
// Some devices ship with --X mapping for exexecutables so work around that
mprotect(align_ptr(reinterpret_cast<void *>(&dlopen)), getpagesize(), PROT_WRITE | PROT_READ | PROT_EXEC);
// dlopen is just a wrapper for __loader_dlopen that passes the return address as the third arg hence we can just walk it to find __loader_dlopen
auto blInstr{reinterpret_cast<BranchLinked *>(&dlopen)};
while (!blInstr->Verify())
blInstr++;
return reinterpret_cast<loader_dlopen_t>(blInstr + blInstr->offset);
}()};
// Protect the loader_dlopen function to remove the BTI attribute (since this is an internal function that isn't intended to be jumped indirectly to)
mprotect(align_ptr(reinterpret_cast<void *>(&loader_dlopen)), getpagesize(), PROT_WRITE | PROT_READ | PROT_EXEC);
// Passing dlopen as a caller address tricks the linker into using the internal unrestricted namespace letting us access libraries that are normally forbidden in the classloader namespace imposed on apps
auto ldHandle{loader_dlopen("ld-android.so", RTLD_LAZY, reinterpret_cast<void *>(&dlopen))};
if (!ldHandle)
return;
android_link_namespaces_all_libs = reinterpret_cast<android_link_namespaces_all_libs_t>(dlsym(ldHandle, "__loader_android_link_namespaces_all_libs"));
if (!android_link_namespaces_all_libs)
return;
android_link_namespaces = reinterpret_cast<android_link_namespaces_t>(dlsym(ldHandle, "__loader_android_link_namespaces"));
if (!android_link_namespaces)
return;
auto libdlAndroidHandle{loader_dlopen("libdl_android.so", RTLD_LAZY, reinterpret_cast<void *>(&dlopen))};
if (!libdlAndroidHandle)
return;
loader_android_create_namespace = reinterpret_cast<loader_android_create_namespace_t>(dlsym(libdlAndroidHandle, "__loader_android_create_namespace"));
if (!loader_android_create_namespace)
return;
android_get_exported_namespace = reinterpret_cast<android_get_exported_namespace_t>(dlsym(libdlAndroidHandle, "__loader_android_get_exported_namespace"));
if (!android_get_exported_namespace)
return;
// Lib is now safe to use
lib_loaded = true;
}
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
// https://cs.android.com/android/platform/superproject/+/0a492a4685377d41fef2b12e9af4ebfa6feef9c2:art/libnativeloader/include/nativeloader/dlext_namespaces.h;l=25;bpv=1;bpt=1
enum {
ANDROID_NAMESPACE_TYPE_REGULAR = 0,
ANDROID_NAMESPACE_TYPE_ISOLATED = 1,
ANDROID_NAMESPACE_TYPE_SHARED = 2,
ANDROID_NAMESPACE_TYPE_EXEMPT_LIST_ENABLED = 0x08000000,
ANDROID_NAMESPACE_TYPE_ALSO_USED_AS_ANONYMOUS = 0x10000000,
ANDROID_NAMESPACE_TYPE_SHARED_ISOLATED = ANDROID_NAMESPACE_TYPE_SHARED | ANDROID_NAMESPACE_TYPE_ISOLATED,
};
/**
* @brief Checks if linkernsbypass loaded successfully and is safe to use
* @note IMPORTANT: This should be called before any calls to the rest of the library are made
* @return true if loading succeeded
*/
bool linkernsbypass_load_status();
// https://cs.android.com/android/platform/superproject/+/0a492a4685377d41fef2b12e9af4ebfa6feef9c2:art/libnativeloader/include/nativeloader/dlext_namespaces.h;l=86;bpv=1;bpt=1
struct android_namespace_t *android_create_namespace(const char *name,
const char *ld_library_path,
const char *default_library_path,
uint64_t type,
const char *permitted_when_isolated_path,
struct android_namespace_t *parent_namespace);
struct android_namespace_t *android_create_namespace_escape(const char *name,
const char *ld_library_path,
const char *default_library_path,
uint64_t type,
const char *permitted_when_isolated_path,
struct android_namespace_t *parent_namespace);
// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=3554
typedef struct android_namespace_t *(*android_get_exported_namespace_t)(const char *);
extern android_get_exported_namespace_t android_get_exported_namespace;
// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=2499
typedef bool (*android_link_namespaces_all_libs_t)(struct android_namespace_t *, struct android_namespace_t *);
extern android_link_namespaces_all_libs_t android_link_namespaces_all_libs;
// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=2473
typedef bool (*android_link_namespaces_t)(struct android_namespace_t *, struct android_namespace_t *, const char *);
extern android_link_namespaces_t android_link_namespaces;
/**
* @brief Like android_link_namespaces_all_libs but links from the default namespace
*/
bool linkernsbypass_link_namespace_to_default_all_libs(struct android_namespace_t *to);
/**
* @brief Loads a library into a namespace
* @note IMPORTANT: If `filename` is compiled with the '-z global' linker flag and RTLD_GLOBAL is supplied in `flags` the library will be added to the namespace's LD_PRELOAD list
* @param filename The name of the library to load
* @param flags The rtld flags for `filename`
* @param ns The namespace to dlopen into
*/
void *linkernsbypass_namespace_dlopen(const char *filename, int flags, struct android_namespace_t *ns);
/**
* @brief Force loads a unique instance of a library into a namespace
* @param libPath The path to the library to load with hooks applied
* @param libTargetDir A temporary directory to hold the soname patched library at `libPath`, will attempt to use memfd if nullptr
* @param flags The rtld flags for `libName`
* @param ns The namespace to dlopen into
*/
void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *libTargetDir, int flags, struct android_namespace_t *ns);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <initializer_list>
#include <cstdint>
#include <errno.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <link.h>
#include <elf.h>
#include "elf_soname_patcher.h"
bool elf_soname_patch(const char *libPath, int targetFd, const char *sonamePatch) {
struct stat libStat{};
if (stat(libPath, &libStat))
return false;
if (ftruncate(targetFd, libStat.st_size) == -1)
return false;
// Map the memory so we can read our elf into it
auto mappedLib{reinterpret_cast<uint8_t *>(mmap(nullptr, libStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, targetFd, 0))};
if (!mappedLib)
return false;
int libFd{open(libPath, O_RDONLY)};
if (libFd == false)
return false;
// Read lib elf into target file
if (read(libFd, mappedLib, libStat.st_size) != libStat.st_size)
return false;
// No longer needed
close(libFd);
auto eHdr{reinterpret_cast<ElfW(Ehdr) *>(mappedLib)};
auto sHdrEntries{reinterpret_cast<ElfW(Shdr) *>(mappedLib + eHdr->e_shoff)};
// Iterate over section headers to find the .dynamic section
for (ElfW(Half) i{}; i < eHdr->e_shnum; i++) {
auto &sHdr{sHdrEntries[i]};
if (sHdr.sh_type == SHT_DYNAMIC) {
auto strTab{reinterpret_cast<char *>(mappedLib + sHdrEntries[sHdr.sh_link].sh_offset)};
auto dynHdrEntries{reinterpret_cast<ElfW(Dyn) *>(mappedLib + sHdr.sh_offset)};
// Iterate over .dynamic entries to find DT_SONAME
for (ElfW(Xword) k{}; k < (sHdr.sh_size / sHdr.sh_entsize); k++) {
auto &dynHdrEntry{dynHdrEntries[k]};
if (dynHdrEntry.d_tag == DT_SONAME) {
char *soname{strTab + dynHdrEntry.d_un.d_val};
// Partially replace the old soname with the soname patch
size_t charIdx{};
for (; soname[charIdx] != 0 && sonamePatch[charIdx] != 0; charIdx++)
soname[charIdx] = sonamePatch[charIdx];
return true;
}
}
}
}
return false;
}
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Overwrites a portion of the soname in an elf by loading it into shared memory and modifying .dynstr
* @note IMPORTANT: The supplied soname patch will overwrite the first strlen(sonamePatch) chars of the soname
* @param elfPath Full path to the elf to patch
* @param targetFd FD to use for storing the patched library
* @return True on success
*/
bool elf_soname_patch(const char *elfPath, int targetFd, const char *newSoname);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,18 @@
linkernsbypass_files = files(['android_linker_ns.cpp', 'android_linker_ns.h', 'elf_soname_patcher.cpp', 'elf_soname_patcher.h'])
linkernsbypass_inc = include_directories('.')
libdl_dep = cpp.find_library('dl')
linkernsbypass_lib = static_library (
'linkernsbypass',
linkernsbypass_files,
c_args: (['-Wall', '-Wextra']),
cpp_args: (['-Wall', '-Wextra']),
include_directories: ([linkernsbypass_inc, adrenotools_inc]),
dependencies: ([libandroid_dep, libdl_dep])
)
# linkernsbypass_include = static_library('linkernsbypass', sources)
# target_compile_options(['linkernsbypass', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_libraries(['linkernsbypass', 'android', 'dl'])
# target_include_directories(['linkernsbypass', 'PUBLIC', '.'])
Loaded 100 of 1271 files, more files were not shown because too many files have changed in this diff. Show more