Compare commits

...
172 Commits
Author SHA1 Message Date
Kurt Himebauch f635e92498 fix: Steam touch controls load the layout Steam picked for DroidDeck's touch controller, including workshop layouts (#840) 2026-10-14 16:09:40 -04:00
RaidennrlandxXJsonDeruloXx eda1e9b9c6 Fix wired PowerA and PDP right-stick and trigger mapping (#692)
Fix wired PowerA and PDP stick and trigger axes

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-14 13:56:17 -04:00
kmnkv1andxXJsonDeruloXx fa9a9446bb Build and test on Windows: LF session scripts, '/' in relative paths (#552)
fix: build and test on Windows

- .gitattributes keeps the session's scripts LF under core.autocrlf=true;
  CRLF scripts made every session exit with status 2.
- runtime_inputs.py and the scrub record / log zip use '/' in relative
  paths, which Windows reported with '\' (runtime inputs never matched).
- The read-only-folder scrub test is skipped on Windows, which still
  creates files in a read-only directory.

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-14 13:56:12 -04:00
Gustash 3e3c4d69e4 feat: hide unfolded controls when a controller is connected (#829)
* feat: hide unfolded controls when a controller is connected

* fix: also hide controls when a BLE controller is connected

* fix: use refreshControls on session resume as well
2026-10-14 13:52:13 -04:00
Tjebbe Kerstens 913806f169 fix: seed redist markers again when the registry lost them (#793)
* fix: redist markers a replaced registry lost are seeded again

droiddeck-seed-redists recorded what it seeded in .droiddeck-redists and
trusted that record from then on. A Proton update replaces its
default_pfx system.reg but leaves our record beside it, and every new
prefix copies both, so new games got a record of markers their registry
never had: Steam then ran their install scripts for real. Batman: Arkham
City's AMD Dual-Core Optimizer installer hangs there, leaving the game
on "Running install script" on every launch.

Each recorded marker is now looked up in the registry, in both views
where both were written, and seeded again when it is gone; a record of
markers nobody wants any more is rewritten without them.

* Check every VC runtime architecture before keeping the vcrt marker

vcrt writes Installed for x86, x64 and arm64 in both registry views, but
present() only looked at x86. If only an x64 or arm64 value was gone the
runtime would not be seeded again. Require all six values and test the
loss of the arm64 Wow6432Node one.
2026-10-14 13:52:07 -04:00
Tjebbe Kerstens 7b018e6c42 fix: Batman: Arkham City starts with full x87 precision under FEX (#792)
Arkham City (BatmanAC.exe) stopped with Unreal's "Fatal error!" about
1.5 s after start: an access violation in BatmanAC.exe right after
steam.dll loads. The game leans on x87 and needs full precision under
FEX (FEX wiki), while the default FEX preset sets
FEX_X87REDUCEDPRECISION=1. Engine fixes sit above the shared preset and
below the player's own game profile, so a fix keyed on BatmanAC.exe
beside the started exe (BatmanAC.exe or BmLauncher.exe) restores
x87 precision for this game only. Arkham Asylum, which shares
BmLauncher.exe but not BatmanAC.exe, keeps the preset.
2026-10-14 13:52:01 -04:00
KirkandKirk e45d6f7192 fix: game shortcuts use the cover art when Steam's icon is too small (#825)
Steam keeps a 32x32 icon for each game, and the home screen stretched
it to launcher size. A square from the middle of the cover art is used
instead unless the icon is at least 128 px; the small icon is still
the fallback when there is no art.

Co-authored-by: Kirk <[email protected]>
2026-10-14 13:51:56 -04:00
Muaz Kadan b3ff93e80f Fix battery time estimates showing thousands of hours on phones that report current in mA (#820)
fix: read battery current reported in mA so time estimates aren't 1000x too long
2026-10-14 13:51:51 -04:00
William AllenandClaude Opus 5.5 c035351b4f fix: read the replaced listing's watch descriptor before detach can free it (#685)
install() replaces an existing cache entry by calling detach(old), which
destroys the listing when no reader holds it, and then reads old->wd to
decide whether to release the watch. When the old listing has no readers,
that read happens after the free.

Two threads that enumerate the same uncached directory at the same time
reach this path: both build a listing, the first installs an entry with
no readers, and the second replaces it.

Save the watch descriptor before detaching, as drop() already does.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-14 13:51:44 -04:00
Resul Övürandovurrsl 10cdf97480 fix: Turkish translation for zRAM, reset-settings and store storage messages (#826)
Adds the 14 translatable strings that values-tr was missing: the zRAM toggle and its hints, the Reset all settings row, dialog and toasts, and the store install free-space messages. Keys marked translatable=false stay untranslated on purpose.

Co-authored-by: ovurrsl <[email protected]>
2026-10-14 13:51:38 -04:00
Resul Övürandovurrsl f614483d01 docs: README names the child-process setting the way Android shows it (#831)
* docs: README names the child-process setting the way Android shows it

Developer options label this switch "Disable child process restrictions", and it has to be turned on. The README said to turn off "Restrict child processes".

* docs: README limits the child-process toggle to Android 14 and newer

The Developer options switch only exists from Android 14 (PhantomProcessLimit.hasDeveloperToggle); Android 12 and 13 keep the Fix it for me flow.

---------

Co-authored-by: ovurrsl <[email protected]>
2026-10-14 13:51:34 -04:00
Imgau f5eb5e7b81 fix: declare all-files access for raw USB storage (#760) 2026-10-14 13:48:14 -04:00
Kurt Himebauch bf99dbbe73 feat: store sign-in walls and the Steam home wall fill with curated featured art (#835) 2026-10-14 13:42:27 -04:00
Kushagra 46fac69438 feat: map a controller's two back paddles to the Deck's L4 and R4 (#797) 2026-10-14 15:31:59 +00:00
Gustash 82f04fb22e fix: the touchpad moves a game's own cursor when it locks the pointer (#772) 2026-10-14 11:30:54 -04:00
Kurt Himebauch 9796525449 perf: local builds reuse unchanged native steps across runs and worktrees (#819) 2026-10-14 15:30:24 +00:00
Kurt Himebauch 9838c83fc2 fix: store installs stay shortcuts instead of becoming Steam installs (#816) 2026-10-14 10:10:12 -04:00
Kurt Himebauch 799be037e5 Show controller-driven cursors in nested Wayland sessions (#759) 2026-10-14 08:25:08 -04:00
The412Banner d99d728ba5 fix(stores): drop expired Epic and Amazon tokens when a refresh fails (#804)
A failed token refresh returned the stored access token even after it had expired, so the next request failed with a generic error instead of asking to sign in again. The stored token is now returned only while it is still valid.

The Epic docs now say that achievements are sent with Epic's launcher user agent, which the achievements service requires.
2026-10-14 07:43:02 -04:00
The412Banner 4b17bc9a23 feat: rewrite the Epic and Amazon store modules (#802)
Both store modules are rewritten from a protocol-level spec: new Rust engines behind a small JNI surface (nativeCall/nativeInstall) and thin Kotlin facades, with the launcher's own user agent.

Epic: sign-in carries over, library, manifest download with byte-exact verification, assembly, uninstall, launch arguments, achievements.
Amazon: sign-in carries over, library, manifest download with per-file hash checks, path checks, executable detection from fuel.json, uninstall.
On-disk install layout is unchanged, so existing installs keep working.

Store game pages now take controller focus on their lead hero action (Install, Uninstall, Pause, Resume, Get, Buy), fall back to Back, and keep D-pad focus inside the page while it covers the library.
2026-10-14 07:20:06 -04:00
Kurt Himebauch 4ac78621cc docs: credit Phobos for Epic and GOG integration work (#801) 2026-10-14 06:41:42 -04:00
Kurt Himebauch 079261759e perf: the session's background watchers share two loops and one D-Bus service process (#575) 2026-10-14 01:02:24 -04:00
Kurt Himebauch 3fe03c0a88 perf: Steam's web helper runs its GPU, audio and zygote work inside one process (#566) 2026-10-14 01:01:49 -04:00
Kurt Himebauch 6211faf6bc Verify native SD-card library paths before binding them (#757) 2026-10-14 00:59:38 -04:00
Kurt Himebauch 81e645a917 Fix microphone capture in DirectAudio games (#747) 2026-10-14 00:58:52 -04:00
Kurt Himebauch 06b09b37bd Preserve native Linux and FEX compatibility choices (#749) 2026-10-14 00:57:45 -04:00
Kurt Himebauch 0c3444afbc Wait for Steam shortcut overviews before cold launches (#758) 2026-10-14 00:55:04 -04:00
Kurt Himebauch 0988cdc43a Run CodeRabbit reviews independently of APK builds (#777) 2026-10-14 00:29:37 -04:00
Kurt Himebauch 06bd0de4d7 feat: redesign the store game page's facts, description and achievements (#775) 2026-10-13 23:33:43 -04:00
Kurt Himebauch 3a408058ba Configure CodeRabbit reviews (#774) 2026-10-13 22:47:19 -04:00
Juan MartíandThe412Banner a074ba994b Improve store install storage checks and recovery; adaptive parallel Epic installs (#710)
Space checks before Epic, GOG and Amazon installs (measured zero vs unknown), clear failure reasons with retry/clear, resume-aware checks, and safe per-file replacement so a failed or cancelled update never breaks an installed game.

Epic installs stream chunks straight into staged files in parallel (most bytes written once, a small budgeted cache for shared chunks, about 1x game size instead of 2x), pace updates by free space, and keep the lookahead, batch and bounded Java writers as hidden alternatives.

Co-authored-by: Juan Marti Mercado <[email protected]>
Co-authored-by: The412Banner <[email protected]>
2026-10-13 22:29:23 -04:00
d310e59fce fix: desktop session: Shift no longer stays held after typing capitals on a physical keyboard (#766)
* fix: desktop session: Shift no longer stays held after typing capitals on a physical keyboard

Android repeats a held key as more key presses. Those were forwarded to the
session as more wl_keyboard presses, and KWin's xkb counts every press of a
modifier, so Shift held for a few capitals took several presses and one
release and stayed held: digits typed !@#, letters stayed capital. The same
applied to Ctrl and Alt.

- the compositor no longer sends a press for a key that is already down;
  releases always go through (clients repeat keys themselves from the
  repeat_info it already sends)
- the app stops forwarding Android's repeat events for keyboard keys

Reported by whatsupbros with a Keychron Q7 over USB - thank you.

* Remove stale comments from statx patch

---------

Co-authored-by: The412Banner <[email protected]>
Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-13 22:03:28 -04:00
Kurt Himebauch 0d0465d19e Fix Flatpak installs on older Android kernels (#752) 2026-10-13 21:31:26 -04:00
Kurt Himebauch 83ec5708ed cleanup (#764) 2026-10-13 21:27:42 -04:00
The412Banner 4163b31b02 fix: Windows components: .NET Core / Desktop runtimes install again (#740)
droiddeck-msi-install: a DuplicateFile entry onto the file itself (dotnet.exe in the .NET host MSI) is skipped like Windows Installer does, instead of stopping the whole install with SameFileError. Fixes .NET Desktop 3/6/7/8, .NET Core 3 and .NET 5. Thanks to whatsupbros for finding and tracing it.
2026-10-13 19:03:38 -04:00
Kurt Himebauch 02888834d4 fix: pace gamescope's fixed-refresh frame limiter per window by time (#753) 2026-10-13 22:21:01 +00:00
Kirk ec537421ac fix: the Debugging tools hint points to Setup > Diagnostics (#725) 2026-10-13 15:01:40 -04:00
Kirk 5e23c668f3 fix: the Android keyboard opens on the first tap after the system dismissed it (#665) 2026-10-13 14:17:31 -04:00
Jagger15 a12ac8134a Add experimental Quest 2 and Adreno 6xx display-driver support (#676) 2026-10-13 14:16:20 -04:00
0x1Nawaf 4562d61139 Feat/reset all settings (#696) 2026-10-13 13:36:31 -04:00
MaxsTechReview cc5ae4ff0b Add translations for 23 more languages and fill missing strings (#669)
* Add translations for 23 more languages and fill missing strings

* Translate the strings added in main into every language

Cardboard VR and the session drawer's component downloads in all 31
languages, the game details facts and HowLongToBeat labels in the 23 new
ones, and the reworded Components page note there.
2026-10-13 13:34:00 -04:00
MaxsTechReviewandxXJsonDeruloXx f0c52c273b Add opt-in zRAM compression for the Steam client while a game runs (#442)
* Add opt-in zRAM compression for the Steam client while a game runs

A "zRAM compression" toggle under Steam > Settings > Session, below
"Keep downloads running". Off by default, kept once set, and disabled
on devices without swap or with a kernel older than 5.4.

When on, the session watches for Steam's reaper. 20 s into a game the
client and its web helpers page their private anonymous memory out to
the device's zram with MADV_PAGEOUT from inside their own process
(libblsession zram.c, SIGURG), again every 5 minutes, and read every
swapped page back once the game exits. No root is needed: only a
process itself may do this without CAP_SYS_NICE. With the toggle off
nothing is installed or started.

RedMagic NP06J, Palworld: Steam's resident memory fell from ~1.7 GB to
~1.1 GB with ~560 MB in zram, available memory during play rose by
~0.5-0.8 GB, and ~2 MB was left in zram after the read-back.

* Turn zRAM compression on by default below 12 GB of RAM

Until the toggle is changed, it follows the device: on when the kernel
supports it and the device has less than 12 GB of RAM, read from the
advertised memory size on Android 14+ and from total memory before that.
Turning it off is kept.

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-13 13:33:19 -04:00
Kurt Himebauch 975c76e3e8 fix: don't count a re-attached unchanged buffer as a new game frame (#731) 2026-10-13 13:24:50 -04:00
Phobos665 a394dfd31b feat(): Rich Game details for GOG & Epic - Publisher, Developer, Release Date, HLTB & Metacritic (#712) 2026-10-13 11:29:03 -04:00
Kurt Himebauch a2e555f4cc Use preview-builds for the Discord channel name (#729) 2026-10-13 11:27:39 -04:00
Kurt Himebauch 31d12a9f6c Align Discord build names with the app (#726) 2026-10-13 11:17:41 -04:00
Kurt Himebauch 3820445561 feat: Cardboard VR mode with ARM64 SteamVR (#704)
* feat: add Cardboard mode with side-by-side lens-distorted eyes and 3DoF head tracking

* fix: derive the Cardboard field of view from the panel and recentre fully when far from upright

* feat: share the Cardboard head pose with the guest and add a SteamVR driver for it

* feat: run SteamVR's compositor on Turnip/KGSL with a display-extension layer and a driver-direct HMD

* feat: show SteamVR frames in Cardboard mode through a driver-side presenter and a fake direct-mode display

* feat: install and start SteamVR for Cardboard mode from the session drawer

* docs: describe Cardboard mode and the SteamVR path

* feat: drive SteamVR hands from the game pad and keep the Cardboard headset out of standby

* docs: record where the SteamVR dashboard stops

* fix: map Cardboard head tracking to the screen's axes and keep turns about the real vertical as yaw

* fix: give Cardboard its previous picture back when SteamVR stops

* fix: report sem_otime from emulated SysV semaphores so SteamVR's web helper can start

* fix: bridge SteamVR's shared semaphores across processes and keep Steam alive while SteamVR runs

* feat: present SteamVR straight to DroidDeck's compositor at panel resolution

* docs: update Cardboard VR notes for the dashboard, semaphore bridge and direct presenter

* fix: keep Wine and FEX signal handlers in the VR layer; add semaphore tracing

* fix: clear a stale SteamVR active marker on status

* fix: clear only an old SteamVR active marker

* fix: unblock SteamVR clients on Turnip and keep Steam's web helper alive across SteamVR runs

* fix: judge shared waits before a submit's own signals and on Vulkan 1.0 devices

* docs: record VR games, SteamVR Home and OpenXR results on the Thor

* docs: add Cardboard VR learnings from the Thor bring-up

* docs: add Cardboard VR screenshots from the Thor

* fix: keep the VR layer's display stand-in to SteamVR runs and clear a leftover marker at session start

* fix: report SteamVR start and stop failures from the helper's exit code
2026-10-13 14:36:56 +00:00
Kurt Himebauch 75d7edd2c3 fix: anchor wp_presentation timestamps to the vsync tick (#717) 2026-10-13 13:29:32 +00:00
Kurt Himebauch da8fe068d9 fix: cap the session refresh rate at the device's peak refresh rate (#715) 2026-10-13 13:16:29 +00:00
Kurt Himebauch 8f9ce19638 Fix FEX dropdown scaling and Discover search focus (#681) 2026-10-13 07:01:05 -04:00
Kurt Himebauch b74edb7e8d Download components from the session drawer (#679) 2026-10-12 22:18:47 -04:00
Jagger15 2503d69293 Add Quest 3/3S compatibility to the standard APK (#642) 2026-10-12 17:13:07 -04:00
Kurt Himebauch 5e38e79c96 Add opt-in GOG achievements through Comet (#651) 2026-10-12 16:40:06 -04:00
Kurt Himebauch 5e5ffee93e Default Snapdragon 8 Gen 2 core allocation (#654) 2026-10-12 16:26:24 -04:00
Phobos665andxXJsonDeruloXx a7662ae13a feat(): Achievements UI for GOG & Epic game details page (#641)
* feat(): gog & epic achievements UI

* feat(): AI-generated translations - Please get review for these

* build_local: run on macOS hosts with Docker Desktop

Two steps of tools/build_local.sh failed on a Mac:

- The uruntime staging runs on the host, where macOS's BSD install has
  no -D, so `install -Dm644` stopped with "No such file or directory".
  The folders are now made first and the files copied with install -m644.
- Inside the cross-compile container, Docker Desktop's VirtioFS share
  now and then refuses the chmod that install -m makes (EPERM, on a
  different file each run, e.g. "setting permissions for
  .../droiddeck-esync: Operation not permitted"). The overlay and
  desktop scripts are copied with a plain write instead. Nothing is
  lost: the APK keeps no file modes, and the app sets them when it
  stages the runtime.

* feat(): adjust the store

* feat(): store-achievements adjustment for UI

* feat(): translation fixes and separate hidden achievements

* Show all achievements directly on store game details

---------

Co-authored-by: xXJsonDeruloXx <[email protected]>
2026-10-12 19:49:55 +00:00
The412Banner a249e896f7 feat: Start without Steam, a per-game switch for games that close right away (#659) 2026-10-12 15:29:59 -04:00
The412Banner ca3743520e Merge remote-tracking branch 'org/main' into feat/start-without-steam 2026-10-12 15:16:35 -04:00
Kirk 98822fdd27 fix: a touchpad tap holds its click long enough for games to see it (#644) 2026-10-12 19:12:20 +00:00
Gustash 8d0225932c fix: an unfolded Follow screen session turns to portrait again (#626) 2026-10-12 15:07:35 -04:00
The412Banner 40bca96db5 feat: Start without Steam, a per-game switch for games that close right away
A game that closes as soon as it starts can be tripped by Proton's Steam
setup (steam.exe, the Steam registry entries, the Steam client bridge).
The Games page of the session drawer gets a per-game Start without Steam
switch, stored as DROIDDECK_NO_STEAM=1 in the game's environment and listed
in its profile summary.

droiddeck-game-env acts on it for a game launch only: a Proton with umu's
launch (GE-Proton, Proton-CachyOS, DroidDeck-Proton) gets UMU_ID, so it
runs umu.exe in place of steam.exe and still sets up the prefix; a Steam
game keeps its protonfixes under its own id, an added game gets 0. Any
other Proton runs the game straight in the prefix (runinprefix).
2026-10-12 13:55:56 -04:00
Muaz Kadan a2a9586b54 fix: Shut Down in Steam's power menu ends the session, and Restart restarts the client (#629) 2026-10-12 13:44:16 -04:00
Muaz Kadan 2fe6ba36c4 Fix the QAM button doing nothing when the pad is an Xbox or PlayStation controller (#631) 2026-10-12 13:35:41 -04:00
Phobos665 e4c84e6e24 chore(): Fix the MacOS build process locally. (#637) 2026-10-12 17:11:01 +00:00
Kurt Himebauch 10ef8c4bd6 chore: bump version to 0.4.0 (13) (#627) 2026-10-12 10:56:11 -04:00
Kurt Himebauch 8e01cfb2ef docs: draft DroidDeck 0.4.0 release notes (#619)
* docs: draft 0.4.0 release notes

* docs: add Thor store and launcher media for 0.4.0

* docs: show Steam switching to KDE on Thor

* docs: show Castle Crashers touch layouts and controller choices

* docs: add Amazon game install view

* docs: add trimmed user recording of store downloads

* docs: smoother touch controller recording, GOG sign-in and per-game profile screenshots

* docs: clean desktop round-trip recording replaces the KDE gif and file explorer still

* docs: add multi-controller and Steam Controller media, rebuild desktop gif without glitch frames

* docs: add foldable touch gamepad screenshots from its PR

* docs: add the Discord badge to the 0.4.0 notes and tidy contributor lines

* docs: fold fixes to new 0.4.0 features into their entries, add Follow screen shots, point the changelog at the 0.4.0 tag

* docs: add the Add a game dialog to the non-Steam games entry

* docs: add a TL;DR for Mr. Sujano to the 0.4.0 notes
2026-10-12 10:49:14 -04:00
Neolyum b25c4ea26b Fix multiple controller discovery (#614) 2026-10-12 11:45:00 +00:00
Kurt Himebauch cebad5692d feat: store uninstall confirms in a step-out, shows progress, and lands on Install (#609)
feat: store uninstall confirms in a step-out, shows its progress in place, and the game reads Install the moment it is gone
2026-10-12 11:03:03 +00:00
Kurt Himebauch 6b2c8ace09 fix: keep the Steam desktop client in KDE (#610) 2026-10-12 06:56:56 -04:00
Kurt Himebauch 4d1b8421fb feat: store game pages flood onto their hero art, and downloads survive a restart (#608)
* feat: a store game's page floods out of its card onto its own hero art, and draws back from it

* fix: installs interrupted by the app closing show in Downloads with Resume and Clear

* feat: finished and interrupted downloads share the running card's art look, and the queue sits under what is running

* fix: the download queue outlives the app: queued, running and paused downloads come back after a restart
2026-10-12 03:58:07 +00:00
Kurt Himebauch ced4ac5db8 fix: a step-out out of a wide control draws its box again (#607) 2026-10-12 03:31:48 +00:00
Kurt Himebauch e190f57b53 feat: components newest first, Steam settings open on Steam with offline mode, smoother chip selection (#606)
* feat: components list newest first, Steam's sheet opens on its Steam tab with offline mode, and Setup's overview drops the account group

* fix: chip selections stretch across on every move, drawn over the chips instead of behind them
2026-10-12 03:25:55 +00:00
Kurt Himebauch 22f02fb6bf feat: the focus ring is the only fill and keeps up with focus (#605)
* feat: with a pad the focus ring fills what it holds and is the only fill; chip rows mark their pick with a bar

* fix: the focus ring chases focus on a continuous spring per edge instead of restarting on every move
2026-10-12 03:16:07 +00:00
Kurt Himebauch 52c67df4c7 fix: store chips mark the shown store without faking focus, and step-outs open and fold cleanly (#604)
* fix: a chip selection hopping past a chip fades across instead of crawling over as a drop

* fix: the store chips' fill only shows where a pad's focus is, not on the shown store

* feat: the shown store's chip carries a bar on its foot, every chip pulses when picked, and the queue's settings live only in the cog

* fix: step-outs grow and fold in one move with no tail, keep the ring out while moving, and hand focus back to their control
2026-10-12 03:04:33 +00:00
Kurt Himebauch de1a734fb5 fix: settings sheet focus ring, Left to fold, chips pick on focus (#600) 2026-10-12 02:11:53 +00:00
Kurt Himebauch be9939d82c Fix black Steam output with FSR 1 (#597) 2026-10-11 21:03:07 -04:00
Kurt Himebauch e4c375ad0d Fix Discord launcher layout and controller navigation (#596) 2026-10-11 20:08:59 -04:00
Gustash 0cafa83ea7 fix: Epic games with a JSON manifest download (#578) 2026-10-11 17:37:47 -04:00
Juan MartíandKurt Himebauch 435beb7f19 feat: add persistent per-game compatibility profiles (#449)
* feat(profiles): add persistent per-game compatibility profiles

* fix(profiles): show Windows component overrides

Include per-game Windows component selections in the resolved profile summary so the session overlay marks those games as custom and shows the selected components.

* fix(session): detect Proton games across runtime paths

* fix(profiles): select the active Gamescope game

* fix(profiles): improve per-game reset feedback

* fix(profiles): align session components with game overrides

* fix(session): prefer selected Steam game during refresh

* fix(session): resume profile following when reopening games

* chore: keep local dev testing outside the repository

* test(session): remove local dev package path

* fix(session): preserve profiles when refresh fails

* fix(session): reject empty game profile reset scope

* perf(session): cache drawer inventory by refresh revision

* fix(profiles): align game presets and profile reset behavior

* fix(profiles): preserve originals and normalize shortcut ids

* fix(profiles): keep editor scope stable after rebase

* fix(tests): host Compose tests in both build variants

* fix(profiles): recover interrupted component selection saves

---------

Co-authored-by: Kurt Himebauch <[email protected]>
2026-10-11 17:00:28 -04:00
The412Banner 78fcb60fd5 DroidDeck-Proton as a Proton source, and update checks for every source (#588) 2026-10-11 16:07:44 -04:00
The412Banner 4995b3553b Merge remote-tracking branch 'org/main' into feat/droiddeck-proton-source 2026-10-11 15:56:18 -04:00
MaxsTechReview 3c2878cc52 Red Dead Redemption 2: launches again, no screensaver over games, hidden launcher held to one core, /tmp cleaned (#585)
* fix: Red Dead Redemption 2 launches again, its Vulkan runtime installer is left to run

droiddeck-seed-redists marked every process in every game's install script
as already run, Red Dead Redemption 2's VulkanRT-1.1.108.0 installer
included. Steam's compat config for the game says nativevulkanloader, so
Proton loads vulkan-1.dll native only, and Khronos' loader is in the prefix
only once that installer has run. Without it the Rockstar launcher starts
RDR2.exe and it exits with STATUS_DLL_NOT_FOUND (0xc0000135) before it
opens a window.

A Vulkan runtime installer is now left out of the seeding, and a marker an
earlier seeding wrote for one comes out of each prefix and Proton template
once (dates kept), so the client runs the installer on the next launch.
The installer finishes in a few seconds under FEX.

* fix: Steam's screensaver no longer covers a running game

The client's defaults show screensavers while a game runs, after 300 s on
AC power without a button press - and a held stick or trigger is no press.
Red Dead Redemption 2's intro runs longer than that: the slideshow covered
the game, and the button that woke it reached the game too (B asked to
quit to the desktop). Customization/Screensaver/WhileInGame is seeded "0"
in config.vdf once, like the UI scale and native resolution, so turning it
back on in Settings stays the user's choice.

On the RedMagic: 18 minutes from the last button press through the intro
and riding, focus never left the game and no screensaver was created.

* fix: Rockstar's hidden launcher no longer takes cores from its game

The Rockstar Games Launcher keeps drawing its Chromium interface
(SocialClubHelper.exe: renderers, and a GPU process compositing through
DXVK) for as long as the game runs, hidden behind it - and Wine passes
Chromium's priorities on, so those threads ran above the game's (the GPU
process at nice -10), across every core. With Red Dead Redemption 2 in
front that was 1.3 to 2.5 cores under FEX for a page nobody sees.

droiddeck-launcher-cores follows gamescope's focus from the agent's
focus.json: while a game window has it, every SocialClubHelper thread runs
on the last core of the slowest cluster at nice 19; when the launcher's own
window has focus again each thread gets its own mask and priority back
(also on exit). Stopping the helpers would not hold - proot resumes a
stopped tracee. Started for Steam sessions; BL_LAUNCHER_CORES=0 turns it off.

RedMagic, RDR2 all Low, riding at the thermal limit, same spot and session:
released 46.5 fps, 1% low 27.5, p99 31.8 ms, CPU 89%, 2200 MHz;
held 48.7 fps, 1% low 32.6, p99 27.6 ms, CPU 79%, 2638 MHz.

* fix: sessions clean up what earlier ones left in /tmp

The guest's /tmp is a directory in the rootfs that no boot ever empties,
and Android ends sessions by killing them. Every session left a
/tmp/mangohud.XXXXXXXX (mktemp, never removed), and the Steam client, its
web helper, Chromium apps, dbus-run-session, AppImage runs and KDE their
own temporary files and sockets: 1,422 entries on the RedMagic.

mangoapp's config is now one fixed file rewritten each session, and the
session's first pass sweeps each kind of leftover while nothing that could
own one is running, with a single find. On the device that cleared 1,391
entries over two sessions, leaving 38.
2026-10-11 15:53:01 -04:00
The412Banner 715032062d feat: DroidDeck-Proton as a Proton source, and update checks for every source
Setup > Proton versions lists DroidDeck-Proton (The412Banner/DroidDeck-Proton) beside
GE-Proton and proton-cachyos: same aarch64 tarball pattern as GE, verified against its
sha512 file, installed by droiddeck-proton-extra (also as "droiddeck" in the session queue)
and adopted by steam-compatibility like any other extra, so it shows as
"DroidDeck-Proton <ver> (Compatible)". Its folder has a space; nothing in the install,
adopt, esync or components paths splits on it.

Update checks: each source's newest full release is looked up at app start and when the
page opens, at most every 6 h (offline keeps the last answer). A newer release shows
"Update available -> <tag>" with Update; it installs beside the old build, which stays
until removed ("Remove old ... versions"), since games are mapped to it by name.
Installed builds are ordered by their version numbers (11-9 before 11-10), not by name,
and the tag each was installed from is recorded. Prereleases and drafts are skipped.
2026-10-11 15:32:07 -04:00
Kurt Himebauch dde08414fc feat: big ass redesign: Carry the stores' motion and layout into Steam settings, Components, Setup, Flathub and Desktop (#548) 2026-10-11 19:05:21 +00: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
1152 changed files with 160767 additions and 39284 deletions

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
language: en-US
early_access: false
tone_instructions: >-
Prioritize actionable correctness, security, and runtime regressions. Keep
feedback concise. Do not request explanatory code comments; preserve license
headers and compiler-consumed annotations.
reviews:
profile: chill
request_changes_workflow: false
high_level_summary: true
high_level_summary_in_walkthrough: true
poem: false
sequence_diagrams: false
tools:
github-checks:
enabled: false
auto_review:
enabled: true
auto_incremental_review: true
auto_pause_after_reviewed_commits: 0
drafts: false
pre_merge_checks:
docstrings:
mode: "off"
title:
mode: "off"
description:
mode: "off"
finishing_touches:
docstrings:
enabled: false
unit_tests:
enabled: false
simplify:
enabled: false
autofix:
enabled: false
fix_ci:
enabled: false
resolve_merge_conflict:
enabled: false
path_instructions:
- path: "**/*"
instructions: >-
For cleanup changes, compare executable behavior against the PR merge
base. Check for removed guards, changed return values, weakened compiler
flags, and deleted regression tests. A passing APK build alone does not
establish behavior preservation. Avoid style-only or comment-coverage
requests.
- path: "app/src/main/**"
instructions: >-
Review Android lifecycle, external-display visibility, input cancellation,
permissions, threading, and Compose state. For JNI, native, and Rust
changes, check ABI compatibility, resource ownership, synchronization,
and error paths. Preserve persisted user settings and existing runtime
contracts.
- path: "tools/**"
instructions: >-
Check shell quoting, error handling, path and filesystem semantics, and
Python helper behavior. Preserve MSI same-file protection and strict
compiler checks. Verify downstream patches apply to their pinned sources
and retain intended executable behavior, including embedded scripts.
- path: ".github/workflows/**"
instructions: >-
Check job permissions, triggers, merge validation, build variants,
signing, artifact paths, and release conditions. Verify checks apply to
the current PR head and keep review configuration advisory.
+4
View File
@@ -0,0 +1,4 @@
tools/linuxfs/** text=auto eol=lf
tools/mangoapp/** text=auto eol=lf
tools/comet/LICENSE whitespace=-blank-at-eof
tools/comet/droiddeck.patch whitespace=-blank-at-eol,-blank-at-eof
-2
View File
@@ -1,7 +1,5 @@
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:
+92
View File
@@ -0,0 +1,92 @@
name: Build desktop package (KDE Plasma)
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)"
- 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
-6
View File
@@ -1,9 +1,5 @@
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:
@@ -43,6 +39,4 @@ jobs:
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
-49
View File
@@ -1,49 +0,0 @@
name: Build wlroots (patched)
# The desktop's wlroots (Arch Linux ARM's wlroots0.20, what the desktop package's labwc links)
# rebuilt with tools/wlroots/patches: a Vulkan DMA-BUF allocator for the Adreno node, which is not
# a DRM device, so labwc's vulkan and gles2 renderers can start. Built in an Arch Linux ARM
# container on an arm64 runner and published as a release asset the apk build fetches
# (tools/wlroots/release.env names the tag and sha). The hosted desktop package is never changed.
on:
push:
paths:
- 'tools/wlroots/**'
- '.github/workflows/build-wlroots.yml'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish as (e.g. wlroots-0.20.2-p1); empty = artifact only'
required: false
default: ''
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-24.04-arm
timeout-minutes: 90
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/wlroots/build-in-arch.sh
sudo chown -R "$(id -u):$(id -g)" .
- uses: actions/upload-artifact@v4
with:
name: wlroots-patched
path: |
wlroots.tzst
wlroots.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 "wlroots 0.20.2 with tools/wlroots/patches (Vulkan DMA-BUF allocator for non-DRM GPU nodes); staged by the apk at /usr/local/lib/droiddeck-wlroots for the desktop's GPU renderers. sha256 $(cut -d' ' -f1 wlroots.tzst.sha256)" wlroots.tzst wlroots.tzst.sha256 || gh release upload "${{ inputs.tag }}" wlroots.tzst wlroots.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
+120 -96
View File
@@ -22,8 +22,6 @@ permissions:
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' }}
@@ -44,6 +42,19 @@ jobs:
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"
@@ -173,7 +184,7 @@ jobs:
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/**') }}
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'app/src/main/cpp/adrenotools/src/hook/kgsl.h', 'tools/quest/*', '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/comet/**', 'tools/linuxfs/debian-image.env', 'tools/vr/**') }}
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
if: steps.preloads.outputs.cache-hit != 'true'
@@ -195,13 +206,17 @@ jobs:
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"
# Force-SSBS adapter, preloaded into a game's Wine only when the game asks for it.
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"
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"
VK_INCLUDE=$(ls -d "${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"/toolchains/llvm/prebuilt/*/sysroot/usr/include | head -1) \
tools/vr/build.sh --native "$D/usr/local/lib/droiddeck-vr"
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
@@ -210,20 +225,12 @@ jobs:
done
if [ -f tools/gamescope/release.env ]; then
. tools/gamescope/release.env
gh release download "${GAMESCOPE_TAG}" -R "${GITHUB_REPOSITORY}" -p gamescope.tzst -O gamescope.tzst
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
if [ -f tools/wlroots/release.env ]; then
. tools/wlroots/release.env
gh release download "${WLROOTS_TAG}" -R "${GITHUB_REPOSITORY}" -p wlroots.tzst -O wlroots.tzst
echo "${WLROOTS_SHA256} wlroots.tzst" | sha256sum -c -
tar --use-compress-program=unzstd -xf wlroots.tzst -C "$D"
test -f "$D/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so"
rm wlroots.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 -
@@ -243,14 +250,12 @@ jobs:
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
@@ -262,10 +267,12 @@ jobs:
done
install -m644 msi-pkgs/usr/bin/msiinfo "$T/msiinfo"
install -m644 msi-pkgs/usr/bin/cabextract "$T/cabextract"
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; do
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
@@ -273,17 +280,38 @@ jobs:
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/autostart "$D/etc/xdg/labwc/autostart"
install -Dm644 tools/linuxfs/desktop/rc.xml "$D/etc/xdg/labwc/rc.xml"
install -Dm644 tools/linuxfs/desktop/panel.conf "$D/etc/xdg/lxqt/panel.conf"
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"
- 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 debian:bullseye bash -c '
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
@@ -331,6 +359,8 @@ jobs:
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
- 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
@@ -340,12 +370,12 @@ jobs:
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-sink.so pa-bundle/modules/arm64/module-directaudio-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 .)
mv pulseaudio.tzst app/src/main/assets/pulseaudio.tzst
tar --use-compress-program=unzstd -tf app/src/main/assets/pulseaudio.tzst | grep -c '\.so$'
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
@@ -364,42 +394,34 @@ jobs:
- name: Check the preloads
run: |
set -euo pipefail
D=app/src/main/assets/linuxfs
NEED=$(readelf -d "$D/libfakeinput.so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p')
for bad in libstdc++.so.6 libgcc_s.so.1; do
if printf '%s\n' "$NEED" | grep -qx "$bad"; then
echo "::error::libfakeinput.so links $bad - the C++ runtime must stay static"; exit 1
fi
done
syms() { readelf -Ws "$1" | awk '$4 == "FUNC" && $5 == "GLOBAL" {sub(/@.*/, "", $8); print $8}'; }
FAKE=$(syms "$D/libfakeinput.so")
for sym in open openat ioctl read close poll ppoll select stat fstat access scandir; do
grep -qx "$sym" <<<"$FAKE" || { echo "::error::libfakeinput.so does not export $sym"; exit 1; }
done
# The static C++ runtime stays private: every session process preloads this library,
# and an exported personality routine takes over other programs' unwinding (shadPS4
# aborted in pthread_exit).
if grep -qxE '__gxx_personality_v0|__cxa_throw|_Unwind_Resume' <<<"$FAKE"; then
echo "::error::libfakeinput.so exports the C++ runtime"; exit 1
fi
SESSION=$(syms "$D/libblsession.so")
for sym in socket bind getsockname setsockopt statfs statvfs syscall shm_open shm_unlink mmap mmap64 munmap drmPrimeFDToHandle drmCloseBufferHandle; do
grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; }
done
# Zink's GEM_CLOSE is caught at ioctl(); an exported drmIoctl made NMS replay old frames.
if grep -qx drmIoctl <<<"$SESSION"; then
echo "::error::libblsession.so exports drmIoctl"; exit 1
fi
SSBS=$(syms "$D/libssbs.so")
for sym in sigaction signal bsd_signal sysv_signal; do
grep -qx "$sym" <<<"$SSBS" || { echo "::error::libssbs.so does not export $sym"; exit 1; }
done
test -f "$D/usr/local/bin/droiddeck-session"
test -f "$D/usr/local/bin/droiddeck-proton-extra"
test -f "$D/usr/local/bin/droiddeck-msi-install"
for f in msiinfo cabextract libmsi-1.0.so.0 libgsf-1.so.114 libgcab-1.0.so.0; do
test -f "$D/usr/local/lib/droiddeck-msitools/$f" || { echo "::error::droiddeck-msitools/$f missing"; exit 1; }
done
bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs
- 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
@@ -411,14 +433,50 @@ jobs:
restore-keys: |
cxx-${{ env.NDK_VERSION }}-
- name: Build GOG Galaxy companion
if: steps.preloads.outputs.cache-hit != 'true'
run: |
rustup toolchain install 1.90.0 --profile minimal
rustup target add --toolchain 1.90.0 aarch64-unknown-linux-gnu
RUSTUP_TOOLCHAIN=1.90.0 bash tools/comet/build.sh
- 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/quest/build-driver.py "$ANDROID_HOME/ndk/$NDK_VERSION"
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
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
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'
@@ -426,27 +484,6 @@ jobs:
- name: Unit tests
run: ./gradlew testReleaseUnitTest --console=plain -PndkVersion="$NDK_VERSION"
- name: Check the native dependency closure
run: |
set -euo pipefail
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
WORK=$(mktemp -d)
unzip -q "$APK" 'lib/arm64-v8a/*' -d "$WORK"
cd "$WORK/lib/arm64-v8a"
SYSTEM="libc.so libm.so libdl.so liblog.so libandroid.so libz.so libvulkan.so libstdc++.so
libGLESv2.so libEGL.so libnativewindow.so libjnigraphics.so libaaudio.so
libOpenSLES.so libmediandk.so libcamera2ndk.so libsync.so libneuralnetworks.so"
fail=0
for so in *.so; do
for need in $(readelf -d "$so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p'); do
[ -f "$need" ] && continue
case " $(echo $SYSTEM) " in *" $need "*) continue ;; esac
echo "::error::$so needs $need, which is neither in the apk nor a platform library"
fail=1
done
done
exit $fail
- name: Sign with the testkey (v1 + v2 + v3)
run: |
set -euo pipefail
@@ -561,12 +598,6 @@ jobs:
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'
@@ -593,8 +624,6 @@ jobs:
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 }}
@@ -644,15 +673,12 @@ jobs:
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"
@@ -668,7 +694,7 @@ jobs:
echo "apk=$GITHUB_SERVER_URL/$CI_REPO/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
echo "Published $url"
- name: Post to Discord
- name: Post the Preview build to Discord
env:
WEBHOOK: ${{ secrets.DISCORD_BUILDS_WEBHOOK }}
GH_TOKEN: ${{ github.token }}
@@ -682,8 +708,6 @@ jobs:
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
@@ -692,7 +716,7 @@ jobs:
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")
content=$(printf '**New signed Preview build** `main-%s`\n%s\nIn the app: **Updates → Preview**\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"
+51
View File
@@ -0,0 +1,51 @@
name: Publish the CI Debian image
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
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"
+33 -5
View File
@@ -1,9 +1,9 @@
name: PR Release Cleanup
run-name: remove PR #${{ github.event.pull_request.number }}'s test build
run-name: remove PR
# 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 ]
@@ -13,13 +13,27 @@ 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:
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
- 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"
- uses: actions/create-github-app-token@v2
if: ${{ steps.state.outputs.closed == 'true' }}
id: ci-token
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
@@ -27,6 +41,7 @@ jobs:
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 }}
@@ -42,6 +57,19 @@ jobs:
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() }}
+14 -37
View File
@@ -11,8 +11,6 @@ on:
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]
@@ -26,6 +24,7 @@ permissions:
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:
@@ -52,7 +51,6 @@ jobs:
- 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"
@@ -67,8 +65,6 @@ jobs:
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"
@@ -93,9 +89,6 @@ jobs:
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 \
@@ -115,8 +108,6 @@ jobs:
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
@@ -126,8 +117,6 @@ jobs:
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"
@@ -178,13 +167,15 @@ jobs:
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 }}
@@ -203,7 +194,6 @@ jobs:
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:
@@ -245,7 +235,6 @@ jobs:
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' |
@@ -307,39 +296,27 @@ jobs:
$NEXT
EOF
)
id=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate \
--jq ".[] | select(.body | startswith(\"$mark\")) | .id" | head -1 || true)
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
# Asked for (the label, a manual run): always announced. Signed on its own after a push:
# only a PR's first test build is, and later ones update its release quietly.
- name: Post to Discord
if: ${{ always() && steps.publish.outcome == 'success' && (needs.authorize.outputs.automatic != 'true' || steps.publish.outputs.first == 'true') }}
- name: Update the Discord PR announcement
if: ${{ always() && steps.publish.outcome == 'success' }}
env:
WEBHOOK: ${{ secrets.DISCORD_BUILDS_WEBHOOK }}
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: |
set -euo pipefail
if [ -z "$WEBHOOK" ]; then
echo "::notice::No DISCORD_BUILDS_WEBHOOK secret; nothing posted"
exit 0
fi
short=${HEAD_SHA:0:7}
title=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .title)
what="[#$PR_NUMBER]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$PR_NUMBER): $title"
get=$(printf 'Download: [standard APK](<%s>) · [all variants](<%s>)' "$APK" "$RELEASE")
content=$(printf '**New signed PR test build** \`pr-%s\` · \`%s\`\n%s\n_Not merged — for testing._\n%s' "$PR_NUMBER" "$short" "$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"
run: python3 tools/release/pr_discord.py publish
catalog:
needs: publish
-3
View File
@@ -63,7 +63,6 @@ jobs:
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]
@@ -94,7 +93,6 @@ jobs:
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/"
@@ -110,7 +108,6 @@ jobs:
--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
+1 -1
View File
@@ -13,13 +13,13 @@ permissions:
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
+5 -6
View File
@@ -8,19 +8,17 @@ 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
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
@@ -32,5 +30,6 @@ 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/
/tools/vr/out/
+10 -6
View File
@@ -15,22 +15,26 @@ DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big
## 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.
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.
On Android 14 and newer, before Steam launches, you must turn on **Disable child process restrictions** 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 session folder from `Download/DroidDeck/` so the logs come with it.
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, and `zstd` installed. It builds the ARM64 audio sinks from PulseAudio 13.0 and packages them into the APK 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. To install the APK on an attached device, run `tools/deploy_local.sh`.
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. See the session logs in `Download/DroidDeck/` when diagnosing problems.
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.
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. 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.
+70 -37
View File
@@ -49,17 +49,22 @@ if (!(treeState in ['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' }
// tools/release/variants.txt as "package=suffix;...", so the updater picks the apk of the package it runs as.
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)
}
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]}" }
@@ -68,19 +73,14 @@ def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines()
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 11
versionName "0.3.1"
versionCode 13
versionName "0.4.0"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
@@ -88,6 +88,11 @@ android {
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'
@@ -107,10 +112,6 @@ android {
}
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"
@@ -135,24 +136,15 @@ android {
}
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']
}
}
@@ -163,8 +155,8 @@ android {
}
testOptions {
// Plain JVM tests: a stray android.util.Log call returns a default instead of throwing.
unitTests.returnDefaultValues = true
unitTests.includeAndroidResources = true
}
compileOptions {
@@ -176,8 +168,6 @@ android {
}
}
// 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/**'
@@ -186,30 +176,73 @@ def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
}
tasks.named('preBuild').configure { dependsOn(stageSessionScripts) }
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) }
def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) {
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)
}
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
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()
commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26',
'--output-dir', rustJniLibsDir.get().asFile.absolutePath,
'build', '--release', '--locked'
}
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.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
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'
testImplementation 'androidx.compose.ui:ui-test-junit4'
testRuntimeOnly 'com.github.luben:zstd-jni:1.5.2-3@jar'
}
+13 -40
View File
@@ -4,30 +4,21 @@
<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.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- 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.POST_NOTIFICATIONS" />
<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" />
<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" />
@@ -45,11 +36,6 @@
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
@@ -77,10 +63,6 @@
</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"
@@ -97,16 +79,7 @@
</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"
@@ -127,6 +100,12 @@
android:screenOrientation="sensorLandscape"
android:theme="@style/AppTheme" />
<activity
android:name=".stores.StoreLoginActivity"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:theme="@style/AppTheme" />
<activity
android:name=".agent.AgentStartActivity"
android:exported="true"
@@ -146,24 +125,20 @@
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" />
<!-- Hands a zipped session folder to the share sheet; nothing else is exposed. -->
<service
android:name=".stores.download.StoreDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.logs"
@@ -173,8 +148,6 @@
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"
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
@@ -1,115 +0,0 @@
# DirectAudio for the Linux Steam client (relay build)
This is the driver for games that run under **Valve's ARM64 Proton inside a Linux
runtime on Android** - Bannerlator's linuxfs session with the native Steam client -
where the game process is a glibc process and cannot touch Android's audio
libraries itself.
Two pieces, both required:
| piece | what it is | where it runs |
|---|---|---|
| `aarch64-unix/winedirectaudio.so` + the two PE shells | the Wine `mmdevapi` driver, glibc build, **no AAudio inside** | in the game, inside the Linux rootfs |
| `directaudio-relay` | a small bionic program that owns the AAudio output and microphone streams | on the Android side, under the app's uid |
They meet over a unix socket. The driver connects, sends its launch config, and
gets back two shared-memory rings: one it fills with mixed game audio, one the
helper fills with microphone audio. The helper plays and records; the driver
keeps doing everything else it always did.
## Which Proton
Built inside `ValveSoftware/wine` at `proton_11.0`. The private `mmdevapi`
interface it implements is byte-identical across **Proton 11.0 (ARM64)**,
**Proton Experimental (ARM64)** and our own Wine-11 layers (checked 2026-09-19),
so this one build serves any of them. A future Proton on a Wine 12 base will
need a rebuild - the interface is not versioned, and a mismatch is silence, not
an error.
## Installing the driver next to Proton (not into it)
Steam verifies and updates its Proton depots, so files added under
`steamapps/common/Proton .../files/lib/wine/` will not survive. Put the driver
in a directory of its own and point Wine at it:
```
<somewhere>/directaudio/lib/wine/aarch64-unix/winedirectaudio.so
<somewhere>/directaudio/lib/wine/aarch64-windows/winedirectaudio.drv
<somewhere>/directaudio/lib/wine/i386-windows/winedirectaudio.drv
```
and set, for the game process:
```
WINEDLLPATH=<somewhere>/directaudio/lib/wine
```
Wine searches `WINEDLLPATH` entries with the per-arch subdirectories appended,
so both the PE shell and the unixlib are found there. In Bannerlator's runtime
the natural place to export it is the `bannerlator-proton` compatibility-tool
wrapper, which every Proton launch goes through.
## Selecting the driver
`mmdevapi` only tries the drivers named in the registry (or its built-in
`pulse,alsa,oss,coreaudio` list, which does not know about this one). Set, in the
game's prefix:
```
wine reg add "HKCU\Software\Wine\Drivers" /v Audio /d directaudio /f
```
Under Proton the prefix is `compatdata/<appid>/pfx`; run the command with that
prefix as `WINEPREFIX` using Proton's own `bin-arm64/wine`, or write the key from
the wrapper before it hands off to Proton.
## Running the helper
```
directaudio-relay --socket <path> [--log]
```
- Run it **under the app's uid**, the way the app already runs its PulseAudio
daemon. Android checks `RECORD_AUDIO` against the calling uid, so a helper
started any other way can play but cannot record.
- Start it **before** the game: the driver asks "is a helper there?" once, when
Wine picks an audio driver, and falls back to unavailable if not.
- The socket path must be reachable from inside the Linux rootfs. Bannerlator
binds the app's files directory and its runtime directory into the session at
their own paths, so any socket under either works unchanged on both sides.
- The helper serves any number of game processes; each gets its own streams.
It exits on `SIGTERM`; a game exiting tears down that game's streams only.
## Telling the driver where the helper is
```
BANNER_AUDIO_DIRECT_RELAY=<the same socket path>
```
Unset, the driver looks for `$XDG_RUNTIME_DIR/directaudio-relay`.
## Everything else is unchanged
All `BANNER_AUDIO_DIRECT_*` knobs mean what they mean in the in-process build:
`_PERF`, `_ADAPTIVE`, `_DECAY`, `_MS`, `_MAXMS`, `_BF`, `_MBF`, `_EXCLUSIVE`,
`_WATCHDOG`, `_STALL_MS`, `_DECAY_*`, `_LOG`, `_PERIOD_MS`, `_MINPERIOD_MS`, and
the live-config mailbox `_RUNTIME` (the helper watches the file; it must be a
path the helper can read on the Android side). **`BANNER_AUDIO_DIRECT_MIC=1`**
exposes the capture endpoint exactly as before: the helper opens the input
stream (`VOICE_COMMUNICATION` preset) when the game connects and makes it hot on
the game's first capture start.
One thing is different: there is one extra hand-off, the ring. The helper's
callback pulls a burst from it, so the driver keeps two bursts queued (about 8 ms
on a 192-frame device) on top of the AAudio buffer the presets control, and the
helper raises that if a callback ever finds the ring short. `get_latency` reports
the sum, so a game that sizes its own buffers from it still sees the truth.
## Proving it is running
- Helper side: `logcat -s DA-Relay:I` shows `hello from <exe>`, then `open: buffer
... ring target ...`, then `capture open` / `capture start` for the mic.
- Game side: the session log carries `DirectAudio: relay: connected - ...`.
- The game process's `/proc/<pid>/maps` shows `winedirectaudio.so` and **no**
`libaaudio.so` (it is in the helper's maps instead). The AudioFlinger track is
owned by the helper's pid.
@@ -1,4 +0,0 @@
directaudio linux-wine11 relay build
source 3548a4a730e8d01cb0cb0268d51a2fb8f23dcd33
wine ValveSoftware/wine@proton_11.0
unixlib.h sha256 fdbed263338d0719c4d12e02b045c23721de33219ec8878e4c2d7ebc9efcbe09
+1 -16
View File
@@ -1,27 +1,16 @@
# 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.
# DXVK's DXBC->SPIR-V translator is zlib licensed (vendored under thirdparty/dxbc).
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
@@ -44,11 +33,7 @@ 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)
@@ -3,7 +3,6 @@
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
@@ -1,6 +1,4 @@
/* 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,
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
@@ -11,29 +12,14 @@ extern "C" {
#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
ADRENOTOOLS_BCN_INCOMPATIBLE,
ADRENOTOOLS_BCN_BLOB,
ADRENOTOOLS_BCN_PATCH
};
/**
* @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
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
@@ -10,60 +11,16 @@ extern "C" {
#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
@@ -1,13 +1,11 @@
// 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,
@@ -16,12 +14,9 @@ enum {
#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 gpu_addr;
uint64_t size;
uint64_t flags;
};
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <array>
#include <cstdio>
#include <cstdlib>
@@ -21,7 +22,6 @@ static loader_android_create_namespace_t loader_android_create_namespace;
static bool lib_loaded;
/* Public API */
bool linkernsbypass_load_status() {
return lib_loaded;
}
@@ -55,8 +55,6 @@ 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);
}
@@ -81,7 +79,6 @@ void *linkernsbypass_namespace_dlopen(const char *filename, int flags, android_n
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{[&] () {
@@ -89,7 +86,6 @@ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *li
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)
@@ -101,21 +97,18 @@ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *li
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);
@@ -125,21 +118,19 @@ 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
int32_t offset : 26;
uint8_t sig : 6;
};
bool Verify() {
@@ -148,10 +139,8 @@ __attribute__((constructor)) static void resolve_linker_symbols() {
};
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++;
@@ -159,10 +148,8 @@ __attribute__((constructor)) static void resolve_linker_symbols() {
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;
@@ -187,6 +174,5 @@ __attribute__((constructor)) static void resolve_linker_symbols() {
if (!android_get_exported_namespace)
return;
// Lib is now safe to use
lib_loaded = true;
}
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
@@ -9,7 +10,6 @@ extern "C" {
#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,
@@ -19,14 +19,8 @@ enum {
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,
@@ -41,39 +35,19 @@ struct android_namespace_t *android_create_namespace_escape(const char *name,
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
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <initializer_list>
#include <cstdint>
#include <errno.h>
@@ -20,7 +21,6 @@ bool elf_soname_patch(const char *libPath, int targetFd, const char *sonamePatch
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;
@@ -29,30 +29,25 @@ bool elf_soname_patch(const char *libPath, int targetFd, const char *sonamePatch
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];
@@ -1,19 +1,13 @@
// 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
@@ -11,8 +11,3 @@ linkernsbypass_lib = static_library (
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', '.'])
-15
View File
@@ -33,18 +33,3 @@ adrenotools_lib = shared_library(
)
subdir('src/hook')
# target_include_directories(['adrenotools', 'PUBLIC', 'include'])
# target_include_directories(['adrenotools', 'PRIVATE', '.'])
# target_compile_options(['adrenotools', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_libraries(['adrenotools', 'android', 'linkernsbypass'])
# hooks_deb = subproject('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
+3 -18
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <fstream>
#include <string>
#include <cstring>
@@ -19,7 +20,6 @@ enum adrenotools_bcn_type adrenotools_get_bcn_type(uint32_t major, uint32_t mino
return ADRENOTOOLS_BCN_PATCH;
}
// Searches /proc/self/maps for the first free page after the given address
static void *find_free_page(uintptr_t address) {
std::ifstream procMaps("/proc/self/maps");
@@ -45,62 +45,48 @@ static void *align_ptr(void *ptr) {
bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn) {
union Branch {
struct {
int32_t offset : 26; //!< 26-bit branch offset
uint8_t sig : 6; //!< 6-bit signature (0x25 for linked, 0x5 for jump)
int32_t offset : 26;
uint8_t sig : 6;
};
uint32_t raw{};
};
static_assert(sizeof(Branch) == 4, "Branch size is invalid");
// Find the nearest unmapped page where we can place patch code
void *patchPage{find_free_page(reinterpret_cast<uintptr_t>(vkGetPhysicalDeviceFormatPropertiesFn))};
if (!patchPage)
return false;
// Map patch region
void *ptr{mmap(patchPage, getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0)};
if (ptr != patchPage)
return false;
// Allow reading from the blob's .text section since some devices enable ---X
// Protect two pages just in case we happen to land on a page boundary
if (mprotect(align_ptr(vkGetPhysicalDeviceFormatPropertiesFn), getpagesize() * 2, PROT_WRITE | PROT_READ | PROT_EXEC))
return false;
// First branch in this function is targeted at the function we want to patch
Branch *blInst{reinterpret_cast<Branch *>(vkGetPhysicalDeviceFormatPropertiesFn)};
constexpr uint8_t BranchLinkSignature{0x25};
// Search for first instruction with the BL signature
while (blInst->sig != BranchLinkSignature)
blInst++;
// Internal QGL format conversion function that we need to patch
uint32_t *convFormatFn{reinterpret_cast<uint32_t *>(blInst) + blInst->offset};
// See mprotect call above
// This time we also set PROT_WRITE so we can write our patch to the page
if (mprotect(align_ptr(convFormatFn), getpagesize() * 2, PROT_WRITE | PROT_READ | PROT_EXEC))
return false;
// This would normally set the default result to 0 (error) in the format not found case
constexpr uint32_t ClearResultSignature{0x2a1f03e0};
// We replace it with a branch to our own extended if statement which adds in the extra things for BCn
uint32_t *clearResultPtr{convFormatFn};
while (*clearResultPtr != ClearResultSignature)
clearResultPtr++;
// Ensure we don't write out of bounds
if (PatchRawData_size > getpagesize())
return false;
// Copy the patch function to our mapped page
memcpy(patchPage, PatchRawData, PatchRawData_size);
// Fixup the patch code so it correctly returns back to the driver after running
constexpr uint32_t PatchReturnFixupMagic{0xffffffff};
constexpr uint8_t BranchSignature{0x5};
@@ -130,6 +116,5 @@ bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn) {
asm volatile("ISB");
// Done!
return true;
}
@@ -80,4 +80,4 @@
.L18:
mov w0, 0
.L3:
.word 0xffffffff // Branch fixup
.word 0xffffffff
+3 -13
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#include <string>
#include <string_view>
#include <sys/stat.h>
@@ -17,17 +18,14 @@
#include <unistd.h>
void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *tmpLibDir, const char *hookLibDir, const char *customDriverDir, const char *customDriverName, const char *fileRedirectDir, void **userMappingHandle) {
// Bail out if linkernsbypass failed to load, this probably means we're on api < 28
if (!linkernsbypass_load_status()) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Could not load linkernsbypass\n");
return nullptr;
}
// Use memfd on Q+ since it's guaranteed to work, but only if tmpLibDir wasn't set
if (android_get_device_api_level() >= 29 && !tmpLibDir)
tmpLibDir = nullptr;
// Verify that params for specific features are only passed if they are enabled
if (!(featureFlags & ADRENOTOOLS_DRIVER_FILE_REDIRECT) && fileRedirectDir) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "ADRENOTOOLS_DRIVER_FILE_REDIRECT present but no file redirect folder found\n");
return nullptr;
@@ -43,7 +41,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
return nullptr;
}
// Verify that params for enabled features are correct
struct stat buf{};
if (featureFlags & ADRENOTOOLS_DRIVER_CUSTOM) {
@@ -58,7 +55,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
}
}
// Verify that params for enabled features are correct
if (featureFlags & ADRENOTOOLS_DRIVER_FILE_REDIRECT) {
if (!fileRedirectDir) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "ADRENOTOOLS_DRIVER_REDIRECT_DIR present but no folder parameter was found\n");
@@ -71,22 +67,18 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
}
}
// Create a namespace that can isolate our hook from the classloader namespace
auto hookNs{android_create_namespace("adrenotools-libvulkan", hookLibDir, nullptr, ANDROID_NAMESPACE_TYPE_SHARED, nullptr, nullptr)};
// Link it to the default namespace so the hook can use libandroid etc
if (!linkernsbypass_link_namespace_to_default_all_libs(hookNs)) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Failed to hook our namespace to the default one\n");
return nullptr;
}
// Preload the hook implementation, otherwise we get a weird issue where despite being in NEEDED of the hook lib the hook's symbols will overwrite ours and cause an infinite loop
auto hookImpl{linkernsbypass_namespace_dlopen("libhook_impl.so", RTLD_NOW, hookNs)};
if (!hookImpl) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Couldn't preload the hook implementation\n");
return nullptr;
}
// Pass parameters to the hook implementation
auto initHookParam{reinterpret_cast<void (*)(const void *)>(dlsym(hookImpl, "init_hook_param"))};
if (!initHookParam) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Couldn't init hook params\n");
@@ -95,7 +87,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
auto importMapping{[&]() -> adrenotools_gpu_mapping * {
if (featureFlags & ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT) {
// This will be leaked, but it's not a big deal since it's only a few bytes
adrenotools_gpu_mapping *mapping{new adrenotools_gpu_mapping{}};
*userMappingHandle = mapping;
return mapping;
@@ -107,7 +98,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *
initHookParam(new HookImplParams(featureFlags, tmpLibDir, hookLibDir, customDriverDir, customDriverName, fileRedirectDir, importMapping));
// Load the libvulkan hook into the isolated namespace
if (!linkernsbypass_namespace_dlopen("libmain_hook.so", RTLD_GLOBAL, hookNs)) {
__android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Failed to load libvulkan into the isolated namespace\n");
return nullptr;
@@ -150,7 +140,7 @@ bool adrenotools_import_user_mem(void *handle, void *hostPtr, uint64_t size) {
importMapping->host_ptr = hostPtr;
importMapping->gpu_addr = info.gpuaddr;
importMapping->size = size;
importMapping->flags = 0xc2600; //!< Unknown flags, but they are required for the mapping to work
importMapping->flags = 0xc2600;
close(kgslFd);
return true;
@@ -189,7 +179,7 @@ bool adrenotools_mem_gpu_allocate(void *handle, uint64_t *size) {
mapping->host_ptr = nullptr;
mapping->gpu_addr = info.gpuaddr;
mapping->size = *size;
mapping->flags = 0xc2600; //!< Unknown flags, but they are required for the mapping to work
mapping->flags = 0xc2600;
close(kgslFd);
return true;
@@ -15,7 +15,7 @@
#define TAG "hook_impl"
#define LOGI(fmt, ...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##__VA_ARGS__)
const HookImplParams *hook_params; //!< Bunch of info needed to load/patch the driver
const HookImplParams *hook_params;
int (*gsl_memory_alloc_pure_sym)(uint32_t, uint32_t, void *);
int (*gsl_memory_alloc_pure_64_sym)(uint64_t, uint32_t, void *);
int (*gsl_memory_free_pure_sym)(void *);
@@ -43,7 +43,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
LOGI("hook_android_dlopen_ext: filename: %s", filename);
// Ignore non-vulkan libraries
if (!strstr(filename, "vulkan."))
return android_dlopen_ext(filename, flags, extinfo);
@@ -52,7 +51,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
return fallback();
}
// customDriverDir will be empty if ADRENOTOOLS_DRIVER_CUSTOM isn't set therefore it's fine to have either way
auto driverNs{android_create_namespace(filename, hook_params->customDriverDir.c_str(),
hook_params->hookLibDir.c_str(), ANDROID_NAMESPACE_TYPE_SHARED,
nullptr, extinfo->library_namespace)};
@@ -61,16 +59,12 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
return fallback();
}
// We depend on libandroid which is unlikely to be in the supplied driver namespace so we have to link it over
android_link_namespaces(driverNs, nullptr, "libandroid.so");
// Preload ourself, a new instance will be created since we have different linker ancestory
// If we don't preload we get a weird issue where despite being in NEEDED of the hook lib the hook's symbols will overwrite ours and cause an infinite loop
auto hookImpl{linkernsbypass_namespace_dlopen("libhook_impl.so", RTLD_NOW, driverNs)};
if (!hookImpl)
return nullptr;
// Pass parameters to ourself
auto initHookParam{reinterpret_cast<void (*)(const void *)>(dlsym(hookImpl, "init_hook_param"))};
if (!initHookParam)
return nullptr;
@@ -86,7 +80,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
LOGI("hook_android_dlopen_ext: applied libfile_redirect_hook");
}
// Use our new namespace to load the vulkan driver
auto newExtinfo{*extinfo};
newExtinfo.library_namespace = driverNs;
@@ -122,8 +115,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
LOGI("hook_android_dlopen_ext: hook failed: failed to apply libgsl_alloc_hook!");
}
// TODO: If there is already an instance of a Vulkan driver loaded hooks won't be applied, this will only be the case for skiavk generally
// To fix this we would need to search /proc/self/maps for the file to a loaded instance of the library in order to read it to patch the soname and load it uniquely
if (hook_params->featureFlags & ADRENOTOOLS_DRIVER_CUSTOM) {
LOGI("hook_android_dlopen_ext: loading custom driver: %s%s", hook_params->customDriverDir.c_str(), hook_params->customDriverName.c_str());
void *handle{android_dlopen_ext(hook_params->customDriverName.c_str(), flags, &newExtinfo)};
@@ -142,7 +133,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char
__attribute__((visibility("default"))) void *hook_android_load_sphal_library(const char *filename, int flags) {
LOGI("hook_android_load_sphal_library: filename: %s", filename);
// https://android.googlesource.com/platform/system/core/+/master/libvndksupport/linker.cpp
for (const char *name : {"sphal", "vendor", "default"}) {
if (auto vendorNs{android_get_exported_namespace(name)}) {
android_dlextinfo dlextinfo{
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#ifdef __cplusplus
@@ -1,15 +1,12 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright © 2021 Billy Laws
#pragma once
#include <string>
#include <adrenotools/priv.h>
/**
* @brief Holds the parameters needed for all hooks
* @note See comments for adrenotools_open_libvulkan as a reference for member variables
*/
struct HookImplParams {
int featureFlags;
std::string tmpLibDir;
File diff suppressed because it is too large. Load diff
@@ -1,32 +1,12 @@
hook_impl_files = files(['hook_impl.cpp', 'hook_impl.h', 'hook_impl_params.h'])
hook_impl_inc = include_directories('../../include')
# hook_impl_lib = shared_library('hook_impl', 'hook_impl.cpp', 'hook_impl.h', 'hook_impl_params.h')
# target_compile_options(['hook_impl', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_libraries(['hook_impl', 'linkernsbypass', 'log'])
# target_include_directories(['hook_impl', 'PRIVATE', '../../include'])
# set_target_properties(['hook_impl', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden'])
main_hook_files = files(['main_hook.c'])
# main_hook_lib = shared_library('main_hook', 'main_hook.c')
# target_compile_options(['main_hook', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_options(['main_hook', 'PRIVATE', '-z', 'global'])
# target_link_libraries(['main_hook', 'hook_impl'])
# set_target_properties(['main_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden'])
file_redirect_hook_files = files(['file_redirect_hook.c'])
# file_redirect_hook_lib = shared_library('file_redirect_hook', 'file_redirect_hook.c')
# target_compile_options(['file_redirect_hook', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_options(['file_redirect_hook', 'PRIVATE', '-z', 'global'])
# target_link_libraries(['file_redirect_hook', 'hook_impl'])
# set_target_properties(['file_redirect_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden'])
gsl_alloc_hook_files = files(['gsl_alloc_hook.c'])
# gsl_alloc_hook_lib = shared_library('gsl_alloc_hook', 'gsl_alloc_hook.c')
# target_compile_options(['gsl_alloc_hook', 'PRIVATE', '-Wall', '-Wextra'])
# target_link_options(['gsl_alloc_hook', 'PRIVATE', '-z', 'global'])
# target_link_libraries(['gsl_alloc_hook', 'hook_impl'])
# set_target_properties(['gsl_alloc_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden'])
hook_impl_lib = shared_library(
'hook_impl',
@@ -5,7 +5,6 @@
thread_local int satisfy_driver_emutls;
// Check libllvm-glnext.so in older drivers for a full list of these, newer ones are slightly more awkward due to the split
enum class OptionType : uint32_t {
ArgsStringList = 1,
QcArgsString = 4,
@@ -21,7 +20,6 @@ struct StringListVal {
struct Option
{
OptionType type;
// PAD
union {
bool boolVal;
int intVal;
@@ -49,7 +47,6 @@ int CompileHook(void *state, OptionsSet *opts) {
Option replacementOpts[ReplacementOptsNum];
uint32_t patchedOptNum{};
// Insert your desired args here - results will be written to stderr
const char *argv[] = {"-help", "-help-hidden", "", NULL };
StringListVal listVal{
.len = 3,
@@ -83,7 +80,6 @@ int CompileHook(void *state, OptionsSet *opts) {
struct LlvmInterface {
uint32_t unk0[3];
// PAD
void *makeContext;
void *delContext;
decltype(CompileHook) *compile;
@@ -30,53 +30,31 @@
#ifndef __QTIGRALLOCDEFS_H__
#define __QTIGRALLOCDEFS_H__
// From gralloc_priv.h
/* Gralloc usage bits indicating the type of allocation that should be used */
/* Refer to BufferUsage in hardware/interfaces/graphics/common/<ver>/types.hal */
/* The bits below are in officially defined vendor space
* i.e bits 28-31 and 48-63*/
/* Non linear, Universal Bandwidth Compression */
#define GRALLOC_USAGE_PRIVATE_ALLOC_UBWC (UINT32_C(1) << 28)
/* Set this for allocating uncached memory (using O_DSYNC),
* cannot be used with noncontiguous heaps */
#define GRALLOC_USAGE_PRIVATE_UNCACHED (UINT32_C(1) << 29)
/* This flag is used to indicate 10 bit format.
* When both GRALLOC_USAGE_PRIVATE_ALLOC_UBWC & GRALLOC_USAGE_PRIVATE_10BIT
* are set then it will indicate UBWC_TP10 format.
* When only GRALLOC_USAGE_PRIVATE_10BIT is set it will indicate linear P010 format.
*/
#define GRALLOC_USAGE_PRIVATE_10BIT (UINT32_C(1) << 30)
/* This flag is used for SECURE display usecase */
#define GRALLOC_USAGE_PRIVATE_SECURE_DISPLAY (UINT32_C(1) << 31)
/* This flag is used to indicate video NV21 format */
#define GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER 1ULL << 48
/* unused legacy flags */
#define GRALLOC_USAGE_PRIVATE_MM_HEAP 0
#define GRALLOC_USAGE_PRIVATE_IOMMU_HEAP 0
/* This flag is set for WFD usecase */
#define GRALLOC_USAGE_PRIVATE_WFD 1ULL << 51
/* This flag is set for HEIF usecase */
#define GRALLOC_USAGE_PRIVATE_HEIF (UINT32_C(1) << 27)
/* TODO(user): Remove when clients stop referencing this flag */
#define GRALLOC_USAGE_PRIVATE_10BIT_TP 0
/* This flag indicates PI format is being used */
#define GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI 1ULL << 49
/* This flag is set while CDSP accesses the buffer */
#define GRALLOC_USAGE_PRIVATE_CDSP 1ULL << 50
// OEM specific HAL formats
#define HAL_PIXEL_FORMAT_RGBA_5551 6
#define HAL_PIXEL_FORMAT_RGBA_4444 7
#define HAL_PIXEL_FORMAT_NV12_ENCODEABLE 0x102
@@ -98,7 +76,6 @@
#define HAL_PIXEL_FORMAT_RAW8 0x123
#define HAL_PIXEL_FORMAT_NV12_HEIF 0x116
// 10 bit
#define HAL_PIXEL_FORMAT_ARGB_2101010 0x117
#define HAL_PIXEL_FORMAT_RGBX_1010102 0x118
#define HAL_PIXEL_FORMAT_XRGB_2101010 0x119
@@ -106,7 +83,7 @@
#define HAL_PIXEL_FORMAT_ABGR_2101010 0x11B
#define HAL_PIXEL_FORMAT_BGRX_1010102 0x11C
#define HAL_PIXEL_FORMAT_XBGR_2101010 0x11D
#define HAL_PIXEL_FORMAT_YCbCr_420_P010 0x36 // HAL_PIXEL_FORMAT_YCBCR_P010
#define HAL_PIXEL_FORMAT_YCbCr_420_P010 0x36
#define HAL_PIXEL_FORMAT_YCbCr_420_P010_UBWC 0x124
#define HAL_PIXEL_FORMAT_YCbCr_420_P010_VENUS 0x7FA30C0A
@@ -115,7 +92,6 @@
#define HAL_PIXEL_FORMAT_INTERLACE 0x180
// Camera utils format
#define HAL_PIXEL_FORMAT_NV12_LINEAR_FLEX 0x125
#define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX 0x126
#define HAL_PIXEL_FORMAT_MULTIPLANAR_FLEX 0x127
@@ -123,22 +99,16 @@
#define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX_4_BATCH 0x129
#define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX_8_BATCH 0x130
// FSC Formats
#define HAL_PIXEL_FORMAT_RGB888_UBWC_FSC 0x131
#define HAL_PIXEL_FORMAT_RGB101010_UBWC_FSC 0x132
// v4l2_fourcc('Y', 'U', 'Y', 'L'). 24 bpp YUYV 4:2:2 10 bit per component
#define HAL_PIXEL_FORMAT_YCbCr_422_I_10BIT 0x4C595559
// v4l2_fourcc('Y', 'B', 'W', 'C'). 10 bit per component. This compressed
// format reduces the memory access bandwidth
#define HAL_PIXEL_FORMAT_YCbCr_422_I_10BIT_COMPRESSED 0x43574259
// UBWC aligned Venus format
#define HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC 0x7FA30C06
#define HAL_PIXEL_FORMAT_YCbCr_420_TP10_UBWC 0x7FA30C09
// Khronos ASTC formats
#define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_4x4_KHR 0x93B0
#define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_5x4_KHR 0x93B1
#define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_5x5_KHR 0x93B2
@@ -170,4 +140,4 @@
enum { BUFFER_TYPE_UI = 0, BUFFER_TYPE_VIDEO };
#endif //__QTIGRALLOCDEFS_H__
#endif
@@ -26,7 +26,6 @@
using namespace android::hardware;
using namespace android;
/* Simplified version of hidl::base::V1_0::IBase */
struct IBase : virtual public RefBase {
virtual bool isRemote() const;
@@ -80,8 +80,6 @@ AdrenoMemInfo::AdrenoMemInfo() {
ALOGE(" Failed to load libadreno_utils.so");
}
// Check if the overriding property debug.gralloc.gfx_ubwc_disable
// that disables UBWC allocations for the graphics stack is set
char property[PROPERTY_VALUE_MAX];
property_get(DISABLE_UBWC_PROP, property, "0");
if (!(strncmp(property, "1", PROPERTY_VALUE_MAX)) ||
@@ -123,16 +121,13 @@ void AdrenoMemInfo::AlignUnCompressedRGB(int width, int height, int format, int
break;
}
int raster_mode = 0; // Adreno unknown raster mode.
int padding_threshold = 512; // Threshold for padding surfaces.
// the function below computes aligned width and aligned height
// based on linear or macro tile mode selected.
int raster_mode = 0;
int padding_threshold = 512;
if (LINK_adreno_compute_fmt_aligned_width_and_height) {
// We call into adreno_utils only for RGB formats. So plane_id is 0 and
// num_samples is 1 always. We may have to add uitility function to
// find out these if there is a need to call this API for YUV formats.
LINK_adreno_compute_fmt_aligned_width_and_height(
width, height, 0 /*plane_id*/, GetGpuPixelFormat(format), 1 /*num_samples*/, tile_enabled,
width, height, 0 , GetGpuPixelFormat(format), 1 , tile_enabled,
raster_mode, padding_threshold, reinterpret_cast<int*>(aligned_w),
reinterpret_cast<int*>(aligned_h));
} else if (LINK_adreno_compute_aligned_width_and_height) {
@@ -140,7 +135,7 @@ void AdrenoMemInfo::AlignUnCompressedRGB(int width, int height, int format, int
width, height, bpp, tile_enabled, raster_mode, padding_threshold,
reinterpret_cast<int*>(aligned_w), reinterpret_cast<int*>(aligned_h));
} else if (LINK_adreno_compute_padding) {
int surface_tile_height = 1; // Linear surface
int surface_tile_height = 1;
*aligned_w = UINT(LINK_adreno_compute_padding(width, bpp, surface_tile_height, raster_mode,
padding_threshold));
ALOGW("%s: Warning!! Old GFX API is used to calculate stride", __FUNCTION__);
@@ -157,9 +152,9 @@ void AdrenoMemInfo::AlignCompressedRGB(int width, int height, int format, unsign
unsigned int* aligned_h) {
if (LINK_adreno_compute_compressedfmt_aligned_width_and_height) {
int bytesPerPixel = 0;
int raster_mode = 0; // Adreno unknown raster mode.
int padding_threshold = 512; // Threshold for padding
// surfaces.
int raster_mode = 0;
int padding_threshold = 512;
LINK_adreno_compute_compressedfmt_aligned_width_and_height(
width, height, format, 0, raster_mode, padding_threshold, reinterpret_cast<int*>(aligned_w),
@@ -341,4 +336,4 @@ bool AdrenoMemInfo::IsPISupportedByGPU(int format, uint64_t usage) {
return false;
}
} // namespace gralloc
}
@@ -35,11 +35,10 @@
namespace gralloc {
// Adreno Pixel Formats
typedef enum {
ADRENO_PIXELFORMAT_UNKNOWN = 0,
ADRENO_PIXELFORMAT_R16G16B16A16_FLOAT = 10,
ADRENO_PIXELFORMAT_R10G10B10A2_UNORM = 24, // Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES
ADRENO_PIXELFORMAT_R10G10B10A2_UNORM = 24,
ADRENO_PIXELFORMAT_R8G8B8A8 = 28,
ADRENO_PIXELFORMAT_R8G8B8A8_SRGB = 29,
ADRENO_PIXELFORMAT_D32_FLOAT = 40,
@@ -55,140 +54,87 @@ typedef enum {
ADRENO_PIXELFORMAT_P010 = 104,
ADRENO_PIXELFORMAT_YUY2 = 107,
ADRENO_PIXELFORMAT_B4G4R4A4 = 115,
ADRENO_PIXELFORMAT_NV12_EXT = 506, // NV12 with non-std alignment and offsets
ADRENO_PIXELFORMAT_R8G8B8X8 = 507, // GL_RGB8 (Internal)
ADRENO_PIXELFORMAT_R8G8B8 = 508, // GL_RGB8
ADRENO_PIXELFORMAT_A1B5G5R5 = 519, // GL_RGB5_A1
ADRENO_PIXELFORMAT_R8G8B8X8_SRGB = 520, // GL_SRGB8
ADRENO_PIXELFORMAT_R8G8B8_SRGB = 521, // GL_SRGB8
ADRENO_PIXELFORMAT_NV12_EXT = 506,
ADRENO_PIXELFORMAT_R8G8B8X8 = 507,
ADRENO_PIXELFORMAT_R8G8B8 = 508,
ADRENO_PIXELFORMAT_A1B5G5R5 = 519,
ADRENO_PIXELFORMAT_R8G8B8X8_SRGB = 520,
ADRENO_PIXELFORMAT_R8G8B8_SRGB = 521,
ADRENO_PIXELFORMAT_A2B10G10R10_UNORM = 532,
// Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES
ADRENO_PIXELFORMAT_R10G10B10X2_UNORM = 537,
ADRENO_PIXELFORMAT_D24_UNORM_X8_UINT = 548,
ADRENO_PIXELFORMAT_D24_UNORM = 549,
ADRENO_PIXELFORMAT_D32_FLOAT_X24S8_UINT = 551,
ADRENO_PIXELFORMAT_S8_UINT = 552,
ADRENO_PIXELFORMAT_ASTC_4X4 = 568, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_5X4 = 569, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_5X5 = 570, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_6X5 = 571, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_6X6 = 572, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X5 = 573, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X6 = 574, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X8 = 575, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X5 = 576, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X6 = 577, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X8 = 578, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X10 = 579, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_12X10 = 580, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_12X12 = 581, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_4X4_SRGB = 582, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_5X4_SRGB = 583, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_5X5_SRGB = 584, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_6X5_SRGB = 585, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_6X6_SRGB = 586, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X5_SRGB = 587, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X6_SRGB = 588, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_8X8_SRGB = 589, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X5_SRGB = 590, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X6_SRGB = 591, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X8_SRGB = 592, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_10X10_SRGB = 593, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_12X10_SRGB = 594, // ASTC Compressed
ADRENO_PIXELFORMAT_ASTC_12X12_SRGB = 595, // ASTC Compressed
// Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES
ADRENO_PIXELFORMAT_R5G6B5 = 610, // RGBA version of B5G6R5
ADRENO_PIXELFORMAT_R5G5B5A1 = 611, // RGBA version of B5G5R5A1
ADRENO_PIXELFORMAT_R4G4B4A4 = 612, // RGBA version of B4G4R4A4
ADRENO_PIXELFORMAT_UYVY = 614, // YUV 4:2:2 packed progressive (1 plane)
ADRENO_PIXELFORMAT_ASTC_4X4 = 568,
ADRENO_PIXELFORMAT_ASTC_5X4 = 569,
ADRENO_PIXELFORMAT_ASTC_5X5 = 570,
ADRENO_PIXELFORMAT_ASTC_6X5 = 571,
ADRENO_PIXELFORMAT_ASTC_6X6 = 572,
ADRENO_PIXELFORMAT_ASTC_8X5 = 573,
ADRENO_PIXELFORMAT_ASTC_8X6 = 574,
ADRENO_PIXELFORMAT_ASTC_8X8 = 575,
ADRENO_PIXELFORMAT_ASTC_10X5 = 576,
ADRENO_PIXELFORMAT_ASTC_10X6 = 577,
ADRENO_PIXELFORMAT_ASTC_10X8 = 578,
ADRENO_PIXELFORMAT_ASTC_10X10 = 579,
ADRENO_PIXELFORMAT_ASTC_12X10 = 580,
ADRENO_PIXELFORMAT_ASTC_12X12 = 581,
ADRENO_PIXELFORMAT_ASTC_4X4_SRGB = 582,
ADRENO_PIXELFORMAT_ASTC_5X4_SRGB = 583,
ADRENO_PIXELFORMAT_ASTC_5X5_SRGB = 584,
ADRENO_PIXELFORMAT_ASTC_6X5_SRGB = 585,
ADRENO_PIXELFORMAT_ASTC_6X6_SRGB = 586,
ADRENO_PIXELFORMAT_ASTC_8X5_SRGB = 587,
ADRENO_PIXELFORMAT_ASTC_8X6_SRGB = 588,
ADRENO_PIXELFORMAT_ASTC_8X8_SRGB = 589,
ADRENO_PIXELFORMAT_ASTC_10X5_SRGB = 590,
ADRENO_PIXELFORMAT_ASTC_10X6_SRGB = 591,
ADRENO_PIXELFORMAT_ASTC_10X8_SRGB = 592,
ADRENO_PIXELFORMAT_ASTC_10X10_SRGB = 593,
ADRENO_PIXELFORMAT_ASTC_12X10_SRGB = 594,
ADRENO_PIXELFORMAT_ASTC_12X12_SRGB = 595,
ADRENO_PIXELFORMAT_R5G6B5 = 610,
ADRENO_PIXELFORMAT_R5G5B5A1 = 611,
ADRENO_PIXELFORMAT_R4G4B4A4 = 612,
ADRENO_PIXELFORMAT_UYVY = 614,
ADRENO_PIXELFORMAT_NV21 = 619,
ADRENO_PIXELFORMAT_Y8U8V8A8 = 620, // YUV 4:4:4 packed (1 plane)
ADRENO_PIXELFORMAT_Y8 = 625, // Single 8-bit luma only channel YUV format
ADRENO_PIXELFORMAT_TP10 = 654, // YUV 4:2:0 planar 10 bits/comp (2 planes)
ADRENO_PIXELFORMAT_Y8U8V8A8 = 620,
ADRENO_PIXELFORMAT_Y8 = 625,
ADRENO_PIXELFORMAT_TP10 = 654,
} ADRENOPIXELFORMAT;
typedef enum {
SURFACE_TILE_MODE_DISABLE = 0x0, // used for linear surface
SURFACE_TILE_MODE_ENABLE = 0x1 // used for tiled surface
SURFACE_TILE_MODE_DISABLE = 0x0,
SURFACE_TILE_MODE_ENABLE = 0x1
} surface_tile_mode_t;
class AdrenoMemInfo {
public:
/*
* Function to compute aligned width and aligned height based on
* width, height, format and usage flags.
*
* @return aligned width, aligned height
*/
void GetAlignedWidthAndHeight(int width, int height, int format, int usage,
unsigned int* aligned_w, unsigned int* aligned_h, bool ubwc_enabled,
bool tile_enabled);
/*
* Function to compute the adreno aligned width and aligned height
* based on the width and format.
*
* @return aligned width, aligned height
*/
void AlignUnCompressedRGB(int width, int height, int format, int tileEnabled,
unsigned int* aligned_w, unsigned int* aligned_h);
/*
* Function to compute the adreno aligned width and aligned height
* based on the width and format.
*
* @return aligned width, aligned height
*/
void AlignCompressedRGB(int width, int height, int format, unsigned int* aligned_w,
unsigned int* aligned_h);
/*
* Function to compute the pixel alignment requirement.
*
* @return alignment
*/
uint32_t GetGpuPixelAlignment();
/*
* Function to query whether GPU supports UBWC for given HAL format
* @return > 0 : supported
* 0 : not supported
*/
bool IsUBWCSupportedByGPU(int format);
/*
* Function to check if GPU supports PI or not
*/
bool IsPISupportedByGPU(int format, uint64_t usage);
/*
* Function to get the corresponding Adreno format for given HAL format
*/
ADRENOPIXELFORMAT GetGpuPixelFormat(int hal_format);
/*
* Function to get graphics metadata blob size
* @return graphics metadata size
*/
uint32_t AdrenoGetMetadataBlobSize();
/*
* Function to populate the graphics metadata blob
* @return 1 : Successful
* 2 : Unsuccessful
*/
int AdrenoInitMemoryLayout(void* metadata_blob, int width, int height, int depth, int format,
int num_samples, int isUBWC, uint64_t usage, uint32_t num_planes);
/*
* Function to get buffer size for based on graphcis metadata
* @return buffer size
*/
uint32_t AdrenoGetAlignedGpuBufferSize(void* metadata_blob);
/*
* Function to check if adreno size calculation APIs are avaliable
* @return true : Avaliable
* false : Unavaliable
*/
bool AdrenoSizeAPIAvaliable();
static AdrenoMemInfo* GetInstance();
@@ -196,7 +142,6 @@ class AdrenoMemInfo {
private:
AdrenoMemInfo();
~AdrenoMemInfo();
// link(s)to adreno surface padding library.
int (*LINK_adreno_compute_padding)(int width, int bpp, int surface_tile_height,
int screen_tile_height, int padding_threshold) = NULL;
void (*LINK_adreno_compute_aligned_width_and_height)(int width, int height, int bpp,
@@ -229,6 +174,6 @@ class AdrenoMemInfo {
static AdrenoMemInfo* s_instance;
};
} // namespace gralloc
}
#endif // __GR_ADRENO_INFO_H__
#endif
@@ -57,29 +57,27 @@ struct private_handle_t {
PRIV_FLAGS_CAMERA_READ = 0x00040000,
PRIV_FLAGS_HW_COMPOSER = 0x00080000,
PRIV_FLAGS_HW_TEXTURE = 0x00100000,
PRIV_FLAGS_ITU_R_601 = 0x00200000, // Unused from display
PRIV_FLAGS_ITU_R_601_FR = 0x00400000, // Unused from display
PRIV_FLAGS_ITU_R_709 = 0x00800000, // Unused from display
PRIV_FLAGS_ITU_R_601 = 0x00200000,
PRIV_FLAGS_ITU_R_601_FR = 0x00400000,
PRIV_FLAGS_ITU_R_709 = 0x00800000,
PRIV_FLAGS_SECURE_DISPLAY = 0x01000000,
PRIV_FLAGS_TILE_RENDERED = 0x02000000,
PRIV_FLAGS_CPU_RENDERED = 0x04000000,
PRIV_FLAGS_UBWC_ALIGNED = 0x08000000,
PRIV_FLAGS_DISP_CONSUMER = 0x10000000,
PRIV_FLAGS_CLIENT_ALLOCATED = 0x20000000, // Ion buffer allocated outside of gralloc
PRIV_FLAGS_UBWC_ALIGNED_PI = 0x40000000, // PI format
PRIV_FLAGS_CLIENT_ALLOCATED = 0x20000000,
PRIV_FLAGS_UBWC_ALIGNED_PI = 0x40000000,
};
// file-descriptors dup'd over IPC
int fd;
int fd_metadata;
// values sent over IPC
int magic;
int flags;
int width; // holds width of the actual buffer allocated
int height; // holds height of the actual buffer allocated
int unaligned_width; // holds width client asked to allocate
int unaligned_height; // holds height client asked to allocate
int width;
int height;
int unaligned_width;
int unaligned_height;
int format;
int buffer_type;
unsigned int layer_count;
@@ -126,7 +124,6 @@ struct private_handle_t {
numFds = kNumFds;
}
// Legacy constructor used by some clients
private_handle_t(int fd, unsigned int size, int usage, int buf_type, int format, int w, int h)
: private_handle_t(fd, -1, PRIV_FLAGS_CLIENT_ALLOCATED, w, h, 0, 0, format, buf_type, size,
static_cast<uint64_t>(usage)) {}
@@ -176,7 +173,6 @@ struct private_handle_t {
unsigned int GetLayerCount() const { return layer_count; }
int GetStride() const {
// In handle we currently store aligned width after allocation.
return width;
}
@@ -187,4 +183,4 @@ struct private_handle_t {
};
#pragma pack(pop)
#endif // __GR_PRIV_HANDLE_H__
#endif
@@ -162,7 +162,6 @@ bool CpuCanRead(uint64_t usage) {
bool CpuCanWrite(uint64_t usage) {
if (usage & BufferUsage::CPU_WRITE_MASK) {
// Application intends to use CPU for rendering
return true;
}
@@ -177,7 +176,6 @@ uint32_t GetDataAlignment(int format, uint64_t usage) {
if (usage & BufferUsage::PROTECTED) {
if ((usage & BufferUsage::CAMERA_OUTPUT) || (usage & GRALLOC_USAGE_PRIVATE_SECURE_DISPLAY)) {
// The alignment here reflects qsee mmu V7L/V8L requirement
align = SZ_2M;
} else {
align = SECURE_ALIGN;
@@ -202,7 +200,6 @@ bool IsGPUFlagSupported(uint64_t usage) {
return ret;
}
// Returns the final buffer size meant to be allocated with ion
unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int alignedh) {
unsigned int size = 0;
int format = info.format;
@@ -223,7 +220,6 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int
} else if (IsCompressedRGBFormat(format)) {
size = alignedw * alignedh * ASTC_BLOCK_SIZE;
} else {
// Below switch should be for only YUV/custom formats
switch (format) {
case HAL_PIXEL_FORMAT_RAW16:
case HAL_PIXEL_FORMAT_Y16:
@@ -237,15 +233,15 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int
case HAL_PIXEL_FORMAT_Y8:
size = alignedw * alignedh * 1;
break;
// adreno formats
case HAL_PIXEL_FORMAT_YCrCb_420_SP_ADRENO: // NV21
case HAL_PIXEL_FORMAT_YCrCb_420_SP_ADRENO:
size = ALIGN(alignedw * alignedh, SIZE_4K);
size += (unsigned int)ALIGN(2 * ALIGN(width / 2, 32) * ALIGN(height / 2, 32), SIZE_4K);
break;
case HAL_PIXEL_FORMAT_YCbCr_420_SP_TILED: // NV12
// The chroma plane is subsampled,
// but the pitch in bytes is unchanged
// The GPU needs 4K alignment, but the video decoder needs 8K
case HAL_PIXEL_FORMAT_YCbCr_420_SP_TILED:
size = ALIGN(alignedw * alignedh, SIZE_8K);
size += ALIGN(alignedw * (unsigned int)ALIGN(height / 2, 32), SIZE_8K);
break;
@@ -296,7 +292,6 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int
return size;
}
// Explicitly defined UBWC formats
bool IsUBwcFormat(int format) {
switch (format) {
case HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC:
@@ -309,7 +304,6 @@ bool IsUBwcFormat(int format) {
}
bool IsUBwcSupported(int format) {
// Existing HAL formats with UBWC support
switch (format) {
case HAL_PIXEL_FORMAT_BGR_565:
case HAL_PIXEL_FORMAT_RGBA_8888:
@@ -336,7 +330,6 @@ bool IsUBwcPISupported(int format, uint64_t usage) {
return false;
}
// As of now only two formats
switch (format) {
case HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC:
case HAL_PIXEL_FORMAT_YCbCr_420_TP10_UBWC: {
@@ -354,27 +347,21 @@ bool IsUBwcPISupported(int format, uint64_t usage) {
}
bool IsUBwcEnabled(int format, uint64_t usage) {
// Allow UBWC, if client is using an explicitly defined UBWC pixel format.
if (IsUBwcFormat(format)) {
return true;
}
// Allow UBWC, if an OpenGL client sets UBWC usage flag and GPU plus MDP
// support the format. OR if a non-OpenGL client like Rotator, sets UBWC
// usage flag and MDP supports the format.
if (((usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC) ||
(usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI) ||
(usage & BufferUsage::COMPOSER_CLIENT_TARGET)) &&
IsUBwcSupported(format)) {
bool enable = true;
// Query GPU for UBWC only if buffer is intended to be used by GPU.
if ((usage & BufferUsage::GPU_TEXTURE) || (usage & BufferUsage::GPU_RENDER_TARGET)) {
if (AdrenoMemInfo::GetInstance()) {
enable = AdrenoMemInfo::GetInstance()->IsUBWCSupportedByGPU(format);
}
}
// Allow UBWC, only if CPU usage flags are not set
if (enable && !(CpuCanAccess(usage))) {
return true;
}
@@ -418,13 +405,10 @@ unsigned int GetRgbUBwcMetaBufferSize(int width, int height, uint32_t bpp) {
return size;
}
// Align meta buffer height to 16 blocks
meta_height = ALIGN(((height + block_height - 1) / block_height), 16);
// Align meta buffer width to 64 blocks
meta_width = ALIGN(((width + block_width - 1) / block_width), 64);
// Align meta buffer size to 4K
size = (unsigned int)ALIGN((meta_width * meta_height), 4096);
return size;
@@ -500,12 +484,10 @@ bool UseUncached(int format, uint64_t usage) {
return true;
}
// CPU read rarely
if ((usage & BufferUsage::CPU_READ_MASK) == static_cast<uint64_t>(BufferUsage::CPU_READ_RARELY)) {
return true;
}
// CPU write rarely
if ((usage & BufferUsage::CPU_WRITE_MASK) ==
static_cast<uint64_t>(BufferUsage::CPU_WRITE_RARELY)) {
return true;
@@ -576,8 +558,6 @@ uint64_t GetHandleFlags(int format, uint64_t usage) {
int GetImplDefinedFormat(uint64_t usage, int format) {
int gr_format = format;
// If input format is HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED then based on
// the usage bits, gralloc assigns a format.
if (format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED ||
format == HAL_PIXEL_FORMAT_YCbCr_420_888) {
if ((usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC || usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI) &&
@@ -585,33 +565,30 @@ int GetImplDefinedFormat(uint64_t usage, int format) {
gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC;
} else if (usage & BufferUsage::VIDEO_ENCODER) {
if (usage & GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER) {
gr_format = HAL_PIXEL_FORMAT_NV21_ENCODEABLE; // NV21
gr_format = HAL_PIXEL_FORMAT_NV21_ENCODEABLE;
} else if (usage & GRALLOC_USAGE_PRIVATE_HEIF) {
gr_format = HAL_PIXEL_FORMAT_NV12_HEIF;
} else if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) {
gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS;
} else {
gr_format = HAL_PIXEL_FORMAT_NV12_ENCODEABLE; // NV12
gr_format = HAL_PIXEL_FORMAT_NV12_ENCODEABLE;
}
} else if (usage & BufferUsage::CAMERA_INPUT) {
if (usage & BufferUsage::CAMERA_OUTPUT) {
// Assumed ZSL if both producer and consumer camera flags set
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; // NV21
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL;
} else {
gr_format = HAL_PIXEL_FORMAT_YCrCb_420_SP; // NV21
gr_format = HAL_PIXEL_FORMAT_YCrCb_420_SP;
}
} else if (usage & BufferUsage::CAMERA_OUTPUT) {
if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) {
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; // NV21
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL;
} else {
gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS; // NV12 preview
gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS;
}
} else if (usage & BufferUsage::COMPOSER_OVERLAY) {
// XXX: If we still haven't set a format, default to RGBA8888
gr_format = HAL_PIXEL_FORMAT_RGBA_8888;
} else if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) {
// If no other usage flags are detected, default the
// flexible YUV format to NV21_ZSL
gr_format = HAL_PIXEL_FORMAT_NV21_ZSL;
}
}
@@ -637,7 +614,7 @@ bool HasAlphaComponent(int32_t format) {
}
void GetRGBPlaneInfo(const BufferInfo& info, int32_t format, int32_t width, int32_t height,
int32_t /* flags */, int* plane_count, PlaneLayoutInfo* plane_info) {
int32_t , int* plane_count, PlaneLayoutInfo* plane_info) {
uint64_t usage = info.usage;
*plane_count = 1;
uint32_t bpp = 0;
@@ -659,4 +636,4 @@ void GetRGBPlaneInfo(const BufferInfo& info, int32_t format, int32_t width, int3
plane_info->scanlines = height;
}
} // namespace gralloc
}
@@ -42,7 +42,7 @@
#ifdef SLAVE_SIDE_CP
#define SECURE_ALIGN SZ_1M
#else // MASTER_SIDE_CP
#else
#define SECURE_ALIGN SZ_4K
#endif
@@ -68,59 +68,38 @@ inline Type1 ALIGN(Type1 x, Type2 align) {
}
enum PlaneComponent {
/* luma */
PLANE_COMPONENT_Y = 1 << 0,
/* chroma blue */
PLANE_COMPONENT_Cb = 1 << 1,
/* chroma red */
PLANE_COMPONENT_Cr = 1 << 2,
/* red */
PLANE_COMPONENT_R = 1 << 10,
/* green */
PLANE_COMPONENT_G = 1 << 11,
/* blue */
PLANE_COMPONENT_B = 1 << 12,
/* alpha */
PLANE_COMPONENT_A = 1 << 20,
/* raw data plane */
PLANE_COMPONENT_RAW = 1 << 30,
/* meta information plane */
PLANE_COMPONENT_META = 1 << 31,
};
struct PlaneLayoutInfo {
/** Components represented the type of plane. */
PlaneComponent component;
/** horizontal subsampling. Must be a positive power of 2. */
uint32_t h_subsampling;
/** vertical subsampling. Must be a positive power of 2. */
uint32_t v_subsampling;
/** offset to the first byte of the top-left pixel of the plane
* and it is calculated from the start of the buffer.
* Add base of the handle with offset to get the first byte of the plane.
*/
uint32_t offset;
/** step is the distance in bytes from one pixel value to the next. */
int32_t step;
/** stride of the plane in pixels */
int32_t stride;
/** stride of the plane in in bytes */
int32_t stride_bytes;
/** plane height or vertical stride */
int32_t scanlines;
/** size of the plane in bytes */
uint32_t size;
};
@@ -151,6 +130,6 @@ int GetImplDefinedFormat(uint64_t usage, int format);
int GetCustomFormatFlags(int format, uint64_t usage, int* custom_format, uint64_t* priv_flags);
bool IsGPUFlagSupported(uint64_t usage);
bool HasAlphaComponent(int32_t format);
} // namespace gralloc
}
#endif // __GR_UTILS_H__
#endif
@@ -26,7 +26,6 @@
#include "QtiGrallocDefs.h"
#include "gr_priv_handle.h"
//#pragma message "Warning: gralloc_priv.h is deprecated"
#define GRALLOC_PROP_PREFIX "vendor.gralloc."
#define GRALLOC_PROP(prop_name) GRALLOC_PROP_PREFIX prop_name
@@ -42,8 +41,6 @@ inline int roundUpToPageSize(int x) {
return (x + (getpagesize() - 1)) & ~(getpagesize() - 1);
}
/* Legacy gralloc1 definitions */
/* Some clients may still be using the old flags */
#define GRALLOC1_PRODUCER_USAGE_PRIVATE_ADSP_HEAP GRALLOC_USAGE_PRIVATE_ADSP_HEAP
#define GRALLOC1_PRODUCER_USAGE_PRIVATE_ALLOC_UBWC GRALLOC_USAGE_PRIVATE_ALLOC_UBWC
#define GRALLOC1_PRODUCER_USAGE_PRIVATE_UNCACHED GRALLOC_USAGE_PRIVATE_UNCACHED
@@ -54,7 +51,6 @@ inline int roundUpToPageSize(int x) {
#define GRALLOC1_CONSUMER_USAGE_PRIVATE_10BIT_TP GRALLOC_USAGE_PRIVATE_10BIT_TP
#define GRALLOC1_PRODUCER_USAGE_PRIVATE_VIDEO_NV21_ENCODER GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER
// for PERFORM API :
#define GRALLOC_MODULE_PERFORM_CREATE_HANDLE_FROM_BUFFER 1
#define GRALLOC_MODULE_PERFORM_GET_STRIDE 2
#define GRALLOC_MODULE_PERFORM_GET_CUSTOM_STRIDE_FROM_HANDLE 3
@@ -72,11 +68,9 @@ inline int roundUpToPageSize(int x) {
#define GRALLOC1_MODULE_PERFORM_GET_INTERLACE_FLAG 15
#define GRALLOC_MODULE_PERFORM_GET_GRAPHICS_METADATA 16
/* possible values for inverse gamma correction */
#define HAL_IGC_NOT_SPECIFIED 0
#define HAL_IGC_s_RGB 1
/* Color Space: Values maps to ColorSpace_t in qdMetadata.h */
#define HAL_CSC_ITU_R_601 0
#define HAL_CSC_ITU_R_601_FR 1
#define HAL_CSC_ITU_R_709 2
@@ -84,20 +78,16 @@ inline int roundUpToPageSize(int x) {
#define HAL_CSC_ITU_R_2020 4
#define HAL_CSC_ITU_R_2020_FR 5
/* possible formats for 3D content*/
enum {
HAL_NO_3D = 0x0,
HAL_3D_SIDE_BY_SIDE_L_R = 0x1,
HAL_3D_SIDE_BY_SIDE_R_L = 0x2,
HAL_3D_TOP_BOTTOM = 0x4,
HAL_3D_IN_SIDE_BY_SIDE_L_R = 0x10000, // unused legacy format
HAL_3D_IN_SIDE_BY_SIDE_L_R = 0x10000,
};
/* Flag to determine interlaced content
* Value maps to Flags presents in types.hal of QtiMapperextensions
*/
enum {
LAYOUT_INTERLACED_FLAG = 1 << 0,
};
#endif // __GRALLOC_PRIV_H__
#endif
@@ -60,43 +60,43 @@ struct IMapper : public IBase {
int32_t height __attribute__((aligned(4)));
};
static sp<IMapper> getService(const std::string& /* serviceName */, const bool /* getStub */);
static sp<IMapper> getService(const std::string& , const bool );
};
} // namespace V2_0
}
namespace V3_0 {
struct IMapper : public IBase {
static sp<IMapper> getService(const std::string& /* serviceName */, const bool /* getStub */);
static sp<IMapper> getService(const std::string& , const bool );
};
} // namespace V3_0
}
namespace V4_0 {
struct IMapper : public IBase {
static sp<IMapper> getService(const std::string& /* serviceName */, const bool /* getStub */);
static sp<IMapper> getService(const std::string& , const bool );
};
} // namespace V4_0
} // namespace android::hardware::graphics::mapper
}
}
namespace vendor::qti::hardware::display::mapper {
namespace V4_0 {
struct IQtiMapper : public IBase {
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V4_0::IMapper>& /* parent */,
bool /* emitError */);
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V4_0::IMapper>& ,
bool );
};
} // namespace V4_0
}
namespace V3_0 {
struct IQtiMapper : public IBase {
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V3_0::IMapper>& /* parent */,
bool /* emitError */);
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V3_0::IMapper>& ,
bool );
};
} // namespace V3_0
}
namespace V2_0 {
struct IQtiMapper : public IBase {
@@ -175,9 +175,9 @@ struct IQtiMapper : public IBase {
V1_0::IQtiMapperExtensions>& extensions)>;
virtual Return<void> getMapperExtensions(getMapperExtensions_cb _hidl_cb);
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V2_0::IMapper>& /* parent */,
bool /* emitError */);
static Return<sp<IQtiMapper>> castFrom(const sp<graphics::mapper::V2_0::IMapper>& ,
bool );
};
} // namespace V2_0
} // namespace vendor::qti::hardware::display::mapper
}
}
@@ -45,7 +45,7 @@ struct IQtiMapperExtensions : public IBase {
IQtiMapperExtensions(){};
};
} // namespace V1_0
}
namespace V1_1 {
using Error = V1_0::Error;
using PlaneLayout = V1_0::PlaneLayout;
@@ -64,7 +64,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, passthrough
using getInterlacedFlag_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, bool interlaced)>;
virtual ::android::hardware::Return<void> getInterlacedFlag(void* buffer,
@@ -79,7 +78,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, passthrough
using getRgbDataAddress_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, void* rgb_data)>;
virtual ::android::hardware::Return<void> getRgbDataAddress(void* buffer,
@@ -95,7 +93,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, passhthrough
using getColorSpace_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t color_space)>;
virtual ::android::hardware::Return<void> getColorSpace(void* buffer, getColorSpace_cb _hidl_cb) {
@@ -103,7 +100,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, passthrough with hidl fixups
using getYuvPlaneInfo_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error,
const ::android::hardware::hidl_vec<
@@ -130,27 +126,22 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, cast private data with no passthrough
using getFd_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t fd)>;
virtual ::android::hardware::Return<void> getFd(void* buffer, getFd_cb _hidl_cb);
// Used, cast private data with no passthrough
using getWidth_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t width)>;
virtual ::android::hardware::Return<void> getWidth(void* buffer, getWidth_cb _hidl_cb);
// Used, cast private data with no passthrough
using getHeight_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t height)>;
virtual ::android::hardware::Return<void> getHeight(void* buffer, getHeight_cb _hidl_cb);
// Used, cast private data with no passthrough
using getOffset_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t offset)>;
virtual ::android::hardware::Return<void> getOffset(void* buffer, getOffset_cb _hidl_cb);
// Used, cast private data with no passthrough
using getSize_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t size)>;
virtual ::android::hardware::Return<void> getSize(void* buffer, getSize_cb _hidl_cb);
@@ -179,12 +170,10 @@ struct IQtiMapperExtensions : public IBase {
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t usage)>;
virtual ::android::hardware::Return<void> getUsageFlags(void* buffer, getUsageFlags_cb _hidl_cb);
// Used, cast private data with no passthrough
using getFormat_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t format)>;
virtual ::android::hardware::Return<void> getFormat(void* buffer, getFormat_cb _hidl_cb);
// Used, cast private data with no passthrough
using getPrivateFlags_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, int32_t flags)>;
virtual ::android::hardware::Return<void> getPrivateFlags(void* buffer,
@@ -198,7 +187,6 @@ struct IQtiMapperExtensions : public IBase {
std::terminate();
}
// Used, PAIN
using getFormatLayout_cb = std::function<void(
::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t size,
const ::android::hardware::hidl_vec<
@@ -208,7 +196,6 @@ struct IQtiMapperExtensions : public IBase {
int32_t height,
getFormatLayout_cb _hidl_cb);
// V1.1, Used, passthrough
virtual ::android::hardware::Return<
::vendor::qti::hardware::display::mapperextensions::V1_0::Error>
getSurfaceMetadata_V1(void* buffer, void* metadata) {
@@ -217,8 +204,8 @@ struct IQtiMapperExtensions : public IBase {
}
static Return<sp<IQtiMapperExtensions>> castFrom(
android::sp<V1_0::IQtiMapperExtensions> const& /* parent */, bool /* emitError */);
android::sp<V1_0::IQtiMapperExtensions> const& , bool );
};
} // namespace V1_1
} // namespace vendor::qti::hardware::display::mapperextensions
}
}
File diff suppressed because it is too large. Load diff
@@ -12,7 +12,6 @@ namespace graphics {
namespace mapper {
namespace V2_0 {
// Forward declaration for forward reference support:
enum class Error : int32_t;
struct YCbCrLayout;
@@ -25,34 +24,8 @@ enum class Error : int32_t {
UNSUPPORTED = 7,
};
/**
* A buffer descriptor is an implementation-defined opaque data returned by
* createDescriptor. It describes the properties of a buffer and is consumed
* by the allocator.
*/
typedef ::android::hardware::hidl_vec<uint32_t> BufferDescriptor;
/**
* Structure for describing YCbCr formats for consumption by applications.
* This is used with PixelFormat::YCBCR_*_888.
*
* Buffer chroma subsampling is defined in the format.
* e.g. PixelFormat::YCBCR_420_888 has subsampling 4:2:0.
*
* Buffers must have a 8 bit depth.
*
* y, cb, and cr point to the first byte of their respective planes.
*
* Stride describes the distance in bytes from the first value of one row of
* the image to the first value of the next row. It includes the width of the
* image plus padding.
* yStride is the stride of the luma plane.
* cStride is the stride of the chroma planes.
*
* chromaStep is the distance in bytes from one chroma pixel value to the
* next. This is 2 bytes for semiplanar (because chroma values are interleaved
* and each chroma value is one byte) and 1 for planar.
*/
struct YCbCrLayout final {
void* y;
void* cb;
@@ -62,9 +35,6 @@ struct YCbCrLayout final {
uint32_t chromaStep;
};
//
// type declarations for package
//
template <typename>
static inline std::string toString(int32_t o);
@@ -107,11 +77,7 @@ constexpr int32_t& operator&=(int32_t& v,
static inline std::string toString(
const ::android::hardware::graphics::mapper::V2_0::YCbCrLayout& o);
// operator== and operator!= are not generated for YCbCrLayout
//
// type header definitions for package
//
template <>
inline std::string toString<::android::hardware::graphics::mapper::V2_0::Error>(int32_t o) {
@@ -218,17 +184,17 @@ static inline std::string toString(
return os;
}
// operator== and operator!= are not generated for YCbCrLayout
} // namespace V2_0
} // namespace mapper
} // namespace graphics
} // namespace hardware
} // namespace android
//
// global type declarations for package
//
}
}
}
}
}
namespace android {
namespace hardware {
@@ -243,8 +209,8 @@ constexpr std::array<::android::hardware::graphics::mapper::V2_0::Error, 6>
::android::hardware::graphics::mapper::V2_0::Error::NO_RESOURCES,
::android::hardware::graphics::mapper::V2_0::Error::UNSUPPORTED,
};
} // namespace details
} // namespace hardware
} // namespace android
}
}
}
#endif // HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_MAPPER_V2_0_TYPES_H
#endif
@@ -13,7 +13,6 @@ namespace display {
namespace mapperextensions {
namespace V1_0 {
// Forward declaration for forward reference support:
enum class Error : int32_t;
struct YCbCrLayout;
enum class PlaneComponent : uint32_t;
@@ -36,61 +35,22 @@ struct YCbCrLayout final {
uint32_t chromaStep;
};
/**
* Plane Component type definitions.
*/
enum class PlaneComponent : uint32_t {
PLANE_COMPONENT_Y = 1u /* (1 << 0) */,
PLANE_COMPONENT_Cb = 2u /* (1 << 1) */,
PLANE_COMPONENT_Cr = 4u /* (1 << 2) */,
PLANE_COMPONENT_R = 1024u /* (1 << 10) */,
PLANE_COMPONENT_G = 2048u /* (1 << 11) */,
PLANE_COMPONENT_B = 4096u /* (1 << 12) */,
PLANE_COMPONENT_A = 1048576u /* (1 << 20) */,
PLANE_COMPONENT_RAW = 1073741824u /* (1 << 30) */,
PLANE_COMPONENT_META = 2147483648u /* (1 << 31) */,
PLANE_COMPONENT_Y = 1u ,
PLANE_COMPONENT_Cb = 2u ,
PLANE_COMPONENT_Cr = 4u ,
PLANE_COMPONENT_R = 1024u ,
PLANE_COMPONENT_G = 2048u ,
PLANE_COMPONENT_B = 4096u ,
PLANE_COMPONENT_A = 1048576u ,
PLANE_COMPONENT_RAW = 1073741824u ,
PLANE_COMPONENT_META = 2147483648u ,
};
/**
* Flags to send with getFormatLayout for extra information for e.g. interlaced,
* otherwise send 0.
*/
enum class Flags : int32_t {
LAYOUT_INTERLACED_FLAG = 1 /* (1 << 0) */,
LAYOUT_INTERLACED_FLAG = 1 ,
};
/**
* Structure for describing plane layout information for YUVA/RGBA/RAW formats
* YUV formats can be planar or semi-planar.
*
* This layout structure is used with any pixel format that can be
* represented by it, such as:
* - HAL_PIXEL_FORMAT_YCbCr_*
* - HAL_PIXEL_FORMAT_RGB*_*
* - even implementation defined formats that can be represented by
* the structures
*
* Each plane can be made up of one or more PlaneComponents depending on
* the format.
*
* Each plane can be subsampled either vertically or horizontally by
* a power-of-two factor.
*
* Pixel increment describes the distance in bytes from one pixel to the
* next pixel (to the right) on the same row for the component plane.
* This can be negative.
*
* Stride is provided in both pixels and bytes.
* Stride_bytes describes the distance in bytes from the first pixel of
* one row to the first pixel of the next row (below) for the component plane.
* This can be negative.
*
* Scanlines describes the height of the plane in terms of number of rows.
*
* Size of plane is stride_bytes * scanlines + any padding required for
* alignment as per format specification.
*
*/
struct PlaneLayout final {
::android::hardware::hidl_bitfield<
::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneComponent>
@@ -137,9 +97,6 @@ static_assert(sizeof(::vendor::qti::hardware::display::mapperextensions::V1_0::P
static_assert(__alignof(::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout) == 4,
"wrong alignment");
//
// type declarations for package
//
template <typename>
static inline std::string toString(int32_t o);
@@ -185,7 +142,6 @@ constexpr int32_t& operator&=(
static inline std::string toString(
const ::vendor::qti::hardware::display::mapperextensions::V1_0::YCbCrLayout& o);
// operator== and operator!= are not generated for YCbCrLayout
template <typename>
static inline std::string toString(uint32_t o);
@@ -284,9 +240,6 @@ static inline bool operator!=(
const ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout& lhs,
const ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout& rhs);
//
// type header definitions for package
//
template <>
inline std::string toString<::vendor::qti::hardware::display::mapperextensions::V1_0::Error>(
@@ -379,7 +332,6 @@ static inline std::string toString(
return os;
}
// operator== and operator!= are not generated for YCbCrLayout
template <>
inline std::string
@@ -642,16 +594,13 @@ static inline bool operator!=(
return !(lhs == rhs);
}
} // namespace V1_0
} // namespace mapperextensions
} // namespace display
} // namespace hardware
} // namespace qti
} // namespace vendor
}
}
}
}
}
}
//
// global type declarations for package
//
namespace android {
namespace hardware {
@@ -664,9 +613,9 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::E
::vendor::qti::hardware::display::mapperextensions::V1_0::Error::NO_RESOURCES,
::vendor::qti::hardware::display::mapperextensions::V1_0::Error::UNSUPPORTED,
};
} // namespace details
} // namespace hardware
} // namespace android
}
}
}
namespace android {
namespace hardware {
@@ -688,9 +637,9 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::P
::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneComponent::
PLANE_COMPONENT_META,
};
} // namespace details
} // namespace hardware
} // namespace android
}
}
}
namespace android {
namespace hardware {
@@ -700,8 +649,8 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::F
hidl_enum_values<::vendor::qti::hardware::display::mapperextensions::V1_0::Flags> = {
::vendor::qti::hardware::display::mapperextensions::V1_0::Flags::LAYOUT_INTERLACED_FLAG,
};
} // namespace details
} // namespace hardware
} // namespace android
}
}
}
#endif // HIDL_GENERATED_VENDOR_QTI_HARDWARE_DISPLAY_MAPPEREXTENSIONS_V1_0_TYPES_H
#endif
@@ -46,14 +46,13 @@
using namespace android::hardware;
using namespace android;
// These two functions are used by the blob and aren't present on Q so weakly stub them
extern "C" uint64_t __attribute__((weak)) atrace_get_enabled_tags() {
return 0;
}
namespace android::hardware::details {
void __attribute__((weak)) return_status::onValueRetrieval() const {}
} // namespace android::hardware::details
}
bool IBase::isRemote() const {
return false;
@@ -146,7 +145,7 @@ sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getSt
return nullptr;
}
} // namespace V2_0
}
namespace V3_0 {
sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getStub) {
@@ -154,7 +153,7 @@ sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getSt
return nullptr;
}
} // namespace V3_0
}
namespace V4_0 {
sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getStub) {
@@ -162,8 +161,8 @@ sp<IMapper> IMapper::getService(const std::string& serviceName, const bool getSt
return nullptr;
}
} // namespace V4_0
} // namespace android::hardware::graphics::mapper
}
}
namespace vendor::qti::hardware::display {
namespace mapper {
namespace V4_0 {
@@ -174,7 +173,7 @@ Return<sp<IQtiMapper>> IQtiMapper::castFrom(const sp<graphics::mapper::V4_0::IMa
return nullptr;
}
} // namespace V4_0
}
namespace V3_0 {
Return<sp<IQtiMapper>> IQtiMapper::castFrom(const sp<graphics::mapper::V3_0::IMapper>& parent,
@@ -183,7 +182,7 @@ Return<sp<IQtiMapper>> IQtiMapper::castFrom(const sp<graphics::mapper::V3_0::IMa
return nullptr;
}
} // namespace V3_0
}
namespace V2_0 {
Return<sp<IQtiMapper>> IQtiMapper::castFrom(const sp<graphics::mapper::V2_0::IMapper>& parent,
@@ -200,8 +199,8 @@ Return<void> IQtiMapper::getMapperExtensions(getMapperExtensions_cb _hidl_cb) {
return Void();
}
} // namespace V2_0
} // namespace mapper
}
}
namespace mapperextensions {
namespace V1_1 {
@@ -467,6 +466,6 @@ Return<void> IQtiMapperExtensions::getFormatLayout(int32_t format, uint64_t usag
return Void();
}
} // namespace V1_1
} // namespace mapperextensions
} // namespace vendor::qti::hardware::display
}
}
}
-5
View File
@@ -1,6 +1,3 @@
// What the phone's own Vulkan driver says about its GPU, for the app's device info window: one
// instance on the system loader, the first physical device's properties, a few limits and the
// extensions DXVK and VKD3D-Proton lean on. Returned as "key=value" lines; nothing is kept open.
#include <dlfcn.h>
#include <jni.h>
#include <stdio.h>
@@ -62,12 +59,10 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz
LINE("device=%s\n", p.deviceName);
LINE("type=%s\n", type_name(p.deviceType));
LINE("api=%u.%u.%u\n", VK_API_VERSION_MAJOR(p.apiVersion), VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion));
// Qualcomm packs its driver version as 10.10.12 bits, the same layout as VK_MAKE_VERSION.
LINE("driver_version=%u.%u.%u\n", p.driverVersion >> 22, (p.driverVersion >> 12) & 0x3ff, p.driverVersion & 0xfff);
LINE("vendor_id=0x%04x\ndevice_id=0x%08x\n", p.vendorID, p.deviceID);
LINE("max_image_2d=%u\n", p.limits.maxImageDimension2D);
// Driver name and build string (Vulkan 1.2 or VK_KHR_driver_properties).
uint32_t extCount = 0;
if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL);
static VkExtensionProperties exts[512];
File diff suppressed because it is too large. Load diff
+85 -243
View File
@@ -84,12 +84,11 @@ struct VkTable {
PFN_vkCmdSetScissor CmdSetScissor;
PFN_vkCmdPipelineBarrier CmdPipelineBarrier;
PFN_vkCmdCopyImage CmdCopyImage;
PFN_vkCmdBlitImage CmdBlitImage; // frame-gen capture resolution: composite -> swapchain upscale
// Compute: the native LSFG chain is 25 compute dispatches.
PFN_vkCmdBlitImage CmdBlitImage;
PFN_vkCmdDispatch CmdDispatch;
PFN_vkCreateComputePipelines CreateComputePipelines;
PFN_vkUnmapMemory UnmapMemory;
// win-fg native: clears its flow scratch, resets its scratch pool on resize.
PFN_vkCmdClearColorImage CmdClearColorImage;
PFN_vkResetDescriptorPool ResetDescriptorPool;
PFN_vkCmdCopyBufferToImage CmdCopyBufferToImage;
@@ -125,11 +124,8 @@ struct VkTable {
#include <condition_variable>
#include <chrono>
// Renderer-neutral direct-scanout impl (owns the SurfaceControl/AHB state).
// Included after SCANOUT_LOG is defined above; its own definition is guarded.
#include "../scanout/ScanoutContext.h"
// Native (compositor-side) LSFG frame generation - capability gate.
#include "lsfg/lsfg_probe.h"
#include <memory>
#include <string>
@@ -139,89 +135,83 @@ namespace winfg { class Engine; }
static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2;
// Push-constant layouts for the spatial-upscaler post passes. The leading vec4
// `ndc` matches upscale.vert; the remaining members are read only by the
// fragment stage. std430 offsets line up with these C structs.
struct SgsrPushConstants { // 36 bytes
struct SgsrPushConstants {
float ndc[4];
float viewportInfo[4]; // xy = 1/inputSize, zw = inputSize px
float edgeSharpness; // SGSR EdgeSharpness, from the slider
float viewportInfo[4];
float edgeSharpness;
};
struct NisPushConstants { // 36 bytes
struct NisPushConstants {
float ndc[4];
float viewportInfo[4]; // xy = 1/inputSize, zw = inputSize px
float sharpness; // NIS sharpness [0..1], from the slider
float viewportInfo[4];
float sharpness;
};
struct EasuPushConstants { // 88 bytes
struct EasuPushConstants {
float ndc[4];
uint32_t con0[4], con1[4], con2[4], con3[4];
float outW, outH;
};
struct RcasPushConstants { // 40 bytes
struct RcasPushConstants {
float ndc[4];
uint32_t con[4]; // con.x = bit-packed sharpness
uint32_t con[4];
float outW, outH;
};
// Composable post effects (AMD CAS sharpen + fake-HDR). Both lead with vec4 ndc
// (offset 0) like the upscaler PCs and stay well under the 88-byte PC range.
struct CasPushConstants { // 32 bytes
struct CasPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px
float sharpness; // CAS SHARPNESS term [0..1]
float resolution[2];
float sharpness;
float _pad;
};
struct HdrPushConstants { // 24 bytes
struct HdrPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px
float resolution[2];
};
// Phase 2 screen effects ported from the GL EffectComposer. Each leads with vec4
// ndc (offset 0) like the upscaler PCs and stays well under the 88-byte PC range.
struct FxaaPushConstants { // 24 bytes
struct FxaaPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px
float resolution[2];
};
struct ToonPushConstants { // 24 bytes
struct ToonPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px
float resolution[2];
};
struct ColorPushConstants { // 32 bytes
struct ColorPushConstants {
float ndc[4];
float brightness; // additive [-1,1]
float contrast; // [0,2]
float gamma; // [0.1,5]
float saturation; // [0,2]; 1 = unchanged, 0 = greyscale.
// APPENDED last so the three offsets above
// stay where color.frag already had them.
float brightness;
float contrast;
float gamma;
float saturation;
};
struct NtscPushConstants { // 28 bytes
struct NtscPushConstants {
float ndc[4];
float resolution[2]; // screen size in px (TextureSize/resolution)
float frameCount; // animated chroma-phase counter
float resolution[2];
float frameCount;
};
struct CrtPushConstants { // 24 bytes
struct CrtPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px (unused by shader)
float resolution[2];
};
struct DebandPushConstants { // 28 bytes
struct DebandPushConstants {
float ndc[4];
float resolution[2]; // input texture size in px (layout parity)
float strength; // dither amplitude in LSBs (1.0 = +/-1/255)
float resolution[2];
float strength;
};
class VulkanRendererContext {
public:
// `lsfgVk11Compat` (experimental) lets the LSFG capability probe accept a
// Vulkan 1.1/1.2 device that offers the extensions the chain needs; it has
// to be known before vkCreateDevice, hence a constructor argument.
VulkanRendererContext(ANativeWindow* window, int cWidth, int cHeight, void* adrenotoolsHandle = nullptr,
bool lsfgVk11Compat = false);
~VulkanRendererContext();
void onSurfaceResized(int width, int height);
void setTransform(float ox, float oy, float sx, float sy);
// #413: game-region clip rect (surface px). Stored and applied as the swapchain scissor in
// recordCmdBuf so a FILL/STRETCH game confined to a half (TOP/BOTTOM alignment) can't bleed into
// the on-screen-controls half. A full-surface (or w<=0) rect means "no clip" (CENTER).
void setClipRegion(int x, int y, int w, int h);
void updatePointerPosition(short x, short y);
void updateWindowContent(int64_t id, void* pixels, short w, short h, short stride, int x, int y);
@@ -241,13 +231,7 @@ public:
void scanoutSetBuffer(AHardwareBuffer* ahb, int x, int y, int w, int h, int fenceFd = -1);
void scanoutSetCursorImage(void* pixels, short w, short h, short stride);
void scanoutSetCursorPos(short x, short y, short hotX, short hotY);
// The renderer-neutral scanout implementation. The scanout methods above are
// thin forwarders to this (see VulkanRendererScanout.cpp); the cursor render-
// thread signalling stays in the forwarders, not in ScanoutContext.
ScanoutContext scanout;
// Bound to scanout's atomics so the existing JNI ABI (r->scanoutActive /
// r->gameFrameDelivered direct member access) keeps compiling against a
// single source of truth. Must be declared after `scanout`.
std::atomic<bool>& scanoutActive = scanout.scanoutActive;
std::atomic<bool>& gameFrameDelivered = scanout.gameFrameDelivered;
std::atomic<bool> surfaceDetached{false};
@@ -286,47 +270,16 @@ public:
void setPresentMode(VkPresentModeKHR mode);
std::vector<int> getSupportedPresentModes() const;
// --- Native LSFG frame generation -------------------------------------
// Capability verdict for the compositor-side LSFG engine. Filled at device
// creation and completed once the swapchain format is known; read by the
// UI (through JNI) to grey the engine out with a reason.
const lsfg::Caps& lsfgCaps() const { return lsfgCaps_; }
// Arm/disarm native LSFG frame generation for this session. Changing the
// armed state recreates the swapchain, because the composite path needs
// TRANSFER_DST usage and a deeper image queue that a normal session does
// not pay for.
void setFrameGenArmed(bool armed, int multiplier);
bool frameGenArmed() const { return fgArmed_.load(std::memory_order_relaxed); }
// Live frame-gen telemetry for the in-game readout:
// [0] generations the governor currently trusts
// [1] generations actually planned for the last source frame
// [2] measured source (real) frames per second
// [3] measured presented frames per second
// [4] thermal status, -1 when the device gives no signal
// [5] GPU milliseconds the chain spends per generated frame, -1 unknown
void frameGenStats(float out[6]) const;
// Why the selected native frame-gen engine cannot run here, for the UI:
// -1 not known yet (caps not probed), 0 fine, 1 this driver lacks what the
// engine needs (lsfgCaps().reason says which gate), 2 the engine failed to start.
int frameGenProblem() const;
// Path to the SPIR-V cache built from the user's Lossless.dll. Setting it
// drops any existing engine so the next armed frame rebuilds from it.
void setLsfgCachePath(const char* path);
// Flow scale (0.25-1.0) and the panel's real refresh rate. The pacer never
// generates above the refresh rate.
// Experimental: `captureHeight` sizes the composite ring the chain runs on,
// the width following the swapchain's aspect, so a phone GPU generates at
// game-like resolution and the result is blitted up. 0 = panel;
// kFgCaptureGame = the X screen's height (containerHeight), which already
// carries the per-game override and render scale; otherwise a pixel height.
static constexpr int32_t kFgCaptureGame = -1;
void setFrameGenTuning(float flowScale, float refreshHz, int32_t captureHeight = 0);
// Which native engine generates: 0 = LSFG (needs the cache built from the
// user's Lossless.dll), 1 = win-fg (our own chain, embedded, needs nothing).
// Switching drops the other engine so only one ever holds GPU resources.
void setFrameGenEngine(int kind);
// win-fg only: interpolation model (3/4) and performance preset (0..2).
void setWinFgTuning(int model, int perfPreset);
private:
@@ -359,11 +312,9 @@ private:
ANativeWindow* window;
int surfaceWidth, surfaceHeight, containerWidth, containerHeight;
// #413 compositor clip rect (surface px). clipRegionW<=0 => disabled (whole swapchain). Written from
// the renderer thread (setClipRegion), read from the render thread (recordCmdBuf) -> atomic.
std::atomic<int> clipRegionX{0}, clipRegionY{0}, clipRegionW{0}, clipRegionH{0};
void* adrenotoolsHandle = nullptr;
bool lsfgVk11Compat_ = false; // see the constructor
bool lsfgVk11Compat_ = false;
int filterMode = 0;
bool swapRB = false;
float maxAnisotropy = 1.0f;
@@ -386,9 +337,6 @@ private:
std::vector<VkImageMemoryBarrier> framePreUpload;
std::vector<VkImageMemoryBarrier> framePostUpload;
// [P0] All SurfaceControl / AHardwareBuffer scanout state moved to
// ScanoutContext (member `scanout` above). Dead members (lastDst*,
// ScanoutPending/scanoutPending*, fnSTSetBackPressure) were dropped.
std::atomic<int> pointerX{0}, pointerY{0};
float sceneOffsetX=0.f, sceneOffsetY=0.f, sceneScaleX=1.f, sceneScaleY=1.f;
@@ -428,138 +376,69 @@ private:
VkPipeline pipeline = VK_NULL_HANDLE;
// ---- Spatial upscaler (SGSR / FSR1) ----
// upscalerMode enum (mirrored by JNI nativeSetUpscaler):
// 0 = none (passthrough; current sampler governs scaling)
// 1 = linear (rebuild base sampler LINEAR; no shader pass)
// 2 = nearest (rebuild base sampler NEAREST; no shader pass)
// 3 = sgsr (Snapdragon GSR 1.0; single pass; aspect-fit / letterbox)
// 4 = fsr (AMD FSR1 EASU+RCAS; two passes; fill / stretch)
// 5 = fsr_fit (AMD FSR1 EASU+RCAS; two passes; aspect-fit / letterbox)
// 6 = sharpen (RCAS-only; any resolution; aspect-fit / letterbox)
// 7 = nis (NVIDIA Image Scaling NVScaler; single pass; aspect-fit)
// 8 = sgsr_quality (SGSR 1 edge-direction variant; single pass; aspect-fit)
// Shader upscaling only engages for modes 3-8 AND when the game render
// resolution (container) is smaller than the swapchain. Otherwise the
// existing direct-to-swapchain path is used unchanged.
int upscalerMode = 0;
// Independent of upscalerMode: high-quality Lanczos downscale for the
// supersampling case (game render res > display res). When enabled and the
// render res exceeds the swapchain, replaces the bilinear minify with a
// proper downscale pass. Supersampling and the upscale modes are mutually
// exclusive in practice (one is render>display, the other render<display).
bool hqDownscale = false;
// Composable post effects, independent of the scaling mode. CAS layers a sharpen
// on top of any mode (incl. native res); HDR is a binary fake-HDR. The upscaler
// sharpness (RCAS lobe scale / SGSR EdgeSharpness) is driven by its own slider as a
// linear 0..1 value: 0 = neutral (no sharpening; the upscale still runs), 1 = max.
bool casEnabled = false;
int casSharpness = 60; // slider 0..100 -> CAS SHARPNESS
int casSharpness = 60;
bool hdrEnabled = false;
float upscaleSharpness01 = 0.75f; // slider/100; RCAS lobe scale, SGSR edge derived
// Debanding: terminal TPDF/IGN dither pass before the 8-bit swapchain quantize.
float upscaleSharpness01 = 0.75f;
bool debandEnabled = false;
int debandStrength = 100; // slider 0..200 -> strength/100 LSBs
int debandStrength = 100;
// Phase 2 screen effects (GL EffectComposer parity). FXAA/Toon/CRT/NTSC are
// binary; Color is the brightness/contrast/gamma grade (always-applied via the
// sliders, no toggle) and is "enabled" only when not at neutral (0,0,1).
bool fxaaEnabled = false;
bool toonEnabled = false;
bool crtEnabled = false;
bool ntscEnabled = false;
bool colorEnabled = false; // derived: !neutral grade
float colorBrightness = 0.0f; // [-1,1] (slider/100, clamped)
float colorContrast = 0.0f; // [0,2] (slider/100, clamped)
float colorGamma = 1.0f; // [0.1,5]
float colorSaturation = 1.0f; // [0,2] (slider/100, clamped)
uint32_t ntscFrameCounter = 0; // animates NTSC chroma phase
bool colorEnabled = false;
float colorBrightness = 0.0f;
float colorContrast = 0.0f;
float colorGamma = 1.0f;
float colorSaturation = 1.0f;
uint32_t ntscFrameCounter = 0;
VkSampler upscaleSampler = VK_NULL_HANDLE; // linear clamp; offscreen/mid input
VkSampler upscaleSampler = VK_NULL_HANDLE;
// === Native LSFG: composite target ring ===================================
// Frame generation cannot composite straight into a swapchain image: the
// finished frame has to be READABLE (it becomes the next frame's LSFG
// input) and generated frames have to be STORAGE-WRITABLE by a compute
// dispatch. Android swapchain images are COLOR_ATTACHMENT only, and
// storage support on a swapchain format is not something a driver owes us.
//
// So when frame gen is armed the whole existing recording is redirected at
// a composite image we own - format-identical to the swapchain, so every
// existing pipeline stays render-pass compatible - and a copy moves it into
// the acquired swapchain image at the end. With frame gen off, not one of
// these objects is created and the direct-to-swapchain path is untouched.
struct CompositeTarget {
VkImage img = VK_NULL_HANDLE;
VkDeviceMemory mem = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE; // colour attachment + sampled
VkImageView storageView = VK_NULL_HANDLE; // compute writes (generate)
VkImageView view = VK_NULL_HANDLE;
VkImageView storageView = VK_NULL_HANDLE;
VkFramebuffer fb = VK_NULL_HANDLE;
VkDescriptorSet ds = VK_NULL_HANDLE; // sampled, for later passes
VkDescriptorSet ds = VK_NULL_HANDLE;
};
// Hard ceiling: (max generations + 1) presentable frames per source frame,
// times a queue depth of 2, capped so the footprint stays bounded.
static constexpr uint32_t kMaxCompositeTargets = 7;
std::vector<CompositeTarget> compositeTargets;
VkRenderPass compositeRenderPass = VK_NULL_HANDLE; // CLEAR -> GENERAL
VkRenderPass compositeRenderPass = VK_NULL_HANDLE;
uint32_t compositeW = 0, compositeH = 0;
uint32_t compositeIndex = 0; // rotates per composite; gives history for free
bool compositeArmed = false; // targets exist AND this frame uses them
bool swapchainTransferDst = false; // swapchain was created with TRANSFER_DST
uint32_t compositeIndex = 0;
bool compositeArmed = false;
bool swapchainTransferDst = false;
// Set from the app when the native LSFG engine is selected for this
// session. Read on the render thread; false keeps every path as it was.
std::atomic<bool> fgArmed_{false};
std::atomic<int> fgMultiplier_{0};
// Tuning is written by the UI thread and consumed by the render thread, so
// it is published through atomics and applied to the engine at the top of
// the frame-gen block. The UI thread never touches the engine itself.
std::atomic<float> fgFlowScale_{1.0f};
std::atomic<float> fgRefreshHz_{0.0f};
std::atomic<bool> fgConfigDirty_{true};
// Experimental (see setFrameGenTuning): capture height of the composite
// ring. 0 = panel resolution, kFgCaptureGame = the X screen's height.
std::atomic<int32_t> fgCaptureHeight_{0};
// Extent the composite ring should have for the current swapchain and
// capture height (width follows the swapchain aspect, both even).
void compositeExtentFor(uint32_t& w, uint32_t& h) const;
// The extent the scene is rendered at this frame: the composite ring while
// frame gen runs on a ring smaller than the swapchain (experimental capture
// resolution), otherwise the swapchain. Every full-target pass (the effect
// chain, the spatial upscalers, the render-scale downscale) sizes itself by
// this, so those keep working below panel resolution and their
// intermediates shrink with the ring.
VkExtent2D renderExtent() const {
if (compositeActive() && (compositeW != swapchainExt.width || compositeH != swapchainExt.height))
return VkExtent2D{compositeW, compositeH};
return swapchainExt;
}
// Copy (same extent) or blit (composite smaller than the swapchain) a
// composite-ring image, already in TRANSFER_SRC, into a swapchain image
// already in TRANSFER_DST.
void recordCompositeToSwapchainTransfer(VkCommandBuffer cb, VkImage src, uint32_t imgIdx);
bool createCompositeRenderPass();
bool ensureCompositeTargets(uint32_t w, uint32_t h, uint32_t count);
void destroyCompositeTargets();
// True when this frame should composite off-swapchain. Call on the render
// thread; every gate must hold or we fall back to the untouched path.
bool compositeActive() const;
// Render pass / framebuffer this frame draws its FINAL pass into.
VkRenderPass targetRenderPass() const;
VkFramebuffer targetFramebuffer(uint32_t imgIdx) const;
// Copy the finished composite into the acquired swapchain image and leave
// it in PRESENT_SRC. No-op when the composite path is not active.
void copyCompositeToSwapchain(VkCommandBuffer cb, uint32_t imgIdx);
// === Native LSFG: the software cursor ====================================
// LSFG interpolates whatever it is given, so a cursor composited into the
// frame gets warped along the flow field and smears. It is therefore
// excluded from the composite while frame gen is armed and drawn once into
// EVERY presented image instead - real and generated alike - through a
// load-op render pass that leaves the image ready to present.
VkRenderPass cursorOverlayRenderPass = VK_NULL_HANDLE;
struct CursorOverlay {
bool draw = false;
@@ -569,19 +448,16 @@ private:
CursorOverlay cursorOverlay_{};
bool createCursorOverlayRenderPass();
// Draw the cursor into an already-copied swapchain image and leave it in
// PRESENT_SRC. Takes over the final transition from the copy helpers.
void recordCursorOverlay(VkCommandBuffer cb, uint32_t imgIdx);
// True when the cursor is being drawn per present rather than composited.
bool cursorDrawnPerPresent() const;
// === Native LSFG: the per-source-frame present plan =======================
// Interpolation produces frames that belong BETWEEN N-1 and N, so the
// generated frames are presented FIRST and real frame N is held back one
// slot. All presents for one source frame are queued together: with FIFO
// the driver then shows them on consecutive vblanks, so the pacing falls
// out of the present mode for free - no sleeps, no render-mode change.
static constexpr uint32_t kMaxPresentsPerFrame = 4; // 1 real + up to 3 generated
static constexpr uint32_t kMaxPresentsPerFrame = 4;
struct FrameGenPlan {
uint32_t generations = 0;
uint32_t presents = 1;
@@ -594,30 +470,17 @@ private:
bool lsfgEngineTried_ = false;
std::unique_ptr<winfg::Engine> winfgEngine_;
bool winfgEngineTried_ = false;
std::atomic<int> fgEngineKind_{0}; // 0 = lsfg, 1 = win-fg
std::atomic<int> fgEngineKind_{0};
std::atomic<int> fgModel_{4};
std::atomic<int> fgPerfPreset_{1};
bool ensureWinFgEngine();
// Capability gate for the SELECTED engine. win-fg's shaders need only a
// storage-capable swapchain format; the LSFG chain also needs the fp16 /
// memory-model feature set, which some otherwise capable GPUs lack.
bool fgCapsOk() const;
// Sync objects are indexed per PRESENT, not per composite: each pending
// present needs its own image-available and render-finished semaphore.
uint32_t syncSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; }
// Destroy and rebuild every acquire/present semaphore. Called with the
// swapchain, so no stale pending signal can survive into the new one.
void recreateSyncObjects();
uint32_t fgAcquireFailLog_ = 0;
// Measured PRESENTS per second - the rate that actually reaches the panel,
// counting generated frames. The pacer's own "loop rate" cannot serve this
// purpose: it is sampled once per SOURCE frame, so it always equals the
// guest rate and contains no evidence that generation happened at all.
float fgPresentedRate_ = 0.0f;
// Measured SOURCE frames per second over the same window, for engines that
// do not measure it themselves (win-fg).
float fgSourceRate_ = 0.0f;
uint32_t fgSourceAccum_ = 0;
uint32_t fgPresentAccum_ = 0;
@@ -626,22 +489,10 @@ private:
void trackPresentedRate(uint32_t presents);
bool ensureLsfgEngine();
// One command buffer per pending present: slot 0 carries the composite,
// the chain's shared passes and generated frame 0; each later generated
// frame and the real frame are recorded and submitted on their own, so a
// finished frame reaches the presentation engine without waiting for the
// rest of the chain.
uint32_t cmdSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; }
// Shared chain passes (the 24 shaders every generated frame depends on).
void recordFrameGenProcess(VkCommandBuffer cb);
// Generated frame g: synthesise into a spare composite target and copy it
// into the swapchain image reserved for it.
void recordFrameGenGeneration(VkCommandBuffer cb, uint32_t g);
// Chain cost: a timestamp pair per frame slot, start before the shared
// passes, end after the last generation's compute (before its copy, so a
// vblank wait on the swapchain image is not counted). Read back after the
// slot's fence wait, one frame later.
VkQueryPool fgQueryPool_ = VK_NULL_HANDLE;
bool fgTimestampsOk_ = false;
float fgTimestampPeriodNs_ = 0.0f;
@@ -654,22 +505,21 @@ private:
void readFgQueryResult();
VkFormat offscreenFmt = VK_FORMAT_R8G8B8A8_UNORM;
VkRenderPass offscreenRenderPass = VK_NULL_HANDLE; // CLEAR -> SHADER_READ_ONLY
VkRenderPass offscreenRenderPass = VK_NULL_HANDLE;
VkPipelineLayout postPipeLayout = VK_NULL_HANDLE;
VkPipeline sgsrPipeline = VK_NULL_HANDLE;
VkPipeline nisPipeline = VK_NULL_HANDLE; // NVIDIA Image Scaling (mode 7)
VkPipeline sgsrQualityPipeline = VK_NULL_HANDLE; // SGSR 1 edge-direction (mode 8)
VkPipeline nisPipeline = VK_NULL_HANDLE;
VkPipeline sgsrQualityPipeline = VK_NULL_HANDLE;
VkPipeline easuPipeline = VK_NULL_HANDLE;
VkPipeline rcasPipeline = VK_NULL_HANDLE;
VkPipeline downscalePipeline = VK_NULL_HANDLE;
// CAS/HDR can be either a non-final pass (writes an fx target via
// offscreenRenderPass) or the final pass (writes the swapchain via renderPass);
// a pipeline is render-pass-specific, so keep one variant of each.
VkPipeline casPipelineOff = VK_NULL_HANDLE; // -> fx target (offscreenRenderPass)
VkPipeline casPipelineSwap = VK_NULL_HANDLE; // -> swapchain (renderPass)
VkPipeline casPipelineOff = VK_NULL_HANDLE;
VkPipeline casPipelineSwap = VK_NULL_HANDLE;
VkPipeline hdrPipelineOff = VK_NULL_HANDLE;
VkPipeline hdrPipelineSwap = VK_NULL_HANDLE;
// Phase 2 effects: same Off (-> fx target) / Swap (-> swapchain) variant pair.
VkPipeline fxaaPipelineOff = VK_NULL_HANDLE;
VkPipeline fxaaPipelineSwap = VK_NULL_HANDLE;
VkPipeline toonPipelineOff = VK_NULL_HANDLE;
@@ -680,10 +530,8 @@ private:
VkPipeline ntscPipelineSwap = VK_NULL_HANDLE;
VkPipeline crtPipelineOff = VK_NULL_HANDLE;
VkPipeline crtPipelineSwap = VK_NULL_HANDLE;
// Debanding is always the LAST effect (terminal) -> only a swapchain variant.
VkPipeline debandPipelineSwap = VK_NULL_HANDLE;
// offscreen composite target @ game/container resolution
VkImage offscreenImg = VK_NULL_HANDLE;
VkDeviceMemory offscreenMem = VK_NULL_HANDLE;
VkImageView offscreenView = VK_NULL_HANDLE;
@@ -691,7 +539,6 @@ private:
VkDescriptorSet offscreenDS = VK_NULL_HANDLE;
int offscreenW = 0, offscreenH = 0;
// FSR intermediate (EASU output) @ upscale output resolution
VkImage midImg = VK_NULL_HANDLE;
VkDeviceMemory midMem = VK_NULL_HANDLE;
VkImageView midView = VK_NULL_HANDLE;
@@ -699,8 +546,6 @@ private:
VkDescriptorSet midDS = VK_NULL_HANDLE;
int midW = 0, midH = 0;
// Effect-chain intermediates @ swapchain resolution. fx1 holds the scaled (or
// composited) image fed into CAS/HDR; fx2 ping-pongs when both effects run.
VkImage fx1Img = VK_NULL_HANDLE;
VkDeviceMemory fx1Mem = VK_NULL_HANDLE;
VkImageView fx1View = VK_NULL_HANDLE;
@@ -715,21 +560,18 @@ private:
VkDescriptorSet fx2DS = VK_NULL_HANDLE;
int fx2W = 0, fx2H = 0;
// Per-frame upscale plan, computed in renderFrame, consumed by recordCmdBuf.
// upFrame.mode reuses the upscalerMode enum (3=sgsr,4=fsr,5=fsr_fit,6=sharpen,7=nis,8=sgsr_quality)
// plus an internal sentinel (UPMODE_DOWNSCALE) for the supersampling path.
struct UpscaleFrame {
bool active = false;
int mode = 0;
int outX = 0, outY = 0, outW = 0, outH = 0;
bool cas = false; // run CAS this frame (snapshot of casEnabled)
bool hdr = false; // run HDR this frame (snapshot of hdrEnabled)
bool fxaa = false; // snapshot of fxaaEnabled
bool toon = false; // snapshot of toonEnabled
bool color = false; // snapshot of colorEnabled (non-neutral grade)
bool ntsc = false; // snapshot of ntscEnabled
bool crt = false; // snapshot of crtEnabled
bool deband = false;// snapshot of debandEnabled (terminal dither)
bool cas = false;
bool hdr = false;
bool fxaa = false;
bool toon = false;
bool color = false;
bool ntsc = false;
bool crt = false;
bool deband = false;
SgsrPushConstants sgsrPC{};
NisPushConstants nisPC{};
EasuPushConstants easuPC{};
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_alpha.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_beta.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_chain.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_common.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_delta.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
+17 -41
View File
@@ -1,9 +1,5 @@
// See lsfg_dll.h. PE resource walk + on-device SPIR-V cache for the Lossless
// Scaling frame-generation chain.
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck
// by @maxjivi05. GPL-3.0-or-later.
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS).
// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
#include "lsfg_dll.h"
#include "lsfg_dxbc.h"
@@ -25,9 +21,9 @@
namespace lsfg {
namespace {
// ---- PE constants ---------------------------------------------------------
constexpr uint16_t kDosMagic = 0x5A4Du; // "MZ"
constexpr uint32_t kPeSignature = 0x00004550u; // "PE\0\0"
constexpr uint16_t kDosMagic = 0x5A4Du;
constexpr uint32_t kPeSignature = 0x00004550u;
constexpr uint16_t kPe32Magic = 0x010Bu;
constexpr uint16_t kPe32PlusMagic = 0x020Bu;
@@ -47,7 +43,6 @@ constexpr size_t kResourceEntrySize = 8u;
constexpr uint32_t kResourceSubdirectoryBit = 0x80000000u;
constexpr uint32_t kResourceTypeRcData = 10u;
// ---- SPIR-V constants -----------------------------------------------------
constexpr uint32_t kSpirvMagic = 0x07230203u;
constexpr size_t kSpirvHeaderWords = 5u;
constexpr uint32_t kSpirvWordCountShift = 16u;
@@ -64,7 +59,7 @@ constexpr uint32_t kVariantFp32Offset = 98u;
constexpr uint32_t kMaxResourceId = 512u;
constexpr size_t kMaxSpirvWords = 16u * 1024u * 1024u;
constexpr uint32_t kCacheMagic = 0x4C534642u; // "BFSL" little-endian
constexpr uint32_t kCacheMagic = 0x4C534642u;
constexpr uint32_t kCacheVersion = 1u;
struct CacheHeader {
@@ -83,7 +78,6 @@ struct PeSection {
uint32_t rawSize = 0;
};
// Read-only mapping of the DLL. Closes itself; the DLL is never executed.
class PeImage {
public:
~PeImage() { close(); }
@@ -168,7 +162,6 @@ private:
std::vector<PeSection> sections_;
};
// RCDATA id -> blob, for the ids we care about.
struct ResourceTable {
const uint8_t* data[kMaxResourceId] = {};
uint32_t size[kMaxResourceId] = {};
@@ -217,7 +210,6 @@ bool resourceReadLeaf(const PeImage& img, size_t leafOffset,
return true;
}
// Walk type -> name -> language and keep the first language of every RCDATA id.
bool collectRcData(const PeImage& img, size_t resourceBase, ResourceTable& out) {
size_t typeTotal = 0;
if (!resourceEntryCount(img, resourceBase, typeTotal)) return false;
@@ -256,7 +248,7 @@ bool collectRcData(const PeImage& img, size_t resourceBase, ResourceTable& out)
if (!resourceReadLeaf(img, resourceBase + langOffset, blob, blobSize)) continue;
out.data[nameId] = blob;
out.size[nameId] = blobSize;
break; // first language wins
break;
}
}
}
@@ -289,7 +281,6 @@ DllStatus parseResources(PeImage& img, ResourceTable& table) {
return DllStatus::Ok;
}
// ---- SPIR-V adoption (precompiled-variant path) ---------------------------
bool isSpirvModule(const uint8_t* blob, uint32_t size) {
if (!blob || size < kSpirvHeaderWords * sizeof(uint32_t)) return false;
@@ -315,9 +306,6 @@ uint32_t lookupDescriptorSet(const std::vector<uint32_t>& w, uint32_t targetId)
return 0;
}
// Precompiled blobs are renumbered into set/binding order - that is the
// convention they were built with. The DXBC path uses encounter order instead
// (see lsfg_dxbc), which is what DXVK's output pairs with. Do not merge them.
bool renumberBindingsSetOrder(std::vector<uint32_t>& w) {
struct Slot { uint32_t set; uint32_t binding; size_t literalOffset; };
std::vector<Slot> slots;
@@ -399,8 +387,6 @@ bool writeCache(const std::string& cachePath, const CacheHeader& header, const M
if (ok) ok = fsync(fileno(f)) == 0;
fclose(f);
// Temp file + rename, so a failed or interrupted build can never leave a
// half-written cache that would later be loaded as if it were complete.
if (!ok || rename(tempPath.c_str(), cachePath.c_str()) != 0) {
unlink(tempPath.c_str());
return false;
@@ -416,9 +402,8 @@ bool readCacheHeader(const std::string& cachePath, CacheHeader& out) {
return ok && out.magic == kCacheMagic && out.version == kCacheVersion;
}
} // namespace
}
// ---- public API -----------------------------------------------------------
const std::vector<uint32_t>& shaderIds() {
static const std::vector<uint32_t> ids = [] {
@@ -584,7 +569,6 @@ DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet) {
return status;
}
// ---- SPIR-V version lowering (Vulkan 1.1/1.2 compat) ----------------------
namespace {
@@ -593,14 +577,14 @@ constexpr uint32_t kSpirvOpExtInstImport= 11u;
constexpr uint32_t kSpirvOpMemoryModel = 14u;
constexpr uint32_t kSpirvOpCapability = 17u;
// Capabilities that are core in the version the translator emits but not in
// the versions we lower to.
constexpr uint32_t kCapVulkanMemoryModel = 5345u; // core from 1.5
constexpr uint32_t kCapVulkanMemoryModelDeviceScope = 5346u; // core from 1.5
constexpr uint32_t kCapDemoteToHelperInvocation = 5379u; // core from 1.6
constexpr uint32_t kCapVulkanMemoryModel = 5345u;
constexpr uint32_t kCapVulkanMemoryModelDeviceScope = 5346u;
constexpr uint32_t kCapDemoteToHelperInvocation = 5379u;
std::vector<uint32_t> encodeOpExtension(const char* name) {
const size_t len = strlen(name) + 1; // incl. NUL
const size_t len = strlen(name) + 1;
const size_t strWords = (len + 3) / 4;
std::vector<uint32_t> ins(1 + strWords, 0u);
ins[0] = ((uint32_t)(1 + strWords) << kSpirvWordCountShift) | kSpirvOpExtension;
@@ -624,17 +608,13 @@ bool hasOpExtension(const std::vector<uint32_t>& w, const char* name) {
return false;
}
} // namespace
}
bool downgradeSpirv(std::vector<uint32_t>& w, uint32_t targetVersion) {
if (w.size() < kSpirvHeaderWords || w[0] != kSpirvMagic) return false;
if (targetVersion < kSpirv14) return false;
if (w[1] <= targetVersion) return true; // already low enough
if (w[1] <= targetVersion) return true;
// Walk the preamble first and only touch the module once it is known to
// be lowerable: note which extension-gated capabilities are declared and
// where the capability block ends (OpExtension must follow every
// OpCapability and precede OpExtInstImport / OpMemoryModel).
bool needMemoryModel = false;
size_t insertAt = kSpirvHeaderWords;
size_t off = kSpirvHeaderWords;
@@ -645,9 +625,6 @@ bool downgradeSpirv(std::vector<uint32_t>& w, uint32_t targetVersion) {
if (opcode == kSpirvOpCapability && length >= 2) {
const uint32_t cap = w[off + 1];
if (cap == kCapVulkanMemoryModel || cap == kCapVulkanMemoryModelDeviceScope) needMemoryModel = true;
// Core only in 1.6, and VK_EXT_shader_demote_to_helper_invocation
// is never enabled on the compat path: refuse rather than hand the
// driver a module that is not legal at the lowered version.
if (cap == kCapDemoteToHelperInvocation) return false;
insertAt = off + length;
} else if (opcode == kSpirvOpExtension) {
@@ -659,7 +636,6 @@ bool downgradeSpirv(std::vector<uint32_t>& w, uint32_t targetVersion) {
off += length;
}
// 1.5 has the memory model as core; only 1.4 needs it declared.
if (targetVersion < kSpirv15 && needMemoryModel
&& !hasOpExtension(w, "SPV_KHR_vulkan_memory_model")) {
const auto e = encodeOpExtension("SPV_KHR_vulkan_memory_model");
@@ -669,4 +645,4 @@ bool downgradeSpirv(std::vector<uint32_t>& w, uint32_t targetVersion) {
return true;
}
} // namespace lsfg
}
+13 -70
View File
@@ -1,35 +1,7 @@
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS).
// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
#pragma once
// ============================================================================
// lsfg_dll - extract the Lossless Scaling frame-generation shader chain from
// the user's own Lossless.dll, and cache it on device as SPIR-V.
//
// The 25 compute shaders that make up LSFG 3.1 are NOT redistributable. They
// ship as RCDATA resources inside Lossless.dll from the user's own paid copy
// of Lossless Scaling (Steam app 993090). Every implementation - upstream
// lsfg-vk, LSFG-Android, eden, WinNative - requires the user to supply their
// own DLL and extracts the resources on device. DroidDeck does the same, and
// bundles nothing.
//
// The DLL is mmap'd READ-ONLY and parsed as data. It is never loaded as a
// library and never executed.
//
// Resource layout, measured against real installs:
// * The base chain lives at RCDATA ids 255, 256 and 280-302 (25 modules),
// and is DXBC. It has always been DXBC.
// * Lossless Scaling MAY additionally carry precompiled SPIR-V copies at
// base+49 (native fp16) and base+98 (native fp32). No build currently
// downloadable from Steam carries them - public buildId 19655272 was
// measured with zero SPIR-V anywhere in the 311 MB install - so the DXBC
// path is the normal path and the translator is not optional. The SPIR-V
// path is kept because it costs nothing and a future build may ship them.
//
// Both producers fill the same shader-id -> SPIR-V words map, so everything
// downstream of this file is source-agnostic.
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck
// by @maxjivi05. GPL-3.0-or-later.
// ============================================================================
#include <cstdint>
#include <string>
@@ -39,44 +11,37 @@ namespace lsfg {
enum class DllStatus {
Ok = 0,
NotInstalled, // no such file / cannot open
UnreadableFile, // opens but cannot be mapped or is empty
NotInstalled,
UnreadableFile,
NotPortableExecutable,
MissingShaders, // parses, but the base chain is not all there
TranslationFailed, // a shader failed DXBC->SPIR-V or SPIR-V adoption
CacheUnusable // cache could not be written or read back
MissingShaders,
TranslationFailed,
CacheUnusable
};
// SPIR-V version words. The translator emits 1.6; a Vulkan 1.1 device takes
// 1.4 (via VK_KHR_spirv_1_4) and a 1.2 device 1.5. One definition for the
// probe, the engine and the shader loader.
constexpr uint32_t kSpirv14 = 0x00010400u;
constexpr uint32_t kSpirv15 = 0x00010500u;
constexpr uint32_t kSpirv16 = 0x00010600u;
enum class Variant {
None = 0,
SpirvFp16, // precompiled fp16 blobs at base+49
SpirvFp32, // precompiled fp32 blobs at base+98
DxbcTranslated // base chain, translated on device (the normal case)
SpirvFp16,
SpirvFp32,
DxbcTranslated
};
// The 25 shader ids that make up the chain.
constexpr uint32_t kShaderMipmaps = 255u;
constexpr uint32_t kShaderGenerate = 256u;
constexpr uint32_t kShaderChainFirst= 280u;
constexpr uint32_t kShaderChainLast = 302u;
constexpr uint32_t kShaderCount = 25u;
// Upstream spelling for the two ids the ported chain files refer to by name.
// Kept so those files stay byte-close to their upstream form; these alias the
// constants above rather than being a second source of truth.
constexpr uint32_t LSFG_SHADER_MIPMAPS = kShaderMipmaps;
constexpr uint32_t LSFG_SHADER_GENERATE = kShaderGenerate;
struct Module {
uint32_t id = 0;
std::vector<uint32_t> words; // SPIR-V
std::vector<uint32_t> words;
};
struct ModuleSet {
@@ -87,41 +52,19 @@ struct ModuleSet {
bool complete() const;
};
// The canonical shader-id list, in dispatch-independent order.
const std::vector<uint32_t>& shaderIds();
const char* statusName(DllStatus s);
const char* variantName(Variant v);
// Does this file look like a usable Lossless.dll? Checks the base chain only,
// so a DLL without the SPIR-V variants (i.e. every build on Steam today) is
// reported as valid - the translator handles it.
DllStatus validateDll(const std::string& dllPath);
// Parse the DLL, produce all 25 SPIR-V modules, and write them to cachePath
// (via temp file + rename, so a failed build cannot leave a half-written
// cache behind). Records which producer ran in the cache header.
DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, bool preferFp16);
// Load a previously built cache.
DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet);
// Which producer built an existing cache, without loading the modules.
DllStatus cacheVariant(const std::string& cachePath, Variant& outVariant);
// Experimental Vulkan 1.1 / 1.2 compat: lower a DXBC-translated module's
// declared SPIR-V version to `targetVersion` (kSpirv14 or kSpirv15). Below 1.5
// the Vulkan memory model is not core, so SPV_KHR_vulkan_memory_model is
// declared for it (the device side enables VK_KHR_vulkan_memory_model). The
// cache is built without a device in hand, always at the translator's native
// 1.6, so this runs at load time against whatever the device can take. A
// module already at or below the target is left untouched. Returns false,
// leaving the module untouched, for a malformed module, a target below 1.4, or
// a module declaring DemoteToHelperInvocation: that capability is core only in
// 1.6 and its extension is never enabled on the compat path, so such a module
// cannot be made legal here (DXVK emits it for pixel-shader discard only, so
// the 25 compute shaders never carry it). Whether the device then accepts the
// result is vkCreateShaderModule's / vkCreateComputePipelines' verdict.
bool downgradeSpirv(std::vector<uint32_t>& words, uint32_t targetVersion);
} // namespace lsfg
}
+5 -16
View File
@@ -1,10 +1,5 @@
// See lsfg_dxbc.h. DXBC -> SPIR-V via DXVK's vendored translator.
//
// Follows upstream lsfg-vk's src/extract/trans.cpp, including its
// encounter-order binding renumber. Ported to WinNative (lsfg_dxbc.cpp) and
// DroidDeck by @maxjivi05 from the work of Camille LaVey / the Eden Emulator
// Project (GPL-3.0-or-later); the translator itself is DXVK's, zlib licensed,
// vendored under cpp/thirdparty/dxbc.
// Ported to WinNative and DroidDeck by @maxjivi05 from Camille LaVey / the Eden Emulator Project (GPL-3.0-or-later).
// DXVK's translator is zlib licensed; vendored under cpp/thirdparty/dxbc.
#include "lsfg_dxbc.h"
@@ -21,12 +16,12 @@ namespace {
struct BindingSlot {
uint32_t bindingIndex = 0;
uint32_t bindingOffset = 0; // word offset of the Binding literal
uint32_t bindingOffset = 0;
uint32_t setIndex = 0;
uint32_t setOffset = 0;
};
} // namespace
}
bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>& outWords) {
outWords.clear();
@@ -38,9 +33,6 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>
const dxvk::DxbcModuleInfo info{};
auto code = module.compile(info, "CS");
// Collect the descriptor decorations in the order the Binding
// decorations appear. Everything before the first OpFunction is the
// decoration block, so the scan stops there.
std::vector<BindingSlot> slotsByVarId;
std::vector<uint32_t> varIdsInOrder;
for (auto ins : code) {
@@ -60,7 +52,6 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>
if (ins.opCode() == spv::OpFunction) break;
}
// Renumber to a dense 0..n range, in encounter order.
uint32_t next = 0;
for (const uint32_t varId : varIdsInOrder) {
const BindingSlot& slot = slotsByVarId[varId];
@@ -75,11 +66,9 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>
std::memcpy(outWords.data(), code.data(), byteSize);
return true;
} catch (...) {
// DXVK's compiler signals malformed bytecode by throwing. A bad
// resource must degrade to "this DLL is unusable", never to a crash.
outWords.clear();
return false;
}
}
} // namespace lsfg
}
+2 -30
View File
@@ -1,39 +1,11 @@
#pragma once
// ============================================================================
// lsfg_dxbc - DXBC -> SPIR-V for the Lossless Scaling shader chain.
//
// Why this exists at all: the base chain inside Lossless.dll is DXBC, and has
// always been DXBC. Lossless Scaling 3.2.2's release note ("Added shaders
// intended for use by the lsfg-vk project") describes precompiled SPIR-V
// copies at RCDATA base+49 / base+98 - but no build downloadable from Steam
// carries them. Measured against the current public build (buildId 19655272,
// 2025-08-19; linux_testing byte-identical on depot 993091): the whole 311 MB
// install contains zero occurrences of the SPIR-V magic word, and the DLL's
// 202 RCDATA entries are all DXBC.
//
// So a translator is required, not optional. Upstream lsfg-vk reaches the same
// conclusion and links DXVK's `dxbc` in src/extract/trans.cpp; we vendor the
// same subset (zlib licence) under cpp/thirdparty/dxbc and follow its
// trans.cpp step for step.
//
// The binding renumber here is ENCOUNTER ORDER - the order in which Binding
// decorations appear in DXVK's output - which is what DXVK's own layout pairs
// with. The precompiled-SPIR-V path in lsfg_dll.cpp uses a set/binding sort
// instead, because those blobs were built with that convention. The two must
// not be merged.
// ============================================================================
// DXVK's dxbc subset is zlib licensed; vendored under cpp/thirdparty/dxbc.
#include <cstdint>
#include <vector>
namespace lsfg {
// Translate one DXBC compute shader to SPIR-V, renumbering descriptor bindings
// into a dense 0..n range in encounter order. Returns false (leaving outWords
// empty) on any malformed input - DXVK's compiler throws, and everything is
// caught here.
// Emits DXVK's native SPIR-V 1.6; lowering for a Vulkan 1.1/1.2 device happens
// at load time (lsfg_dll.h downgradeSpirv).
bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector<uint32_t>& outWords);
} // namespace lsfg
}
+2 -43
View File
@@ -1,8 +1,3 @@
// See lsfg_engine.h.
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by
// @maxjivi05, where it runs on gamescope's output in the compositor.
#include "lsfg_engine.h"
@@ -21,9 +16,6 @@
namespace lsfg {
namespace {
// The chain needs two real frames in its history before anything it produces
// is meaningful, and we require the plan to stay warm for a couple of frames
// so a single settled sample cannot start generation from garbage.
constexpr uint64_t kRequiredFrames = 2;
constexpr uint32_t kRecurrenceFrames = 2;
constexpr uint64_t kTelemetryInterval = 120;
@@ -50,8 +42,6 @@ VkImageMemoryBarrier makeTransition(VkImage image, VkAccessFlags srcAccess,
return b;
}
// Copy the just-composited frame into the chain's input ring. `source` is the
// composite target, which the composite render pass leaves in GENERAL.
void copyPresentedFrame(VkCommandBuffer cmd, VkImage source, LsfgImage& destination,
VkExtent2D extent) {
const VkImageMemoryBarrier before[] = {
@@ -88,7 +78,7 @@ void copyPresentedFrame(VkCommandBuffer cmd, VkImage source, LsfgImage& destinat
destination.SetLayout(VK_IMAGE_LAYOUT_GENERAL);
}
} // namespace
}
Engine::Engine() = default;
@@ -142,9 +132,6 @@ void Engine::setGuestExtent(uint32_t width, uint32_t height) {
peakGuestExtent_.height = std::max(peakGuestExtent_.height, height);
}
// The flow pyramid is the expensive part, so its resolution is scaled relative
// to the largest guest surface seen: a game running well below panel
// resolution does not need a panel-resolution flow field.
float Engine::effectiveFlowScale(uint32_t width) const {
if (width == 0 || peakGuestExtent_.width == 0) return flowScale_;
const float ratio = (float)peakGuestExtent_.width / (float)width;
@@ -171,8 +158,6 @@ bool Engine::prepare(uint32_t width, uint32_t height, VkFormat format) {
chain_ = std::make_unique<LsfgChain>(device_, *shaders_, VkExtent2D{width, height}, format,
scale);
if (!chain_->Valid()) {
// A device that cannot build the chain reports unsupported once and
// stops trying, rather than thrashing allocations every frame.
LSFG_LOGW("chain build failed at %ux%u; frame generation unavailable", width, height);
chain_.reset();
unavailable_ = true;
@@ -204,17 +189,9 @@ uint32_t Engine::plan(uint32_t capacity, uint64_t sourceFrames) {
plan_ = pacer_.Plan(std::min<size_t>(capacity, kMaxGenerations), sourceFrames);
// The pacer says how many frames FIT in the panel's budget. The governor
// says how many this device can actually afford right now - on a handheld
// the chain competes with the game for one GPU, so an extra generation has
// to prove it improves total output without collapsing the real frame rate.
if (governorEnabled_) {
const LsfgPacerStats s = pacer_.Stats();
governor_.configure((uint32_t)std::min<size_t>(pacer_.MaxGenerations(), kMaxGenerations));
// presentedRate_, NOT the pacer's loop rate. The pacer samples its loop
// once per SOURCE frame, so its "loop rate" is the guest rate by
// construction and can never show that generation added anything - the
// governor could not accept a probe on any hardware.
plan_.generations = governor_.cap((uint32_t)plan_.generations, s.source_rate,
presentedRate_);
}
@@ -223,12 +200,6 @@ uint32_t Engine::plan(uint32_t capacity, uint64_t sourceFrames) {
warmStreak_ = warm_ ? warmStreak_ + 1 : 0;
generating_ = warm_ && warmStreak_ >= kRecurrenceFrames && plan_.generations > 0;
// The ring is only seeded while generating, so the first frame after any
// gap - arming, or a pacer discontinuity after a hitch - would interpolate
// between this frame and whatever the other slot held when generation last
// stopped. That is a visible warp, and it happens every time the engine
// starts. Spend this frame filling the ring instead and generate from the
// next one, when both slots are genuinely consecutive.
primeHistory_ = false;
if (generating_ && !historyFresh()) {
primeHistory_ = true;
@@ -265,11 +236,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32
lastCount_ = count;
lastGenerations_ = generations;
// Seeding the input ring is a full-resolution image copy every frame, so it
// is NOT free at 100 fps - it was costing real frame rate while producing
// nothing. Copy only when generating, on the one priming frame that
// precedes generation, or while the governor is warming up towards a
// probe - so the ring is populated by the time it is needed.
const bool needHistory = generations > 0 || primeHistory_ || followEveryFrame()
|| (governorEnabled_ && governor_.wantsHistory());
if (needHistory) {
@@ -278,13 +244,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32
haveCopied_ = true;
}
// The SHARED chain is 24 of the 25 shaders - the whole flow pyramid - and
// only `generate` runs per generated frame. Running it while producing
// nothing spends almost the entire cost of frame generation for no frames
// at all, every frame. That is what pinned the GPU at 100% on device
// regardless of the game's own settings, and it fed straight back into the
// governor: the chain made the source rate collapse, the governor saw the
// collapse and refused to generate, and refusing did not stop the chain.
if (warm_ && (generations > 0 || followEveryFrame())) chain_->DispatchShared(cmd, count);
}
@@ -318,4 +277,4 @@ void Engine::reset() {
plan_ = {};
}
} // namespace lsfg
}
+2 -58
View File
@@ -1,25 +1,4 @@
#pragma once
// ============================================================================
// lsfg_engine - the compositor's handle on native LSFG frame generation.
//
// Owns the shader modules, the interpolation chain and the pacer, and exposes
// the small contract the render loop needs:
//
// prepare(w, h, format) build/rebuild the chain for this size
// plan(capacity, sourceFrames) how many frames to generate this time
// process(cmd, source, w, h, gens) take frame N in, run the shared chain
// generateInto(cmd, g, i, img, view) synthesise generated frame g
//
// Ordering matters and is not obvious: interpolation produces frames that
// belong BETWEEN N-1 and N, so the generated frames are presented FIRST and
// the real frame N is held back one slot. That one output interval is the
// unavoidable cost of interpolating rather than extrapolating; no placement of
// this code changes it.
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by
// @maxjivi05, where it runs on gamescope's output in the compositor.
// ============================================================================
#include <cstdint>
#include <memory>
@@ -28,7 +7,7 @@
#include <vulkan/vulkan.h>
#include "lsfg_common.hpp"
#include "lsfg_dll.h" // kSpirv16
#include "lsfg_dll.h"
#include "lsfg_governor.h"
#include "lsfg_pacer.hpp"
@@ -37,27 +16,16 @@ namespace lsfg {
class LsfgShaders;
class LsfgChain;
// Hard ceiling on generated frames per source frame (2x..4x -> 1..3).
constexpr uint32_t kMaxGenerations = 3;
class Engine {
public:
// Both the constructor and destructor are out-of-line ON PURPOSE. The
// shader set and the chain are held by unique_ptr to forward-declared
// types, and an inline constructor would make the compiler emit the
// exception-cleanup path for those members at every construction site -
// which needs the complete types there. Keeping both here confines that to
// lsfg_engine.cpp, where the types are complete.
Engine();
~Engine();
Engine(const Engine&) = delete;
Engine& operator=(const Engine&) = delete;
// Build the shader modules from a cache produced by lsfg_dll::buildCache.
// Returns false if the cache does not yield all 25 modules. `spirvTarget`
// is the highest SPIR-V version the device accepts (lsfg_probe.h); the
// cached 1.6 modules are lowered to it when it is below that.
bool init(VkDevice device, VkPhysicalDevice physicalDevice, const std::string& cachePath,
uint32_t spirvTarget = kSpirv16);
@@ -71,28 +39,18 @@ public:
bool needsRebuild(uint32_t width, uint32_t height, VkFormat format) const;
bool prepare(uint32_t width, uint32_t height, VkFormat format);
// How many frames to generate for this source frame. Returns 0 until the
// history slots are valid and the rates have settled, so the first frames
// after a start or a resize are never interpolated from garbage.
uint32_t plan(uint32_t capacity, uint64_t sourceFrames);
// Take the just-composited frame as LSFG input and run the shared part of
// the chain (everything except `generate`). `source` must be in GENERAL.
void process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height,
uint32_t generations);
// Synthesise generated frame `generation` into the given storage image.
void generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t targetIndex,
VkImage targetImage, VkImageView targetView,
uint32_t width, uint32_t height);
// What the probe governor currently trusts, and the latest thermal
// reading (-1 = no signal). Surfaced for the HUD and diagnostics.
uint32_t acceptedGenerations() const { return governor_.accepted(); }
int thermalStatus() const { return governor_.thermalStatus(); }
float sourceRate() const;
// The rate that actually reaches the panel, generated frames included.
// Supplied by the renderer, which is the only thing that counts presents.
float loopRate() const { return presentedRate_; }
void setPresentedRate(float fps) { presentedRate_ = fps; }
@@ -102,16 +60,8 @@ public:
private:
float effectiveFlowScale(uint32_t width) const;
// A generated frame is a blend of the last two REAL frames, so the input
// ring - which is two images deep - only holds usable history when the
// other slot was filled by the frame immediately before this one. It is
// not, every time generation resumes after a pause: the ring is seeded
// only while generating, so the slot still carries whatever was there when
// generation last stopped.
bool historyFresh() const { return haveCopied_ && lastCopiedCount_ + 1 == frameCount_; }
// With a target rate the pacer plans 0..3 generations frame by frame, so the ring and the
// shared chain have to follow every frame it keeps warm, not just the ones that generate.
bool followEveryFrame() const { return warm_ && pacer_.Config().target_rate != 0; }
Device device_{};
@@ -120,12 +70,6 @@ private:
std::unique_ptr<LsfgChain> chain_;
LsfgPacer pacer_;
ProbeGovernor governor_;
// The governor is OFF by default: the device's own thermal management is
// the authority, and a probe loop that can veto the user's setting is not
// wanted here. The multiplier is then what was asked for, still clamped by
// what the panel can show. Kept behind a flag rather than deleted because
// the probe/backoff logic is the right answer for a device WITHOUT decent
// native throttling, and this is the only thing that would have to change.
bool governorEnabled_ = false;
LsfgPlan plan_{};
@@ -151,4 +95,4 @@ private:
bool unavailable_{};
};
} // namespace lsfg
}
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_gamma.hpp"
#include "lsfg_shaders.hpp"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_generate.hpp"
#include "lsfg_dll.h"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -1,4 +1,3 @@
// See lsfg_governor.h.
#include "lsfg_governor.h"
@@ -12,13 +11,10 @@
namespace lsfg {
namespace {
// A probe has to run long enough to survive a single unlucky frame, and the
// baseline long enough to be a fair comparison.
constexpr float kBaselineSeconds = 2.0f;
constexpr float kProbeSeconds = 2.0f;
constexpr uint32_t kMinSamples = 30;
// Keep the extra generation only if it actually paid off.
constexpr float kOutputGainRequired = 1.15f;
constexpr float kSourceFloor = 0.70f;
@@ -28,20 +24,20 @@ constexpr uint32_t kBackoffSteps = 4;
constexpr float kThermalPollSeconds = 2.0f;
constexpr float kThermalDecaySeconds = 1.0f;
// ADEVICE_THERMAL_STATUS_* from <android/thermal.h>, which is API 30. Resolved
// by dlsym so the build keeps working below that and on devices that do not
// ship the service; -1 simply means "no thermal signal", never "hot".
// Android reports a STATUS, not a temperature - NONE 0, LIGHT 1, MODERATE 2,
// SEVERE 3, CRITICAL 4, EMERGENCY 5, SHUTDOWN 6 - so there is no degree value
// here to raise. The equivalent of "let it run hotter" is to act on a higher
// status, which is what these now do.
//
// A handheld under a real 3D load sits at SEVERE routinely; blocking there
// made the governor refuse on a device that was working exactly as intended.
// It now only stops GROWING at SEVERE and only gives frames back at CRITICAL,
// leaving the device's own thermal management as the authority.
constexpr int kThermalBlock = 4; // CRITICAL: give generations back
constexpr int kThermalNoGrowth = 3; // SEVERE: keep what we have, stop probing
constexpr int kThermalBlock = 4;
constexpr int kThermalNoGrowth = 3;
using PFN_AThermal_acquireManager = void* (*)();
using PFN_AThermal_getCurrentThermalStatus = int (*)(void*);
@@ -72,17 +68,12 @@ float phaseSeconds(std::chrono::steady_clock::time_point a,
return std::chrono::duration<float>(b - a).count();
}
} // namespace
}
void ProbeGovernor::configure(uint32_t maxGenerations) {
if (maxGenerations_ == maxGenerations) return;
// Raising the ceiling must NOT throw away what we already know works -
// going 2x -> 4x used to reset the accepted level to zero and restart the
// whole baseline/probe cycle, so a user raising the multiplier got FEWER
// frames. Only clamp down to a ceiling that has actually been lowered.
maxGenerations_ = maxGenerations;
if (accepted_ > maxGenerations_) accepted_ = maxGenerations_;
// Re-measure against the new ceiling, but keep the accepted level.
phase_ = Phase::Baseline;
havePhaseStart_ = false;
sourceAccum_ = loopAccum_ = 0.0f;
@@ -132,19 +123,8 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate
if (!havePhaseStart_) enterBaseline(now);
pollThermal(now);
// Thermal override: at SEVERE and above we stop asking for more and give
// generations back, but SLOWLY.
//
// This runs once per source frame - about forty times a second - so the
// previous version's unconditional accepted_-- was not "give one back", it
// was "slam to zero in three frames", and its unconditional enterBackoff()
// reset the backoff clock every single call, so the backoff could never
// expire and the governor could never measure or recover. One decay per
// second, and no churn at all once there is nothing left to give back.
thermalBlocked_ = thermal_ >= kThermalBlock;
if (thermal_ >= kThermalBlock) {
// Logged, sparingly. This is a REFUSAL TO EVEN MEASURE, and it was
// silent: on device it looked identical to "measured and declined".
if ((refusalLog_++ % 600u) == 0u)
GOV_LOGI("thermal status %d - not probing (accepted=%u)", thermal_, accepted_);
const bool decayDue = !haveThermalDecay_
@@ -183,12 +163,10 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate
sourceAccum_ = loopAccum_ = 0.0f;
samples_ = 0;
} else {
// Also previously silent. "Baseline completed but we did not
// probe" is a decision worth seeing in a log.
if ((refusalLog_++ % 600u) == 0u)
GOV_LOGI("not probing: accepted=%u max=%u requested=%u thermal=%d",
accepted_, maxGenerations_, requested, thermal_);
enterBaseline(now); // nothing to try; measure again later
enterBaseline(now);
}
}
break;
@@ -226,12 +204,10 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate
break;
}
// While probing we allow one more than is currently accepted; otherwise the
// accepted level is the ceiling, and the pacer's own answer still applies.
const uint32_t allowed = phase_ == Phase::Probing
? std::min(accepted_ + 1, maxGenerations_)
: accepted_;
return std::min(requested, allowed);
}
} // namespace lsfg
}
+1 -35
View File
@@ -1,29 +1,4 @@
#pragma once
// ============================================================================
// lsfg_governor - decides whether an extra generated frame is actually WORTH
// it on this device, right now.
//
// The ported pacer answers "how many frames fit in the panel's budget". That
// is necessary but not sufficient on a handheld: the LSFG chain competes with
// the game for the same GPU, so a naive fixed 3x on a GPU-bound title lowers
// the real frame rate by more than the generated frames add. The user sees a
// higher HUD number and a worse-looking game.
//
// So the multiplier is treated as a ceiling to earn, not a setting to obey.
// The governor starts at 0 generations, periodically PROBES one more, and
// keeps it only if the probe measurably paid off:
//
// * total output rate must improve by at least 1.15x, and
// * the real (source) frame rate must not collapse below 0.70x baseline.
//
// A failed probe reverts immediately and backs off 5s, 15s, 30s, then 60s, so
// a device that cannot afford another generation stops being asked.
//
// Thermals are the second input a phone needs and a desktop does not.
// Sustained frame generation is a thermal decision as much as a performance
// one, so the governor gives back generations as the device heats and refuses
// to probe while it is throttling.
// ============================================================================
#include <chrono>
#include <cstdint>
@@ -34,22 +9,15 @@ class ProbeGovernor {
public:
void configure(uint32_t maxGenerations);
// Cap the pacer's answer. Called once per source frame with the measured
// rates; returns the number of generations actually permitted.
uint32_t cap(uint32_t requested, float sourceRate, float loopRate);
void reset();
uint32_t accepted() const { return accepted_; }
bool probing() const { return phase_ == Phase::Probing; }
// Whether the chain's input ring needs to be kept fed. Seeding it is a
// full-resolution copy every frame, so it is skipped during the long idle
// stretches - a backoff, or a thermal block - and resumed during Baseline,
// which gives it a couple of seconds to fill before a probe can start.
bool wantsHistory() const {
return !thermalBlocked_ && phase_ != Phase::Backoff;
}
// Latest thermal reading, 0 (none) .. 6 (shutdown); -1 when unavailable.
int thermalStatus() const { return thermal_; }
private:
@@ -68,11 +36,9 @@ private:
Clock::time_point phaseStart_{};
bool havePhaseStart_ = false;
// Rolling means over the current phase window.
float sourceAccum_ = 0.0f, loopAccum_ = 0.0f;
uint32_t samples_ = 0;
// What the accepted level achieves, measured during Baseline.
float baselineSource_ = 0.0f, baselineLoop_ = 0.0f;
bool haveBaseline_ = false;
@@ -87,4 +53,4 @@ private:
bool haveThermalPoll_ = false;
};
} // namespace lsfg
}
+1 -6
View File
@@ -1,8 +1,3 @@
// JNI surface for native LSFG shader-cache management.
//
// Deliberately free of any renderer handle: extracting the shader chain from
// Lossless.dll and caching it is a slow, once-per-DLL operation that the app
// runs off the render thread, long before a game launches.
#include "lsfg_dll.h"
@@ -20,7 +15,7 @@ std::string toStdString(JNIEnv* env, jstring s) {
return out;
}
} // namespace
}
extern "C" JNIEXPORT jint JNICALL
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeValidateDll(JNIEnv* env, jclass, jstring dllPath) {
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_mipmaps.hpp"
#include "lsfg_dll.h"
@@ -3,10 +3,6 @@
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -1,9 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_pacer.hpp"
@@ -21,7 +17,6 @@ constexpr float SOURCE_SMOOTHING = 0.15f;
constexpr float SOURCE_STALE_SECONDS = 0.5f;
constexpr float DISCONTINUITY_SECONDS = 0.25f;
constexpr float HEADROOM_EPSILON = 0.02f;
// A target is aimed just under, so rounding never puts output past the panel's refresh rate.
constexpr float TARGET_MARGIN = 0.1f;
constexpr float CREDIT_EPSILON = 1.0e-4f;
constexpr float SOURCE_ACCUM_FLOOR = 0.01f;
@@ -121,17 +116,6 @@ LsfgPlan LsfgPacer::Plan(size_t capacity, uint64_t source_frames) {
if (target_rate > 0.0f) target_rate -= TARGET_MARGIN;
if (target_rate == 0.0f) {
// No explicit target rate: the multiplier is the multiplier, full stop.
// The panel-headroom clamp was here briefly and was removed at the
// user's decision; this is also upstream's behaviour on this branch.
//
// What that means above the panel's refresh rate, for the record: the
// display cannot show more frames than it refreshes, so under FIFO the
// surplus presents queue and the render thread waits on acquire. Nothing
// is displayed faster, and the compositor falls behind on guest
// deliveries instead. Below the refresh rate - every source at or under
// panel/4 - this branch behaves identically with or without the clamp.
// To restore it: limit = std::min(ceiling, HeadroomLimit()).
output_credit = 0.0f;
limit = ceiling;
return LsfgPlan{limit, true};
@@ -1,9 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
+3 -20
View File
@@ -1,4 +1,3 @@
// See lsfg_probe.h. Capability gate for native compositor-side LSFG.
#include "lsfg_probe.h"
#include "../VulkanRendererContext.h"
@@ -16,7 +15,7 @@ bool hasExtension(const std::vector<VkExtensionProperties>& exts, const char* na
return false;
}
} // namespace
}
FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd,
const std::vector<VkExtensionProperties>& deviceExtensions,
@@ -33,33 +32,21 @@ FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd,
const bool below12 = props.apiVersion < VK_API_VERSION_1_2;
if (below12) {
// Below 1.2 there is no VkPhysicalDeviceVulkan12Features to chain, and
// the SPIR-V 1.6 modules will not load anyway. Unless the experimental
// compat path is on AND the driver offers the extensions that make the
// chain loadable as SPIR-V 1.4, don't touch the device further:
// device creation stays exactly as it is today.
if (!allowVk11) return fs;
fs.hasSpirv14 = hasExtension(deviceExtensions, VK_KHR_SPIRV_1_4_EXTENSION_NAME);
fs.hasFloatControls = hasExtension(deviceExtensions, VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME);
fs.hasMemoryModelExt = hasExtension(deviceExtensions, VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME);
if (!fs.hasSpirv14 || !fs.hasFloatControls || !fs.hasMemoryModelExt) {
// Report what was looked at so explain() can name the gap.
fs.queried = true;
return fs;
}
fs.extensionPath = true;
fs.spirvTarget = kSpirv14;
} else if (!fs.apiAtLeast13 && allowVk11) {
// 1.2: SPIR-V 1.5 is core, VulkanMemoryModel is core. Nothing to
// enable beyond the features; the modules only need their header
// lowered from 1.6 to 1.5. Same experimental gate as the 1.1 path so
// a production build keeps rejecting 1.2 exactly as before.
fs.extensionPath = true;
fs.spirvTarget = kSpirv15;
}
// On 1.2+ the Vulkan12Features struct carries the memory model; below
// that the KHR struct does (same sType value, same layout).
VkPhysicalDeviceVulkan12Features v12{};
v12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES;
VkPhysicalDeviceVulkanMemoryModelFeaturesKHR mm{};
@@ -87,7 +74,7 @@ FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd,
std::vector<const char*> extensionNames(const FeatureSupport& f) {
std::vector<const char*> out;
if (!f.extensionPath || f.deviceApiVersion >= VK_API_VERSION_1_2) return out;
out.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); // dependency of spirv_1_4
out.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME);
out.push_back(VK_KHR_SPIRV_1_4_EXTENSION_NAME);
out.push_back(VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME);
return out;
@@ -108,10 +95,6 @@ bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt) {
VkFormatProperties fp{};
vk.GetPhysicalDeviceFormatProperties(pd, fmt, &fp);
// The composite target is COLOR_ATTACHMENT (effect chain writes it),
// SAMPLED (next frame's LSFG input reads it), STORAGE (generate writes it)
// and both blit ends (it is copied - or, below panel resolution, blitted -
// into the swapchain image).
const VkFormatFeatureFlags need =
VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT
| VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT
@@ -163,4 +146,4 @@ void explain(Caps& caps) {
}
}
} // namespace lsfg
}
+7 -59
View File
@@ -1,45 +1,17 @@
#pragma once
// ============================================================================
// lsfg_probe - capability gate for native (compositor-side) LSFG frame
// generation.
//
// The Lossless Scaling chain is 25 compute shaders that DXVK's DXBC translator
// emits as SPIR-V 1.6 with OpCapability VulkanMemoryModel and
// StorageImageWriteWithoutFormat. Three consequences, all checked here:
//
// * SPIR-V 1.6 will not load on a Vulkan 1.1 device, so the DEVICE (not just
// the instance) must report 1.3+ ... OR, on the experimental Vulkan 1.1
// compat path, offer VK_KHR_spirv_1_4 + VK_KHR_vulkan_memory_model so the
// same modules can be handed over as SPIR-V 1.4 (see downgradeSpirv in
// lsfg_dll.h). The stock Qualcomm driver on Adreno 7xx phones reports
// 1.1.128 yet carries every one of those extensions (measured on an
// Adreno 710 / HyperOS device with `cmd gpu vkjson`).
// * vulkanMemoryModel, shaderStorageImageWriteWithoutFormat and
// shaderStorageImageExtendedFormats must be ENABLED at device creation.
// Today the renderer enables no features at all, so all three are off.
// * `generate` writes into a storage image, and Android swapchain formats
// are frequently not storage-capable - so the format is probed separately
// once the swapchain has picked one.
//
// A device failing any gate reports unsupported UP FRONT, with a reason, so
// the UI can grey the engine out instead of failing later inside
// vkCreateShaderModule or vkCreateComputePipelines.
// ============================================================================
#include <vulkan/vulkan.h>
#include <cstdint>
#include <vector>
#include "lsfg_dll.h" // kSpirv14/15/16
#include "lsfg_dll.h"
struct VkTable;
namespace lsfg {
// Which of the three required features the physical device OFFERS. Queried
// before vkCreateDevice; what we actually enable is recorded in Caps below.
struct FeatureSupport {
bool queried = false; // vkGetPhysicalDeviceFeatures2 resolved
bool queried = false;
bool apiAtLeast13 = false;
bool vulkanMemoryModel = false;
bool vulkanMemoryModelDeviceScope= false;
@@ -47,65 +19,41 @@ struct FeatureSupport {
bool storageImageExtendedFormats = false;
uint32_t deviceApiVersion = 0;
// Experimental Vulkan 1.1 compat: the device is below 1.3 but offers the
// extensions that make the chain loadable. When set, the renderer must
// enable those extensions at device creation (see extensionNames) and the
// modules are downgraded to `spirvTarget` at load.
bool extensionPath = false;
bool hasSpirv14 = false; // VK_KHR_spirv_1_4
bool hasFloatControls = false; // VK_KHR_shader_float_controls (spirv_1_4 depends on it)
bool hasMemoryModelExt = false; // VK_KHR_vulkan_memory_model
bool hasSpirv14 = false;
bool hasFloatControls = false;
bool hasMemoryModelExt = false;
uint32_t spirvTarget = kSpirv16;
// Every hard device-level gate passes (format is checked separately).
bool deviceGatesPass() const {
return queried && (apiAtLeast13 || extensionPath) && vulkanMemoryModel
&& storageImageWriteWithoutFormat && storageImageExtendedFormats;
}
};
// What was actually enabled + the running verdict. Owned by the renderer.
struct Caps {
FeatureSupport features;
bool featuresEnabled = false; // the chain was passed to vkCreateDevice
bool featuresEnabled = false;
bool storageOnSwapchainFormat = false;
// The swapchain format blits with VK_FILTER_LINEAR (needs
// SAMPLED_IMAGE_FILTER_LINEAR on the source). Not a gate: without it the
// capture-resolution upscale falls back to NEAREST.
bool linearBlitOnSwapchainFormat = false;
VkFormat probedFormat = VK_FORMAT_UNDEFINED;
char reason[160] = "not probed";
// The single question the UI and the render path ask.
bool supported() const {
return featuresEnabled && features.deviceGatesPass() && storageOnSwapchainFormat;
}
};
// Ask the physical device which of the required features it offers.
// Safe on any driver: if vkGetPhysicalDeviceFeatures2 cannot be resolved, or
// the device reports below Vulkan 1.2 and `allowVk11` is false, nothing is
// chained and `queried` is left false - the caller then creates the device
// exactly as it always has. With `allowVk11`, a 1.1 device is accepted when
// `deviceExtensions` lists VK_KHR_spirv_1_4, VK_KHR_shader_float_controls and
// VK_KHR_vulkan_memory_model, and the memory-model features are queried
// through the KHR struct instead of VkPhysicalDeviceVulkan12Features.
FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd,
const std::vector<VkExtensionProperties>& deviceExtensions,
bool allowVk11);
// The device extensions the extension path needs enabled, in the order they
// should be pushed onto VkDeviceCreateInfo. Empty unless extensionPath.
std::vector<const char*> extensionNames(const FeatureSupport& f);
// Probe VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT on the live swapchain format.
// `generate` writes into an image of this format via a compute dispatch.
bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt);
// Whether the format may be the source of a VK_FILTER_LINEAR blit.
bool probeLinearBlit(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt);
// Fill caps.reason with the FIRST gate that failed (or "supported").
void explain(Caps& caps);
} // namespace lsfg
}
@@ -1,9 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#include "lsfg_shaders.hpp"
#include "lsfg_common.hpp"
@@ -27,12 +23,6 @@ LsfgShaders::LsfgShaders(const Device& device_, const std::string& cache_path,
return;
}
// Vulkan 1.1/1.2 compat: the cache is always built at the translator's
// native SPIR-V 1.6 (no device exists at import time), so lower each module
// to what THIS device accepts. A no-op on 1.3+ devices. Only the
// DXBC-translated chain is lowered: downgradeSpirv knows the capabilities
// DXVK emits, not whatever a precompiled vendor SPIR-V variant may carry,
// and relabelling one of those could hand the driver invalid SPIR-V.
uint32_t lowered = 0;
for (Module& module : set.modules) {
if (module.words.size() > 1 && module.words[1] > spirv_target) {
@@ -1,9 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
// Vulkan dispatch differs here (see lsfg_vkd.h).
#pragma once
@@ -20,8 +16,6 @@ class Device;
class LsfgShaders {
public:
LsfgShaders() = default;
// `spirv_target` is the highest SPIR-V version the device accepts (see
// lsfg_probe.h); cached modules above it are lowered before creation.
LsfgShaders(const Device& device, const std::string& cache_path,
uint32_t spirv_target = kSpirv16);
~LsfgShaders();
@@ -1,4 +1,3 @@
// See lsfg_vkd.h. Fills the chain's dispatch from the renderer's own table.
#include "lsfg_vkd.h"
#include "../VulkanRendererContext.h"
-23
View File
@@ -1,24 +1,4 @@
#pragma once
// ============================================================================
// lsfg_vkd - the Vulkan entry points the LSFG chain calls, as a single global.
//
// Two dispatch styles meet here. DroidDeck's compositor resolves Vulkan
// through a per-context VkTable (`vk_`), because an adrenotools-loaded driver
// lives in an isolated linker namespace and shares no global symbols with the
// system loader. The LSFG chain, ported from Eden by way of WinNative, calls
// through a file-scope `vkd`. Rather than rewrite ~2,300 lines of proven chain
// code to thread a table through every call, this shim exposes exactly the 34
// entry points the chain uses and is filled from the renderer's own table when
// the chain is created.
//
// That keeps the ported files close to their upstream form, which matters:
// they are the part of this feature that is hardest to review by eye and
// easiest to break with a well-meaning edit.
//
// Single-renderer assumption: there is one Vulkan compositor per process, so
// one global table is correct. lsfgVkdInit() is idempotent and simply
// overwrites; lsfgVkdReady() reports whether every pointer resolved.
// ============================================================================
#include <vulkan/vulkan.h>
@@ -63,8 +43,5 @@ struct LsfgVkDispatch {
extern LsfgVkDispatch vkd;
// Fill `vkd` from the renderer's table. Returns false (and leaves the chain
// unusable) if any entry point failed to resolve - better to report the engine
// unsupported than to call through a null pointer mid-frame.
bool lsfgVkdInit(const VkTable& table);
bool lsfgVkdReady();
+93 -155
View File
@@ -1,6 +1,3 @@
// win-fg - runtime config: enable gate, model, multiplier, and the synthesis
// tuning parameters. Sourced from environment variables and an optional
// conf.toml (a config file, when present, wins over the env defaults).
#pragma once
#include <cstdint>
#include <cstdlib>
@@ -12,143 +9,110 @@
namespace winfg {
struct Config {
bool enabled = false; // gate; loader also honours the manifest enable var
// GRANULAR PRESENT-PATH DEBUG TRACE. Default OFF. When ON, winfg_QueuePresentKHR
// emits a per-frame, step-by-step trail (logcat tag "win-fg") so a freeze/
// crash-on-enable leaves an obvious LAST line at the exact stage that hung or
// faulted (acquire-spare, compute-submit, present-generated/real, fence wait,
// fallback). Zero behaviour change and effectively zero cost when off - one
// predicted-not-taken bool test per step, no logcat writes.
// WIN_FG_DEBUG=1|0 / conf.toml debug=on|off.
bool enabled = false;
bool debug = false;
// ── EVEN-CADENCE FRAME PACING ─────────────────────────────────────────────
// Default ON. The 2x insert path presents the GENERATED frame then the REAL
// frame back-to-back in one vkQueuePresentKHR hook call, so on Mailbox/
// Immediate both land in ~one instant followed by a full frame-interval gap
// until the next pair - uneven in time → judder ("slows down to catch up").
// With pacing on, the layer measures the real-frame interval (EMA of a
// monotonic clock) and holds the REAL present to its scheduled beat so the
// generated frame lands near the temporal MIDPOINT between consecutive real
// frames (even gen,real,gen,real cadence). Pure bounded CPU sleep - no GPU
// wait added; if a frame is already behind schedule the wait is SKIPPED
// (present immediately) so worst case is one un-paced pair, never a hitch.
// FIFO self-paces so the waits collapse to ~0 there. Off ⇒ byte-identical
// back-to-back behaviour. WIN_FG_PACING=on|off / conf.toml pacing=on|off.
// ISOLATION BUILD: default flipped to OFF to A/B the AYANEO-750 post-setting-change
// flicker regression against the pacing feature (everything else byte-identical).
bool pacing = false;
int model = 4; // 3 = symmetric flow, 4 = bidir + occlusion gate
int multiplier = 2; // generated presents per real present + 1
// EXTRA SWAPCHAIN IMAGE HEADROOM (device-freeze fix). On top of the +1 spare the
// insert path needs, request this many ADDITIONAL images at swapchain creation:
// requested minImageCount = app_min + 1 (spare) + extra_images,
// clamped to surfaceCaps.maxImageCount (0 = unlimited -> no clamp), never below
// app_min+1. The extra headroom stops the guest's NEXT vkAcquireNextImageKHR from
// starving while win-fg holds a spare + a real frame in flight to the AHB-backed
// host compositor. Some SoCs (Adreno 840 / Turnip) recycle displayed AHBs too
// slowly to tolerate the extra present with only the +1 spare and freeze on the
// first inserted frame; the AYANEO (Adreno 750) recycles fast enough to tolerate
// it. Default 2 (a few MB VRAM), applied on all devices. On a device that already
// works this only grows the pool - no FPS/quality change; the pool-headroom guard
// in QueuePresentKHR stays a no-op when headroom is adequate.
// WIN_FG_EXTRA_IMAGES / conf.toml extra_images.
int model = 4;
int multiplier = 2;
int extraImages = 2;
// ── FLOW-RESOLUTION PERFORMANCE PRESET ────────────────────────────────────
// Trades base-FPS-drop against optical-flow quality by selecting the FINEST
// pyramid level the dense flow is solved at (FrameGen::kFlowFinest):
// 0 = Quality -> kFlowFinest = 1 (~1/2-res flow; best motion, biggest FG cost)
// 1 = Balanced -> kFlowFinest = 2 (~1/4-res; TODAY's behaviour) [DEFAULT]
// 2 = Performance -> kFlowFinest = 3 (~1/8-res; cheapest -> smallest base-FPS drop)
// Out-of-range clamps to Balanced (1), so an unset/older conf.toml is byte-
// identical to today. Changing this LIVE rebuilds the affected per-size flow
// images (like a resize) inside the layer and resets the flow predictor, so a
// preset change is fully hot - no FG toggle needed. See FrameGen::configure /
// FrameGen::flowFinestForPreset. WIN_FG_PERF_PRESET / conf.toml perf_preset.
int perfPreset = 1; // 0 quality, 1 balanced (default), 2 performance
float flowScale = 1.0f; // scales solved flow magnitude
// C1 GLOBAL-MOTION PRE-WARP. Estimate the per-frame camera affine (LK) and
// remove it before the dense SAD flow search so the search only sees coherent
// object-only residual motion (kills fast-pan "melt"). 0 = auto (engage only
// when the LK solve is stable), 1 = on (force-engage once a solve exists),
// 2 = off (never pre-warp; identity affine → pipeline == pre-C1 exactly).
// WIN_FG_GM=auto|on|off / conf.toml global_motion=auto|on|off.
int gmMode = 0; // 0 auto, 1 on, 2 off
// C2 FLOW REGULARIZATION (TV-L1 smoothness prior). After C1 removes the camera
// motion, the dense flow (flowLvl_ at kFlowFinest, 1/4-res) is the OBJECT-only
// residual; it still carries spurious per-block vectors. C2 runs N semi-implicit
// TV-L1 denoising iterations on that field - an L1 data term + edge-aware Total-
// Variation regularizer - to kill incoherent vectors while KEEPING true motion
// discontinuities (object edges) sharp. Solved in-place; expand's median + C1
// add-back consume the cleaned field unchanged. 0 iterations ⇒ byte-identical to
// pre-C2. See shaders/of3_flowreg.comp.
// WIN_FG_FLOWREG=auto|on|off / conf.toml flow_reg=auto|on|off.
int frMode = 0; // 0 auto (engaged), 1 on (engaged), 2 off
int frIters = 4; // TV-L1 iterations at kFlowFinest (0 ⇒ off)
float frLambda = 2.0f; // L1 data-fidelity weight (higher ⇒ trust SAD flow)
float frDt = 0.25f; // semi-implicit smoothing step (higher ⇒ smoother)
float frEdge = 8.0f; // luma-gradient edge sensitivity for the TV weight g
float frEps = 0.05f; // Charbonnier epsilon (px) for the TV diffusivity
// synthesis (wfg_synth) tuning
float beta = 8.0f; // softmax sharpness on importance Z
float lambda = 0.6f; // FB-consistency vs photometric weight
// Tightened 2026-08-17 to prioritise "no ghost" over max sharpness - the
// remaining single-frame ghosts came from pixels that the previous defaults
// still trusted with warp when they should have cross-faded. Raising both
// pushes borderline pixels toward the safe cross-fade path (softer, but no
// smear). Prewarp will lift the ceiling further; this is the "safety net"
// knob change until then.
// Walked back from (0.10, 9.0) tighten after user reported softening. The
// real fix for the residual ghosts is the 3a-fallback path change in
// layer.cpp (present real curr on fallback instead of blitting synth over);
// the gate can stay closer to the original values now.
float epsilon = 0.07f; // disocclusion floor (0.05 -> 0.10 -> 0.07)
float photoScale = 7.5f; // photometric residual scale into Z (6.0 -> 9.0 -> 7.5)
// HUD exclusion rect in pixel coords (x0,y0,x1,y1). Fragments inside are
// passed through as the real current frame (no warp, no synth) so text /
// overlays don't ghost. Disabled when x0 >= x1 or y0 >= y1 (the default).
// Sourced from WIN_FG_HUD_RECT="x0,y0,x1,y1" env var or conf.toml
// (hudRect="x0,y0,x1,y1"). Pattern is Isygold's Vegas DXVK framegen -
// host knows where the HUD is, tell the shader to skip it.
int perfPreset = 1;
float flowScale = 1.0f;
int gmMode = 0;
int frMode = 0;
int frIters = 4;
float frLambda = 2.0f;
float frDt = 0.25f;
float frEdge = 8.0f;
float frEps = 0.05f;
float beta = 8.0f;
float lambda = 0.6f;
float epsilon = 0.07f;
float photoScale = 7.5f;
float hudX0 = 0.0f;
float hudY0 = 0.0f;
float hudX1 = 0.0f;
float hudY1 = 0.0f;
// ── TRAINING-DATA CAPTURE MODE (Route-B VFI dataset collection) ───────────
// A dev-only gate. When OFF (default) the present path is byte-identical to a
// build without this feature - a single bool test and nothing else. When ON,
// every real present is downscaled on the GPU, read back async, and written to
// disk as lossless QOI (motion-rich 256² triplet patches by default, or full
// downscaled frames) plus a JSONL manifest, for offline (i-1,i+1)->i training.
// NEVER captures win-fg's generated frames - capture reads the real swapchain
// image at the TOP of QueuePresentKHR, before any interpolation.
// WIN_FG_CAPTURE=on|off / conf.toml capture=on|off.
bool capture = false;
// Downscale target box (aspect-preserving, never upscales). WIN_FG_CAPTURE_W/H,
// conf capture_width/capture_height.
int captureW = 1280;
int captureH = 720;
// 0 = patch (self-contained aligned triplets, DEFAULT), 1 = frame (full
// downscaled frames in order). WIN_FG_CAPTURE_MODE=patch|frame.
int capMode = 0;
int capPatches = 3; // patches per triplet in patch mode (WIN_FG_CAPTURE_PATCHES)
int capPatchSize = 256; // patch edge in downscaled px (WIN_FG_CAPTURE_PATCH)
// Skip a unit whose inter-frame motion (mean per-pixel luma abs-diff, 0..255)
// is below this. WIN_FG_CAPTURE_MOTION / conf capture_motion.
int capPatches = 3;
int capPatchSize = 256;
float capMotion = 2.0f;
// Output dir; empty => $HOME/.cache/winfg-capture. WIN_FG_CAPTURE_DIR /
// conf capture_dir. A per-run session subdir is created under it.
std::string capDir;
// Rolling-shard size cap (MiB). Output is a small number of large packed
// .wfgcap containers, not thousands of loose files; a new shard opens when the
// current one passes this. WIN_FG_CAPTURE_SHARD_MB / conf capture_shard_mb.
int capShardMB = 1024;
void sanitize() {
if (model < 3) model = 3; if (model > 4) model = 4;
if (multiplier < 2) multiplier = 2; if (multiplier > 4) multiplier = 4;
if (extraImages < 0) extraImages = 0; if (extraImages > 8) extraImages = 8;
if (perfPreset < 0 || perfPreset > 2) perfPreset = 1; // clamp out-of-range -> Balanced
if (perfPreset < 0 || perfPreset > 2) perfPreset = 1;
if (gmMode < 0) gmMode = 0; if (gmMode > 2) gmMode = 2;
if (frMode < 0) frMode = 0; if (frMode > 2) frMode = 2;
if (frIters < 0) frIters = 0; if (frIters > 16) frIters = 16;
@@ -158,7 +122,6 @@ struct Config {
if (frEps < 1e-3f) frEps = 1e-3f;
if (flowScale < 0.05f) flowScale = 0.05f; if (flowScale > 4.0f) flowScale = 4.0f;
if (beta < 0.0f) beta = 0.0f; if (lambda < 0.0f) lambda = 0.0f;
// capture knobs
if (captureW < 16) captureW = 16; if (captureW > 7680) captureW = 7680;
if (captureH < 16) captureH = 16; if (captureH > 4320) captureH = 4320;
if (capMode < 0) capMode = 0; if (capMode > 1) capMode = 1;
@@ -176,7 +139,6 @@ static inline int envi(const char* k, int d) {
const char* v = std::getenv(k); return v ? std::atoi(v) : d;
}
// "auto"/"on"/"off" (also true/false/on/off numerics) -> {0 auto, 1 on, 2 off}.
static inline int parse_tristate(const std::string& v, int dflt) {
if (v.empty()) return dflt;
if (v == "auto") return 0;
@@ -185,19 +147,16 @@ static inline int parse_tristate(const std::string& v, int dflt) {
return dflt;
}
// "on"/"off"/"1"/"true"/... -> bool.
static inline bool parse_bool(const std::string& v, bool dflt) {
int t = parse_tristate(v, dflt ? 1 : 2);
return t == 1;
}
// "patch"|"frame" -> {0 patch, 1 frame}.
static inline int parse_capmode(const std::string& v, int dflt) {
if (v == "patch" || v == "patches" || v == "0") return 0;
if (v == "frame" || v == "frames" || v == "1") return 1;
return dflt;
}
// key=value TOML-lite reader (flat keys, '#' comments) - enough for our knobs.
static inline void apply_toml(Config& c, const std::string& path) {
std::ifstream f(path);
if (!f) return;
@@ -229,7 +188,6 @@ static inline void apply_toml(Config& c, const std::string& path) {
else if (k == "lambda") c.lambda = std::strtof(v.c_str(), nullptr);
else if (k == "epsilon") c.epsilon = std::strtof(v.c_str(), nullptr);
else if (k == "photoScale") c.photoScale = std::strtof(v.c_str(), nullptr);
// ── capture-mode knobs ────────────────────────────────────────────────
else if (k == "capture") c.capture = parse_bool(v, c.capture);
else if (k == "capture_dir") c.capDir = v;
else if (k == "capture_width") c.captureW = std::atoi(v.c_str());
@@ -240,7 +198,6 @@ static inline void apply_toml(Config& c, const std::string& path) {
else if (k == "capture_motion") c.capMotion = std::strtof(v.c_str(), nullptr);
else if (k == "capture_shard_mb") c.capShardMB = std::atoi(v.c_str());
else if (k == "hudRect") {
// "x0,y0,x1,y1" in pixel coords
float r[4] = {0,0,0,0}; int n = 0;
size_t start = 0; std::string s = v;
for (; n < 4; ++n) {
@@ -255,22 +212,18 @@ static inline void apply_toml(Config& c, const std::string& path) {
}
}
// Path of the conf.toml the app writes (used for hot-reload mtime checks).
static inline std::string conf_path() {
if (const char* p = std::getenv("WIN_FG_CONF")) return p;
const char* home = std::getenv("HOME");
return home ? std::string(home) + "/.config/win-fg/conf.toml" : std::string();
}
// Resolve the capture output root: explicit override, else $HOME/.cache/winfg-capture,
// else a /tmp fallback so a HOME-less prefix still writes somewhere pullable.
static inline std::string capture_root(const Config& c) {
if (!c.capDir.empty()) return c.capDir;
if (const char* home = std::getenv("HOME")) return std::string(home) + "/.cache/winfg-capture";
return std::string("/tmp/winfg-capture");
}
// env defaults first, then conf.toml overrides (file wins when present).
static inline Config load_config() {
Config c;
c.enabled = envi("WIN_FG_ENABLE", 0) != 0;
@@ -292,7 +245,6 @@ static inline Config load_config() {
c.lambda = envf("WIN_FG_LAMBDA", c.lambda);
c.epsilon = envf("WIN_FG_EPSILON", c.epsilon);
c.photoScale = envf("WIN_FG_PHOTOSCALE", c.photoScale);
// ── capture-mode env (conf.toml still wins below) ─────────────────────────
if (const char* cc = std::getenv("WIN_FG_CAPTURE")) c.capture = parse_bool(cc, c.capture);
c.captureW = envi("WIN_FG_CAPTURE_W", c.captureW);
c.captureH = envi("WIN_FG_CAPTURE_H", c.captureH);
@@ -303,7 +255,6 @@ static inline Config load_config() {
c.capShardMB = envi("WIN_FG_CAPTURE_SHARD_MB", c.capShardMB);
if (const char* cd = std::getenv("WIN_FG_CAPTURE_DIR")) c.capDir = cd;
if (const char* h = std::getenv("WIN_FG_HUD_RECT")) {
// "x0,y0,x1,y1" - pixel coords; disabled when x0>=x1 or y0>=y1
float r[4] = {0,0,0,0}; int n = 0;
std::string s = h; size_t start = 0;
for (; n < 4; ++n) {
@@ -321,37 +272,24 @@ static inline Config load_config() {
return c;
}
// UBO layout - MUST match wfg_synth.comp binding 0.
// The vec4 hudRect is 16-byte aligned in std140, sits at offset 32 (after the
// 8 leading floats which naturally occupy 32B). Disabled when hudX0>=hudX1.
struct SynthUBO {
float flowScale, alpha, beta, lambda, epsilon, photoScale, pad0, pad1;
float hudX0, hudY0, hudX1, hudY1;
};
// UBO layout - MUST match of3_flow / of3_expand_m4 binding 0 (Model3UBO).
struct FlowUBO { float flowScale; uint32_t level; float occlLo; float occlHi; };
// C1 global-motion UBO - MUST match binding 1 in of3_gm_reduce / of3_gm_prewarp /
// of3_expand(_m4). std140: four vec4 = 64 bytes. The affine is in centered-UV
// space: P = [[p1,p3],[p2,p4]], translation (tx,ty); warp uvSrc = uv + P*(uv-0.5)
// + t maps a prev(template) coord to the curr(image) coord it samples (prev→curr).
// lin[] = running LK estimate (linearization point fed to of3_gm_reduce)
// app[] = APPLIED affine (of3_gm_prewarp + expand add-back); identity when
// disengaged so the pipeline is byte-identical to pre-C1.
struct GMUBO {
float lin[4]; // p1,p2,p3,p4
float linT[4]; // tx,ty, engaged, pad
float app[4]; // p1,p2,p3,p4
float appT[4]; // tx,ty, pad, pad
float lin[4];
float linT[4];
float app[4];
float appT[4];
};
// C2 flow-regularization UBO - MUST match binding 0 (vec4 params) in
// of3_flowreg.comp. std140: one vec4 = 16 bytes. See Config::fr* fields.
struct FlowRegUBO {
float dt; // params.x - semi-implicit smoothing step
float lambda; // params.y - L1 data-fidelity weight (θ = dt·lambda)
float edgeAlpha; // params.z - luma-gradient edge sensitivity for g
float epsTV; // params.w - Charbonnier epsilon (px) for the TV diffusivity
float dt;
float lambda;
float edgeAlpha;
float epsTV;
};
} // namespace winfg
}
@@ -1,5 +1,3 @@
// GENERATED by tools/embed_spv.py - do not edit by hand.
// win-fg embedded SPIR-V: our optical-flow front end + synthesis only.
#pragma once
#include <cstdint>
#include <cstddef>
@@ -41,4 +39,4 @@ static inline Blob blob(Shader s) {
default: return { nullptr, 0 };
}
}
} } // namespace winfg::embedded
} }
Loaded 100 of 1152 files, more files were not shown because too many files have changed in this diff. Show more