Compare commits

...
Author SHA1 Message Date
The412Banner 911ad29004 Progress log: 2026-09-25 2026-09-25 17:29:12 -04:00
The412Banner 8cdedbe18a Merge pull request #22 from Droid-Deck/feat/steam-touch
Steam client: real touch in Big Picture, and a touchpad cursor that hovers and clicks
2026-09-25 17:06:19 -04:00
The412Banner 0f39d71f95 Merge remote-tracking branch 'org/main' into feat/steam-touch 2026-09-25 16:58:18 -04:00
The412Banner 1dc49b527b gamescope: stage 3.16.29-p3 (0111, pointer warps in Passthrough) 2026-09-25 16:38:50 -04:00
The412Banner cc3fec0768 gamescope: the nested pointer moves Steam's pointer in Passthrough too
In touchpad mode the cursor moved but Big Picture highlighted nothing and a tap-to-click
selected nothing: the nested pointer's motion reaches wlserver as touch 0, and in Passthrough
(the touch mode Big Picture sets) a motion for a touch that is not down moves nothing, so the X
pointer stayed where it was. 0111 passes bAlwaysWarpCursor, as the other touch modes already warp.
2026-09-25 16:34:22 -04:00
Kurt Himebauch 5270574e5a Add native Decky Loader support (#16)
* Add native Decky Loader management

* Enable Decky Steam frontend injection

* Disable unavailable stable Decky channel

* Decky: tie CEF debugging to session setting

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The client reads /sys/class/power_supply/BAT<n>/; Android's supply is called battery and laid
out differently, so the Quick Access Menu had no battery. BatteryComponent writes BAT0 and BAT1
from Android's battery API every five seconds - status, percentage, charge, energy, power and
time to empty - into a directory bound over /sys/class/power_supply.
2026-09-23 09:11:54 -04:00
Kurt Himebauch a4e59986a8 Merge pull request #8 from The412Banner/chore/cleanup
chore: simplify UI copy and repository text
2026-09-23 08:32:51 -04:00
xXJsonDeruloXx 93b4ec47d1 docs: move progress log under docs 2026-09-23 08:27:09 -04:00
xXJsonDeruloXx a4b1d12f36 Merge remote-tracking branch 'origin/main' into chore/cleanup
# Conflicts:
#	README.md
2026-09-23 08:25:15 -04:00
xXJsonDeruloXx 217485e021 chore: remove lineage audit document 2026-09-23 08:23:55 -04:00
github-actions[bot] 1f41d6d282 README: contributions ledger 2026-09-23 12:23:14 +00:00
xXJsonDeruloXx 6876a8d649 chore: remove generated contributor ledger 2026-09-23 08:22:53 -04:00
xXJsonDeruloXx 02c159c5fd chore: simplify UI copy and repo text 2026-09-23 08:22:11 -04:00
github-actions[bot] a1e204c117 README: contributions ledger 2026-09-23 11:09:36 +00:00
The412Banner ece9f7cd46 docs: lineage audit — inherited from Winlator vs written fresh in the forks; the AAudio sink is the one inherited piece that does real work 2026-09-22 23:58:25 -04:00
The412Banner 16dab991e0 docs: the lineage audit — what is measurably Winlator's, what only carries the name, and what leaving it would take 2026-09-22 23:54:07 -04:00
github-actions[bot] c17975943a README: contributions ledger 2026-09-23 03:39:33 +00:00
Kurt Himebauch 9d9ee98eaf Merge pull request #6 from The412Banner/steam-qam-buttons 2026-09-22 23:39:23 -04:00
The412Banner b0bcac6926 PROGRESS_LOG: after the 0.1.5 cut, and what comes next 2026-09-22 23:32:34 -04:00
xXJsonDeruloXx 3b7ad53994 Start drawer QAM chord on button press 2026-09-22 23:26:13 -04:00
The412Banner 0fedfae1e6 PROGRESS_LOG: 0.1.5 released 2026-09-22 23:18:08 -04:00
The412Banner 3fa2a35c12 0.1.5 notes 2026-09-22 23:17:28 -04:00
xXJsonDeruloXx a39859a39e Move Steam shortcuts to the drawer header 2026-09-22 23:17:14 -04:00
The412Banner d2f4a84314 0.1.5 2026-09-22 23:13:14 -04:00
xXJsonDeruloXx 0959eacf39 Add QAM shortcut to the session drawer 2026-09-22 23:10:44 -04:00
github-actions[bot] 78c4b3b264 README: contributions ledger 2026-09-23 03:06:58 +00:00
xXJsonDeruloXx 9f4a841eb6 Add Steam and QAM touch controls 2026-09-22 23:05:29 -04:00
The412Banner 9b034fcbb2 Merge remote-tracking branch 'origin/main' into feat/frontend-motion 2026-09-22 23:02:38 -04:00
The412Banner 81cf964168 front end: every category starts folded when the app opens 2026-09-22 22:55:38 -04:00
The412Banner b5158210a9 themes: every screen colour comes from the palette; Setup › Theme switches at once 2026-09-22 22:49:21 -04:00
github-actions[bot] d1602710a8 README: contributions ledger 2026-09-23 02:44:16 +00:00
The412Banner ec1453831d themes: the icon's colours three ways, chosen under Setup › Theme, Paper on black by default
Palette (ui/Theme.kt): background, surfaces, lines, type, a primary with its gradient partner,
and a separate signal colour for rules, rings, dots and the eyebrow - the blue in every theme,
while the primary is white in Paper and blue in the other two. Every colour the screens used to
hard-code (the purple gradient end, the green, the two line greys) now comes from the palette,
so a change under Setup applies at once, and the session drawer and the file manager pick the
saved theme up when they open.
2026-09-22 22:43:55 -04:00
github-actions[bot] c63af629ac README: contributions ledger 2026-09-23 02:43:13 +00:00
Kurt Himebauch 916b1d46fc Merge pull request #5 from The412Banner/build/local-build-deploy 2026-09-22 22:42:59 -04:00
github-actions[bot] 7df694fe49 README: contributions ledger 2026-09-23 02:32:10 +00:00
xXJsonDeruloXx 071289b56e Add reproducible local build and deploy helpers 2026-09-22 22:31:10 -04:00
The412Banner eca355016c logo: the app's in-app mark is the new launcher icon (its two layers composed and cut to the launcher's circle), in the rail and the loading screen 2026-09-22 22:26:14 -04:00
github-actions[bot] 0053d12407 README: contributions ledger 2026-09-23 02:16:54 +00:00
Kurt Himebauch 4d6ee985b9 Merge pull request #4 from The412Banner/update-app-icon 2026-09-22 22:16:47 -04:00
github-actions[bot] 94d74d35db README: contributions ledger 2026-09-23 02:15:33 +00:00
xXJsonDeruloXx d6a157578d Update app launcher icon 2026-09-22 22:15:06 -04:00
The412Banner 0b9f3106fe gitignore: the Kotlin compiler's session files 2026-09-22 21:54:33 -04:00
The412Banner 17c75bf671 PROGRESS_LOG: pull-request CI and the contributions ledger 2026-09-22 21:46:47 -04:00
github-actions[bot] d6a9c2ed90 README: contributions ledger 2026-09-23 01:44:05 +00:00
Kurt Himebauch 9931dc13a7 Merge pull request #3 from The412Banner/fix/session-back-key-routing 2026-09-22 21:43:54 -04:00
github-actions[bot] 4a3d5a7d66 README: contributions ledger 2026-09-23 01:40:36 +00:00
xXJsonDeruloXx ac2cfc1266 Fix session hardware back routing 2026-09-22 21:39:54 -04:00
310 changed files with 22797 additions and 8174 deletions

No files matched your search

-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# Regenerates the contributions table in README.md, between its markers, from the repository's
# pull requests: one row per pull request by anyone but the owner, and a line of totals per
# contributor. Run by the Contributions ledger workflow when a pull request is opened, merged or
# closed, and by hand with: gh auth status && .github/scripts/contributions.sh OWNER/REPO
set -euo pipefail
REPO=${1:-The412Banner/SteamDeck}
OWNER=${REPO%%/*}
ROWS=$(gh api "repos/$REPO/pulls?state=all&per_page=100" --paginate \
--jq '.[] | select(.user.login != "'"$OWNER"'")
| [.user.login, .number, (.title | gsub("\\|"; "\\\\|")), (.created_at[0:10]),
(if .merged_at then "merged " + .merged_at[0:10] elif .state == "closed" then "closed" elif .draft then "draft" else "open" end),
.html_url] | @tsv' | sort -t"$(printf '\t')" -k1,1 -k2,2n)
{
echo "<!-- contributions:start -->"
echo "_From the repository's pull requests; rewritten when one is opened, merged or closed._"
echo
echo "| Contributor | Pull request | Opened | State |"
echo "|---|---|---|---|"
if [ -z "$ROWS" ]; then
echo "| — | none yet | | |"
else
while IFS=$'\t' read -r login num title opened state url; do
echo "| [@$login](https://github.com/$login) | [#$num]($url) $title | $opened | $state |"
done <<< "$ROWS"
echo
printf '%s\n' "$ROWS" | awk -F'\t' 'NF { t[$1]++; if ($5 ~ /^merged/) m[$1]++ } END { for (l in t) printf("- **@%s**: %d submitted, %d merged\n", l, t[l], m[l] + 0) }' | sort
fi
echo "<!-- contributions:end -->"
} > /tmp/contributions.md
python3 - <<'PY'
import re
s = open('README.md').read()
new = open('/tmp/contributions.md').read().rstrip('\n')
out, n = re.subn(r'<!-- contributions:start -->.*?<!-- contributions:end -->', lambda m: new, s, flags=re.S)
if n != 1:
raise SystemExit("README.md has no contributions markers")
open('README.md', 'w').write(out)
PY
echo "README.md: contributions table rewritten"
+31
View File
@@ -0,0 +1,31 @@
name: Build AAudio sink module
# A quick check of tools/aaudio-sink on its own; the APK build compiles the same module into the
# audio bundle. Runs on pushes that touch the module.
on:
push:
paths:
- 'tools/aaudio-sink/**'
- '.github/workflows/build-aaudio-sink.yml'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: PulseAudio 13.0 source
run: |
set -euo pipefail
curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1
- name: Build
run: |
set -euo pipefail
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/out"
- uses: actions/upload-artifact@v4
with:
name: audio-sink-modules
path: out/*.so
+45
View File
@@ -0,0 +1,45 @@
name: Build gamescope (patched)
# The runtime's own gamescope (Arch Linux ARM's 3.16.29 package) rebuilt with the patches under
# tools/gamescope/patches, in an Arch Linux ARM container on an arm64 runner, then published as a
# release asset the apk build fetches (tools/gamescope/release.env names the tag and sha). The
# hosted runtime image is never changed: the apk stages the binary over /usr/local/bin.
on:
push:
paths:
- 'tools/gamescope/**'
- '.github/workflows/build-gamescope.yml'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish as (e.g. gamescope-3.16.29-p1); empty = artifact only'
required: false
default: ''
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-24.04-arm
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Build in Arch Linux ARM
run: |
set -euo pipefail
docker run --rm -v "$PWD:/work" -w /work menci/archlinuxarm:base-devel bash tools/gamescope/build-in-arch.sh
sudo chown -R "$(id -u):$(id -g)" .
- uses: actions/upload-artifact@v4
with:
name: gamescope-patched
path: |
gamescope.tzst
gamescope.tzst.sha256
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release create "${{ inputs.tag }}" --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
+46
View File
@@ -0,0 +1,46 @@
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 }}" --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
+147 -103
View File
@@ -1,30 +1,14 @@
name: Build APK
# One job: the two glibc preload libraries the session needs, then the app around them.
# Nothing is built on a developer machine — this workflow is the only thing that produces an apk.
#
# This repo is private, so every minute here is billed. Three caches carry the cost of a rebuild
# down to what actually changed:
# • Gradle's own (dependencies, wrapper, build cache, configuration cache) via setup-gradle
# • the CMake/ninja tree, so a Kotlin-only change does not recompile the compositor
# • the two cross-compiled preloads, which also skips installing the cross toolchain
# A run that changes only Kotlin should not touch the C compiler at all.
run-name: ${{ github.event_name == 'pull_request' && format('building pr {0} on {1}', github.event.pull_request.number, github.event.pull_request.head.sha) || format('building {0} on {1}', github.ref_name, github.sha) }}
on:
push:
branches: [ main ]
# Documentation changes cannot alter the apk, so they do not buy a build.
paths-ignore:
- '**/*.md'
- 'LICENSE'
- '.gitignore'
# A pull request against main builds by itself: the contributor's branch, as it would be once
# merged onto main, comes back as an apk on the run without anyone here pressing a button.
# Every push to the pull request rebuilds it and cancels the build it replaces; a draft waits
# until it is marked ready. pull_request_target rather than pull_request: GitHub does not run
# the latter at all while a pull request conflicts with main, and saying what conflicts is the
# point. The workflow file is always main's; the pull request's own code is checked out below.
pull_request_target:
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, ready_for_review ]
paths-ignore:
@@ -35,38 +19,38 @@ on:
permissions:
contents: read
# The pull-request report below is one comment on the pull request, kept up to date.
pull-requests: write
# A newer push makes the running build pointless; stop paying for it.
concurrency:
group: build-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
# Bump to force the preload cache to miss (e.g. after changing a compiler flag below).
PRELOAD_CACHE_VERSION: v1
jobs:
build:
if: ${{ github.event_name != 'pull_request_target' || !github.event.pull_request.draft }}
name: Build APK
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
# A pull request is checked out at its own head with full history, not at GitHub's merge
# ref: that ref does not exist while the pull request conflicts with main, and the point of
# the next step is to say so and show where.
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
# Full history: the merge below needs it (a shallow clone refuses to merge at all).
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
# The pull request as it would be once merged onto main is what gets built. When it cannot
# be merged, the conflicting files and the conflicting hunks go into the run summary and the
# pull-request comment, and the job stops here: there is nothing coherent to build.
- name: No private key in the tree
run: |
set -euo pipefail
keys=$(git ls-files | grep -iE '\.(jks|keystore|p12|pfx|pk8|pem|key)$' | grep -vx 'keystore/testkey.p12' || true)
if [ -n "$keys" ]; then
echo "::error::a private key is committed - remove it from the history and make a new one: $keys"
exit 1
fi
- name: Merge onto main, or say what conflicts
if: github.event_name == 'pull_request_target'
if: github.event_name == 'pull_request'
run: |
set -uo pipefail
git config user.name "ci"
@@ -76,7 +60,7 @@ jobs:
HEAD=$(git rev-parse --short HEAD)
if git merge --no-ff --no-edit origin/main >/tmp/merge.out 2>&1; then
{
echo "### ✅ Mergeable"
echo "### Mergeable"
echo
echo "\`$HEAD\` merges cleanly onto main (\`$BASE\`). What follows was built from the merged result."
} > /tmp/pr-merge.md
@@ -85,9 +69,8 @@ jobs:
fi
FILES=$(git diff --name-only --diff-filter=U)
if [ -z "$FILES" ]; then
# The merge did not even start (git said why): quote it rather than list nothing.
{
echo "### ❌ Could not merge onto main (\`$BASE\`)"
echo "### ERROR: Could not merge onto main (\`$BASE\`)"
echo
echo '```'
cat /tmp/merge.out
@@ -98,7 +81,7 @@ jobs:
exit 1
fi
{
echo "### ❌ Not mergeable — conflicts with main (\`$BASE\`)"
echo "### ERROR: Not mergeable - conflicts with main (\`$BASE\`)"
echo
echo "Files that conflict:"
echo
@@ -127,8 +110,6 @@ jobs:
distribution: temurin
java-version: '17'
# Caches ~/.gradle (dependencies, wrapper distribution, build cache, configuration cache)
# and prunes what the run did not use, so the entry cannot grow without bound.
- uses: gradle/actions/setup-gradle@v4
with:
cache-cleanup: on-success
@@ -137,32 +118,47 @@ jobs:
run: |
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
VERSION=$(sed -n 's/^Pkg.Revision *= *//p' "$NDK/source.properties")
echo "NDK $VERSION at $NDK"
echo "NDK_VERSION=$VERSION" >> "$GITHUB_ENV"
# ── The session's preloads ───────────────────────────────────────────────────────────
# Both are glibc aarch64 libraries that live inside the rootfs, so they are cross-compiled
# here rather than by the NDK. They ship in assets (not in the runtime tarball) so a fix in
# either reaches an already-installed runtime on the next launch instead of waiting on a
# ~790 MB runtime re-host. Their inputs change rarely, so most runs restore them.
- name: Set the APK build label
id: build-label
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BUILD_REF: ${{ github.ref_name }}
BUILD_SHA: ${{ github.sha }}
run: |
if [[ -n "$(git status --porcelain)" ]]; then
tree_state=dirty
else
tree_state=clean
fi
echo "DROIDDECK_BUILD_TREE_STATE=$tree_state" >> "$GITHUB_ENV"
if [[ -n "$PR_NUMBER" ]]; then
label="pr${PR_NUMBER} at ${PR_HEAD_SHA:0:7}"
else
label="${BUILD_REF} at ${BUILD_SHA:0:7}"
fi
echo "label=$label" >> "$GITHUB_OUTPUT"
echo "Build: \`$label\`" >> "$GITHUB_STEP_SUMMARY"
- name: Cache the preloads
id: preloads
uses: actions/cache@v4
with:
path: app/src/main/assets/linuxfs
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/desktop/*') }}
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/mangoapp/*') }}
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
if: steps.preloads.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu >/dev/null
D=app/src/main/assets/linuxfs
mkdir -p "$D"
# Statically linked C++ runtime: this is preloaded into every Steam process, and Steam
# ships its own libstdc++ while rewriting LD_LIBRARY_PATH. Pulling the system one in
# ahead of it is a version conflict waiting to happen. Only libc is left as a dependency.
aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
-pthread -std=c++17 -static-libstdc++ -static-libgcc \
-o "$D/libfakeinput.so" app/src/main/cpp/fakeinput_steam.cpp -ldl
@@ -170,33 +166,106 @@ jobs:
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
-o "$D/libblsession.so" tools/linuxfs/preload/*.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$D/libblsession.so"
# The session's scripts travel the same way, for the same reason.
for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
done
# And the desktop's launcher and labwc defaults, staged over the desktop package's copies.
install -Dm644 tools/linuxfs/desktop/steamdeck-desktop "$D/usr/local/bin/steamdeck-desktop"
for f in tools/linuxfs/overlay/usr/bin/* tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/*; do
[ -f "$f" ] && install -Dm644 "$f" "$D/${f#tools/linuxfs/overlay/}"
done
if [ -f tools/gamescope/release.env ]; then
. tools/gamescope/release.env
gh release download "${GAMESCOPE_TAG}" -R "${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
M="$D/usr/local/lib/mangoapp"
mkdir -p "$M" mango-pkgs
grep -v '^#' tools/mangoapp/packages.txt | while read -r sum url; do
[ -n "$url" ] || continue
curl -fsSL --retry 3 -o mango-pkgs/pkg.tar.zst "$url"
echo "$sum mango-pkgs/pkg.tar.zst" | sha256sum -c -
tar --use-compress-program=unzstd -xf mango-pkgs/pkg.tar.zst -C mango-pkgs
rm mango-pkgs/pkg.tar.zst
done
install -m644 mango-pkgs/usr/bin/mangoapp "$M/mangoapp"
for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1 libtracefs.so.1; do
cp -L "mango-pkgs/usr/lib/$so" "$M/$so"
done
install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp"
rm -rf mango-pkgs
ls -l "$M"
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/firefox-steamdeck.js "$D/usr/lib/firefox/defaults/pref/steamdeck.js"
install -Dm644 tools/linuxfs/desktop/firefox-droiddeck.js "$D/usr/lib/firefox/defaults/pref/droiddeck.js"
ls -lR "$D"
# Runs whether the preloads were built or restored: readelf reads any ELF whatever the host,
# so these assertions cost nothing and need no cross toolchain.
- name: Cache PulseAudio 13.0 source
id: pa-src
uses: actions/cache@v4
with:
path: pa13
key: pulseaudio-src-13.0
- name: Fetch PulseAudio 13.0 source
if: steps.pa-src.outputs.cache-hit != 'true'
run: |
set -euo pipefail
curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1 && rm pa.tar.gz
- name: Build the AAudio sink into the audio bundle
run: |
set -euo pipefail
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/sink-out"
rm -rf pa-bundle && mkdir -p pa-bundle
tar --use-compress-program=unzstd -xf app/src/main/assets/pulseaudio.tzst -C pa-bundle
test ! -f pa-bundle/modules/arm64/module-aaudio-sink.so
install -m755 sink-out/module-aaudio-sink.so pa-bundle/modules/arm64/module-aaudio-sink.so
install -m755 sink-out/module-directaudio-sink.so pa-bundle/modules/arm64/module-directaudio-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$'
rm -rf pa-bundle sink-out
- name: Cache proot
id: proot
uses: actions/cache@v4
with:
path: |
app/src/main/jniLibs/arm64-v8a/libproot.so
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
key: proot-${{ env.NDK_VERSION }}-${{ hashFiles('tools/proot/**') }}
- name: Build proot
if: steps.proot.outputs.cache-hit != 'true'
run: |
set -euo pipefail
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
tools/proot/build.sh app/src/main/jniLibs/arm64-v8a
- name: Check the preloads
run: |
set -euo pipefail
D=app/src/main/assets/linuxfs
NEED=$(readelf -d "$D/libfakeinput.so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p')
echo "libfakeinput NEEDED: $NEED"
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
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}'; }
# The interposers are the whole point: if one is missing the client sees no controller.
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; }
@@ -207,12 +276,7 @@ jobs:
done
test -f "$D/usr/local/bin/bannerlator-session"
test -f "$D/usr/local/bin/bannerlator-proton-extra"
echo "preloads check out"
# ── The native build ─────────────────────────────────────────────────────────────────
# The compositor and adrenotools are the slowest part of a build and change least often.
# The key pins the C sources and the NDK; the restore-key still hands an older tree to
# ninja, which then rebuilds only the files that differ.
- name: Cache the CMake build tree
uses: actions/cache@v4
with:
@@ -223,27 +287,14 @@ jobs:
restore-keys: |
cxx-${{ env.NDK_VERSION }}-
# Release, not debug: AGP quietly drops v1 (JAR) signing from a debug build once minSdk is
# 24 or higher, and the apk has to carry all three schemes. Nothing is minified either way.
# The whole log is kept so a failed build's errors can be quoted back on a pull request.
- name: Build
id: build
env:
DROIDDECK_BUILD_LABEL: ${{ steps.build-label.outputs.label }}
run: |
set -o pipefail
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
- name: Describe the apk
run: |
set -euo pipefail
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
echo "apk: $APK"
ls -l "$APK"
sha256sum "$APK"
unzip -l "$APK" | grep -E 'lib/arm64-v8a|assets/linuxfs' | head -30
# Every NEEDED of every library we ship must resolve inside the apk or on the device. The
# first build shipped libwayland-server.so without libffi.so, and the app died on
# System.loadLibrary the moment a session started — a check this step would have caught.
- name: Check the native dependency closure
run: |
set -euo pipefail
@@ -251,27 +302,20 @@ jobs:
WORK=$(mktemp -d)
unzip -q "$APK" 'lib/arm64-v8a/*' -d "$WORK"
cd "$WORK/lib/arm64-v8a"
# Provided by the platform, so never packaged.
SYSTEM="libc.so libm.so libdl.so liblog.so libandroid.so libz.so libvulkan.so
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
# $SYSTEM spans lines; unquoted echo folds its newlines to spaces, or the last
# name on each line could never match (libaaudio.so did not, for the relay helper).
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
[ "$fail" = 0 ] && echo "every NEEDED resolves"
exit $fail
# AGP signs with v2+v3 and drops v1 whenever minSdk is 24 or higher, and setting
# enableV1Signing in the DSL does not move it. Re-signing here with apksigner is the one way
# to be sure of what the apk carries: zipalign first, then all three schemes explicitly.
- name: Sign with the testkey (v1 + v2 + v3)
run: |
set -euo pipefail
@@ -285,18 +329,12 @@ jobs:
--out "$APK" "$APK.aligned"
rm -f "$APK.aligned" "$APK.idsig"
# The apk must stay installable over the last one, and on every Android version: that means
# one stable key and all three signature schemes. Cheap to assert, easy to lose silently.
- name: Check the signature
run: |
set -euo pipefail
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
APKSIGNER=$(find "$ANDROID_HOME/build-tools" -name apksigner | sort -V | tail -1)
# --min-sdk-version 21: apksigner only verifies the schemes an apk's own minSdk requires,
# so at minSdk 26 it reports v1 as false whether or not the JAR signature is there.
# Asking it to verify from 21 makes it actually look.
"$APKSIGNER" verify --min-sdk-version 21 --verbose --print-certs "$APK" | tee /tmp/sig.txt
# And the files themselves, which is what an old installer reads.
unzip -l "$APK" | grep -qE 'META-INF/.*\.(SF|RSA|DSA)$' \
|| { echo "::error::apk carries no JAR signature files"; exit 1; }
for scheme in "v1 scheme (JAR signing): true" "v2 scheme (APK Signature Scheme v2): true" \
@@ -306,12 +344,8 @@ jobs:
grep -qi "CN=Android, OU=Android, O=Android" /tmp/sig.txt \
|| { echo "::error::apk is not signed with the AOSP testkey"; exit 1; }
# ── The pull-request report ──────────────────────────────────────────────────────────
# One comment per pull request, rewritten on every push: whether it merges, and when the
# build failed, the compiler's own errors with paths made short. Fork pull requests carry a
# read-only token and cannot comment; the same text is in the run summary either way.
- name: Report on the pull request
if: always() && github.event_name == 'pull_request_target'
if: always() && github.event_name == 'pull_request'
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
@@ -320,7 +354,7 @@ jobs:
BUILD_OUTCOME: ${{ steps.build.outcome }}
run: |
set -uo pipefail
MARK="<!-- steamdeck-pr-build -->"
MARK="<!-- droiddeck-pr-build -->"
{
echo "$MARK"
echo "## Build of this pull request"
@@ -329,12 +363,12 @@ jobs:
echo
case "$BUILD_OUTCOME" in
success)
echo "### ✅ Build passed"
echo "### Build passed"
echo
echo "The apk is the \`steamdeck-apk\` artifact on [this run]($RUN_URL) (signed with the same test key as every build, so it installs over any other)."
echo "The \`droiddeck-apk\` artifact is [on this run]($RUN_URL), signed with the public test key. After approval, a maintainer can run **PR Release Publish** from \`main\` to create an APK that installs over the release."
;;
failure)
echo "### ❌ Build failed"
echo "### ERROR: Build failed"
echo
echo "The compiler's errors ([full log]($RUN_URL)):"
echo
@@ -352,13 +386,12 @@ jobs:
echo "</details>"
;;
skipped|cancelled)
echo "### ⏭ Not built"
echo "### Not built"
echo
echo "The build did not run (see above, or [the run]($RUN_URL))."
;;
esac
echo
echo "_Updated on every push to this pull request._"
} > /tmp/pr-report.md
cat /tmp/pr-report.md >> "$GITHUB_STEP_SUMMARY"
ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq ".[] | select(.body | startswith(\"$MARK\")) | .id" | head -1 || true)
@@ -370,9 +403,20 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: steamdeck-apk
name: droiddeck-apk
path: app/build/outputs/apk/release/*.apk
if-no-files-found: error
retention-days: 30
# An apk is a zip already; compressing it again buys nothing and costs seconds.
compression-level: 0
sign:
needs: build
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
permissions:
contents: read
actions: read
uses: ./.github/workflows/sign.yml
with:
environment: signing-main
variants: '["standard"]'
secrets: inherit
-40
View File
@@ -1,40 +0,0 @@
name: Contributions ledger
# Keeps the Contributors section of the README current: every pull request by a contributor,
# with its state, and a line of totals each. Runs when a pull request is opened, merged or
# closed and commits the README to main as the Actions bot. The README is documentation, so
# that commit does not buy an apk build.
on:
pull_request_target:
types: [ opened, reopened, closed, edited ]
workflow_dispatch:
permissions:
contents: write
pull-requests: read
concurrency:
group: contributions
cancel-in-progress: false
jobs:
ledger:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
ref: main
- name: Rewrite the table
env:
GH_TOKEN: ${{ github.token }}
run: .github/scripts/contributions.sh "$GITHUB_REPOSITORY"
- name: Commit it when it changed
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add README.md
if git diff --cached --quiet; then echo "ledger unchanged"; exit 0; fi
git commit -m "README: contributions ledger"
git push origin HEAD:main
+150
View File
@@ -0,0 +1,150 @@
name: PR Release Publish
run-name: sign PR #${{ inputs.pr_number }} with the release key
on:
workflow_dispatch:
inputs:
pr_number:
description: Open, approved pull request
required: true
type: number
permissions:
actions: read
contents: read
pull-requests: read
concurrency:
group: pr-release-${{ inputs.pr_number }}
cancel-in-progress: false
jobs:
authorize:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
outputs:
run_id: ${{ steps.gate.outputs.run_id }}
head_sha: ${{ steps.gate.outputs.head_sha }}
label: ${{ steps.gate.outputs.label }}
steps:
- name: Check the PR, review, and build
id: gate
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ inputs.pr_number }}
run: |
set -euo pipefail
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null
head=$(jq -r '.head.sha' <<<"$pr")
base=$(jq -r '.base.sha' <<<"$pr")
run_id=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \
--event pull_request --commit "$head" \
--json databaseId,headSha,status,conclusion,createdAt,attempt -L 100 |
jq -r --arg head "$head" '[.[] | select(.headSha == $head)] |
sort_by([.createdAt, .attempt]) | last |
select(.status == "completed" and .conclusion == "success") | .databaseId // empty')
[[ -n "$run_id" ]] || { echo '::error::The latest PR build is not green'; exit 1; }
run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id")
jq -e --arg head "$head" --arg base "$base" --argjson number "$PR_NUMBER" '
.event == "pull_request" and .head_sha == $head and .conclusion == "success" and
any(.pull_requests[]?; .number == $number and .head.sha == $head and .base.sha == $base)
' <<<"$run" >/dev/null
approved=$(gh api --paginate --slurp \
"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews?per_page=100" |
jq -r --arg head "$head" '[.[][] | select(.commit_id == $head)] |
group_by(.user.login) | map(max_by(.id)) | .[] |
select(.state == "APPROVED") | .user.login')
maintainer_approved=false
while IFS= read -r reviewer; do
[[ -n "$reviewer" ]] || continue
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$reviewer/permission" --jq .permission)
[[ "$permission" == write || "$permission" == admin ]] && maintainer_approved=true
done <<<"$approved"
[[ "$maintainer_approved" == true ]] || { echo '::error::A maintainer must approve this commit'; exit 1; }
sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}
tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1)
stable_tag=$(gh api repos/The412Banner/DroidDeck/releases/latest --jq .tag_name)
mkdir -p "$RUNNER_TEMP/stable" "$RUNNER_TEMP/pr"
gh release download "$stable_tag" -R The412Banner/DroidDeck \
-p "DroidDeck-$stable_tag.apk" -D "$RUNNER_TEMP/stable"
gh run download "$run_id" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D "$RUNNER_TEMP/pr"
mapfile -t apks < <(find "$RUNNER_TEMP/pr" -type f -name '*.apk')
[[ ${#apks[@]} == 1 ]] || { echo '::error::Expected one PR APK'; exit 1; }
package_line() {
"$tools/aapt" dump badging "$1" | sed -n "s/^package: name='\\([^']*\\)' versionCode='\\([^']*\\)'.*/\\1 \\2/p"
}
stable=$(package_line "$RUNNER_TEMP/stable/DroidDeck-$stable_tag.apk")
candidate=$(package_line "${apks[0]}")
[[ "$stable" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && "$candidate" == "$stable" ]] || {
echo "::error::PR package/versionCode '$candidate' differs from stable '$stable'"; exit 1;
}
current=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
jq -e --arg head "$head" --arg base "$base" '
.state == "open" and .draft == false and .base.ref == "main" and
.head.sha == $head and .base.sha == $base
' <<<"$current" >/dev/null
[[ $(gh pr view "$PR_NUMBER" -R "$GITHUB_REPOSITORY" --json reviewDecision --jq .reviewDecision) == APPROVED ]] || {
echo '::error::The current PR revision still needs approval'; exit 1;
}
{
echo "run_id=$run_id"
echo "head_sha=$head"
echo "label=PR.$PR_NUMBER-${head:0:7}"
} >> "$GITHUB_OUTPUT"
sign:
needs: authorize
uses: ./.github/workflows/sign.yml
with:
environment: signing-main
variants: '["standard"]'
run_id: ${{ needs.authorize.outputs.run_id }}
label: ${{ needs.authorize.outputs.label }}
artifact: droiddeck-pr-signed
secrets: inherit
publish:
needs: [authorize, sign]
runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
steps:
- uses: actions/download-artifact@v4
with:
name: droiddeck-pr-signed-standard
path: signed
- name: Publish to DroidDeck-CI when configured
env:
GH_TOKEN: ${{ secrets.DROIDDECK_CI_TOKEN }}
PR_NUMBER: ${{ inputs.pr_number }}
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
LABEL: ${{ needs.authorize.outputs.label }}
run: |
set -euo pipefail
if [[ -z "$GH_TOKEN" ]]; then
echo '::warning::No CI token; download the signed APK from this run'
exit 0
fi
repo=Droid-Deck/DroidDeck-CI
tag="pr-$PR_NUMBER"
apk="DroidDeck-$LABEL.apk"
sha=$(sha256sum "signed/$apk" | cut -d ' ' -f 1)
notes="Source: https://github.com/$GITHUB_REPOSITORY/pull/$PR_NUMBER
Commit: $HEAD_SHA
APK SHA-256: $sha"
if ! gh release view "$tag" -R "$repo" >/dev/null 2>&1; then
gh release create "$tag" -R "$repo" --target main --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
fi
gh release upload "$tag" "signed/$apk" -R "$repo" --clobber
gh release edit "$tag" -R "$repo" --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
gh api "repos/$repo/releases/tags/$tag" |
jq -r --arg keep "$apk" '.assets[] | select(.name | endswith(".apk")) | select(.name != $keep) | .id' |
while IFS= read -r id; do
gh api --method DELETE "repos/$repo/releases/assets/$id" >/dev/null
done
+60
View File
@@ -0,0 +1,60 @@
name: Release APKs
run-name: release ${{ inputs.version }} from main ${{ github.sha }}
on:
workflow_dispatch:
inputs:
version:
description: Version for the file names, e.g. 0.1.7
required: true
permissions:
contents: read
actions: read
concurrency:
group: release
cancel-in-progress: false
jobs:
find:
name: find the main build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
outputs:
run_id: ${{ steps.find.outputs.run_id }}
variants: ${{ steps.find.outputs.variants }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- id: find
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
[[ $VERSION =~ ^[0-9A-Za-z._-]+$ ]] || { echo "::error::version '$VERSION': letters, digits, . _ - only"; exit 1; }
runs=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml --branch main --commit "$GITHUB_SHA" \
--status success --json databaseId,event,headSha -L 20)
id=$(jq -r --arg sha "$GITHUB_SHA" \
'[.[] | select(.headSha == $sha and (.event == "push" or .event == "workflow_dispatch"))][0].databaseId // empty' <<<"$runs")
if [ -z "$id" ]; then
echo "::error::main ${GITHUB_SHA::7} has no successful Build APK run yet: wait for it, then release"
exit 1
fi
variants=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)
echo "run_id=$id" >> "$GITHUB_OUTPUT"
echo "variants=$variants" >> "$GITHUB_OUTPUT"
echo "Signing Build APK run $id (main ${GITHUB_SHA::7}) as $VERSION: $variants" >> "$GITHUB_STEP_SUMMARY"
sign:
needs: find
uses: ./.github/workflows/sign.yml
with:
environment: release
variants: ${{ needs.find.outputs.variants }}
run_id: ${{ needs.find.outputs.run_id }}
label: ${{ inputs.version }}
artifact: droiddeck-release
secrets: inherit
+95
View File
@@ -0,0 +1,95 @@
name: Sign APKs
on:
workflow_call:
inputs:
environment:
type: string
required: true
variants:
description: JSON array of names from tools/release/variants.txt
type: string
required: true
run_id:
description: The Build APK run whose apk to sign; empty for the calling run's own
type: string
default: ''
label:
description: Goes in the file names (a version); empty for <branch>-<short sha>
type: string
default: ''
artifact:
type: string
default: droiddeck-signed
permissions:
contents: read
actions: read
jobs:
sign:
name: sign ${{ matrix.variant }}
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: ${{ inputs.environment }}
strategy:
fail-fast: false
matrix:
variant: ${{ fromJSON(inputs.variants) }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: The apk (this run's build)
if: inputs.run_id == ''
uses: actions/download-artifact@v4
with:
name: droiddeck-apk
path: in
- name: The apk (a main build)
if: inputs.run_id != ''
env:
GH_TOKEN: ${{ github.token }}
RUN: ${{ inputs.run_id }}
run: gh run download "$RUN" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D in
- name: Sign
env:
RELEASE_KEYSTORE_B64: ${{ secrets.RELEASE_KEYSTORE_B64 }}
RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}
RELEASE_KEY_ALIAS: droiddeck
ENVIRONMENT: ${{ inputs.environment }}
VARIANT: ${{ matrix.variant }}
LABEL: ${{ inputs.label }}
run: |
set -euo pipefail
if [ -z "$RELEASE_KEYSTORE_B64" ]; then
echo "::error::environment '$ENVIRONMENT' has no signing key: run tools/release/make-release-key.sh once"
exit 1
fi
export RELEASE_KEYSTORE="$RUNNER_TEMP/release.p12"
( umask 077; base64 -d <<<"$RELEASE_KEYSTORE_B64" > "$RELEASE_KEYSTORE" )
BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -n1)
export BUILD_TOOLS
[ -n "$LABEL" ] || LABEL="${GITHUB_REF_NAME//\//-}-${GITHUB_SHA::7}"
suffix=$(awk -v v="$VARIANT" '!/^#/ && $1 == v { print $3 }' tools/release/variants.txt)
[ "$suffix" = - ] && suffix=
apks=(in/*.apk)
[ ${#apks[@]} = 1 ] && [ -f "${apks[0]}" ] || { echo "::error::expected one apk in the build"; exit 1; }
mkdir -p out
out="out/DroidDeck-$LABEL$suffix.apk"
tools/release/sign-apk.sh "${apks[0]}" "$VARIANT" "$out"
( cd out && sha256sum -- *.apk > "SHA256SUMS-$VARIANT.txt" && cat "SHA256SUMS-$VARIANT.txt" )
echo "- \`$(basename "$out")\` - $(cut -c1-16 "out/SHA256SUMS-$VARIANT.txt")…" >> "$GITHUB_STEP_SUMMARY"
- name: Forget the key
if: always()
run: rm -f "$RUNNER_TEMP/release.p12"
- uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact }}-${{ matrix.variant }}
path: out/*
if-no-files-found: error
+18
View File
@@ -7,6 +7,24 @@ build/
captures/
.externalNativeBuild/
.cxx/
.kotlin/
# Built in CI from tools/linuxfs and app/src/main/cpp/fakeinput.cpp, never committed, so the two
# copies cannot drift.
app/src/main/assets/linuxfs/
app/src/main/jniLibs/arm64-v8a/libproot.so
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
# Kotlin compiler session files (build-tool state, never source).
.kotlin/
# Signing keys never enter the repo (DroidDeck's lives in GitHub environment secrets; see
# tools/release/make-release-key.sh). keystore/testkey.p12 is the public AOSP testkey.
*.jks
*.keystore
*.p12
*.pfx
*.pk8
*.pem
!keystore/testkey.p12
droiddeck-release-key/
password.txt
-155
View File
@@ -1,155 +0,0 @@
# SteamDeck — Progress Log
Running engineering log for the SteamDeck app (`com.steamdeck.launcher`). Newest state first, then
the timeline, then lessons and backlog. Companion to the README (what the app *does*) and to
`docs/releases/` (what each version said) — this is *how it got here and where it stands*.
---
## 2026-09-22 (late) — motion front end + run-as-a-game, branch `feat/frontend-motion` (unmerged)
- Front end rebuilt around a motion system (`ui/FrontEndScreen.kt`, same state/actions API): the rail's selection is one pill that springs between rows; sub-lists unfold and stagger their children; a page change sinks out and cascades in over a blurred wash of the selection's art; tiles lift/ring/shine on focus and pop a play badge; the launch button sweeps a sheen and squashes on press; the session activity rises over the front end (`res/anim/session_*`). Durations follow the system animator scale.
- The session now runs as a game: manifest `appCategory=game` + `game_mode_config` (Performance mode welcome; the OS FPS cap and downscaling refused), sustained performance mode, the panel's fastest mode at its size, GameManager game state, and an ADPF hint session over the compositor thread fed with each presented frame's interval (`session/PerfMode.kt`, `session/PerfHints.kt`, `nativeCompositorTid`). One `perf:` line in the session log says what took on the device.
- App libraries link 16 KB-aligned. Still 4 KB-only (prebuilt, need a rebuild): libpulse, libpulseaudio, libpulsecommon-13.0, libpulsecore-13.0, libsndfile, libltdl.
- The session's display caps at 720p by default now, client and desktop alike (`SessionPrefs.defaultResolutionCap`); a cap the user chose still wins. The cog's list marks the default per mode.
- Builds: r1 `7ddfb57` (motion only), r2 `546c063` (+ run-as-a-game), r3 `d1ced87` (+ client 720p), r4 `fb2a3d4` (+ desktop 720p) — all CI-green, none device-tested. Staged as `SteamDeck-motion-r1..r4.apk`; r4 has everything.
- r5 `1ba803b` + TZ in the guest; r6 `39272cf` Setup folds, "Linux desktop environment", help link above the grid (r4 seen running on the FIT); r7 `af3f377` settings without a pop-up: the cog and Performance are pages in the pane with anchored menus under each value, frame generation / logs / offline open in place on the rail. **r7 merged fast-forward to `main` (`af3f377`) and main's auto-build staged as `SteamDeck-r52.apk`; no tag, no release.**
- r8 `90ff23a` (branch only, unmerged): the session drawer in the front end's dress with the same rows and menus (Now / Next session / leave), FEXCore presets carried over from Bannerlator (`core/FexPreset`, Steam settings page + drawer, FEX_* into the Steam session's environment; default = FEX defaults as before), file manager and picker locked to landscape.
- Collaborators: `maxjivi05` (push) and `xXJSONDeruloXx` (push, invited 2026-09-22) on the private repo.
## 🔖 Checkpoint 2026-09-22 22:xx — known-good point for the front end
- **`main` @ `36cde7d`**, APK `SteamDeck-r51.apk` staged (sha `adb5acf5…`, run 35761437514) =
frontend-r6 + docs. Same versionCode 5 / 0.1.4 inside; **0.1.5 not cut yet**.
- **Proven on the Pocket FIT this evening:** the front end renders and navigates (rail, art,
square emulator icons, focus outline); RPCS3 lists Tomb Raider (ISO in a subfolder) and God of
War II HD (its HDD); ▶ God of War II HD boots the game under gamescope at ~40 fps
(`session-20260922-133044`: `run: rpcs3.AppImage --no-gui …NPUA80491/USRDIR/EBOOT.BIN`, 399
frames on screen in 10 s).
- **Not yet proven:** desktop→Steam hand-off after the three fixes (r47–r50); ▶ FlatOut from the
rail (Steam session with rungameid); Running tile; volume keys; automatic SD game storage
(Install drive drop-down); Steam desktop-UI session; Tomb Raider ISO boot.
- **Open decision:** rename the app away from Valve's marks before it spreads (shortlist offered:
Linuxlator / Pocketscope / Portascope); rename = label, icon text, `applicationId` (fresh
install + runtime re-download), `Download/SteamDeck/` log folder, repo, release-tag pattern.
- **Rollback:** `git checkout 36cde7d` (or 0.1.4 tag `8e58e8e` for the last release), reinstall
`SteamDeck-r51.apk` / `SteamDeck-0.1.4.apk` from Downloads.
- Loose ends: `ui/MainScreen.kt` keeps ConfirmDialog/CreditsDialog/EmulatorHelpDialog but its
`MainScreen`/tiles are dead; worktree `~/steamdeck-frontend` still exists (branch merged).
## Current state (2026-09-22, evening)
- **`main` @ the front end merge** (`feat/frontend` fast-forwarded): the launcher main screen -
Steam ▸ games, Desktop ▸ emulators ▸ games, Running tile, focus outline. ✅ First emulator game
proven from the app: God of War II HD in RPCS3 under gamescope at ~40 fps on the Pocket FIT.
- Since 0.1.4 on main, unreleased: volume keys to Android; ▶ emulators under gamescope; ROMs chip
+ ? explainer; sysmem copy + ENOSYS hint; automatic game storage (a card = a Steam library);
the client's games on the desktop with a `steam` shim; desktop→Steam hand-off (three causes
fixed from FIT logs: Surface detach race, the replaced session's exit ending the new one,
shared log folder); the front end. Next release = 0.1.5 once the hand-off is seen working.
## State at 0.1.4 (2026-09-22)
- **Latest release: 0.1.4** — private: https://github.com/The412Banner/SteamDeck/releases/tag/0.1.4
· public: https://github.com/The412Banner/winlator-contents/releases/tag/SteamDeck-0.1.4
`main` @ `8e58e8e`, CI run 35682844106, versionCode 5, APK sha256 `4b53ce6a…` (20,957,823 B).
- **Runtime:** `linuxfs-r9` on winlator-contents (790 MB), the only runtime release left; desktop
packages from `steamdeck-desktop-r1`. The app rewrites its own scripts (`bannerlator-*`), the
driver, `bannerlator-netmanager` and the DirectAudio pieces into the installed runtime at every
launch, so a fix in those reaches an installed runtime without a re-download.
- **Shipped feature set:** the native ARM64 Steam client under gamescope on the in-app Wayland
compositor; a LXQt-on-labwc desktop with Firefox and a shelf of emulators; Proton tools
(GE / CachyOS) as downloads; graphics drivers importable per mode; DirectAudio + microphone;
client/game core masks + four session switches; NetworkManager stand-in (Max's); overlay
restored; ROMs folder + Storage bound into the session's home; Bannerlator's File Manager;
a cog per launch mode (resolution, shape, HDR10, drivers, touch, OSC/audio, renderer); per-session
log folders that survive a crash; leftover-process sweep; frame generation (Win-FG, LSFG).
- **Verification level:** every release is CI-green and staged to the maintainer's device. Proven
on hardware (Pocket FIT / Fold): sign-in, store, install + launch (FlatOut 144 Hz), frame
generation, controllers + OSC, sound, desktop + Firefox, folding mid-session, driver import, the
log folder, the ANR fix, the network page. **Not yet proven:** everything 0.1.4 added (ROMs
folder inside an emulator, File Manager, resolution cap, HDR10, crash sweep, orphan sweep, the
drawer's Steam menu), DirectAudio *voice*, the core masks' effect, offline start, the soft
keyboard, any emulator with a game, Adreno 710.
- **Open field reports (Thor Pro, 8 Gen 2):** trackpad-mode tap does not click (code sends the
click; cause not found by reading); crash after 1–2 min in Ballionaire / Geometry Wars (no log
survived — 0.1.4's sweep will leave `crash.log`); a Fold's 10–20 fps client menus (Chromium →
ANGLE → Zink on an experimental A8xx Turnip); Fold crash loop (xalia ENOSYS storm — switches
shipped, untested); FlatOut shrinking after Resume (gamescope forcing the swapchain extent).
## Release pipeline (how every version is cut)
1. Work on `main`, pushed → `Build APK` runs `assembleRelease` (AOSP test key, v1+v2+v3 re-signed
by CI with zipalign + apksigner) → artifact `steamdeck-apk`. Dev builds keep the last release's
versionCode/versionName; a release bumps both in `app/build.gradle` in its own commit.
2. Verify: `gh run view <id> --json conclusion` (never trust a run listing's first row — a
`workflow_dispatch` run gets cancelled by concurrency in favour of the push-triggered one),
download the artifact, sha256 it.
3. Stage: `cp` to `/sdcard/Download/SteamDeck-rN.apk` (dev) or `SteamDeck-X.Y.Z.apk` (release).
Never `pm install` — the maintainer installs.
4. Private release `X.Y.Z` targeting the **full 40-char sha** (a short sha is refused), notes from
the draft in the session scratchpad, `--latest`. Public release `SteamDeck-X.Y.Z` on
winlator-contents with the README-style notes; one public release per version.
5. README ledger, this log, memory.
## Timeline
- **2026-09-19 — 0.1 groundwork.** Lifted out of Bannerlator's gamescope runtime on the user's
ask: one screen, one button. r2 reached Big Picture sign-in on the Pocket FIT. Package renamed to
`com.steamdeck.launcher` (r4), foreground service, test key, on-screen controls.
- **2026-09-20 — 0.1.** The Linux Steam client and a desktop; runtime r6→r9 in a day (Proton
self-selection, proot shipped, refresh rate, video, first run reaches a game). Public explainer
page for non-Linux users.
- **2026-09-21 — 0.1.1.** Catch-up with Bannerlator: driver selection (two lists, per mode),
overlay restored + GameHub force-stop, DirectAudio + microphone, core masks, four session
switches as toggles, per-session log folders with credentials scrubbed, Adreno 710 path via
Banners-Turnip r3.
- **2026-09-22 — 0.1.2.** The teardown ANR (log collection on the main thread) fixed; two
compositor log lines (frame-size change, window rename); README ledger.
- **2026-09-22 — 0.1.3.** Max's NetworkManager stand-in ported (the client's network page); shim
audit vs WinNative = zero functional drift.
- **2026-09-22 (later) — after 0.1.4, on main.** Volume keys; ▶ emulators under gamescope (the
desktop cannot: labwc on pixman offers no dma-buf); ROMs chip; sysmem warning + ENOSYS hint;
automatic game storage; the client's games on the desktop (`steam` shim hands off to a
gamescope session); the hand-off's three faults found in FIT logs and fixed; the front end
merged (branch `feat/frontend`, r1–r6); God of War II HD booted in RPCS3 from the rail.
- **2026-09-22 — 0.1.4.** ROMs folder + Storage in the session's home; Bannerlator's File Manager
ported whole; a cog per mode (railed settings window) with resolution cap, shape, HDR10 gated on
the panel, drivers, touch, OSC/audio/renderer; two-column main menu; crash-safe log folders +
Session logs toggle; OrphanReaper; drawer Steam menu. Public releases reorganised: one per
version, old `Steamdeck` release and runtimes r1–r8 deleted.
## Architecture (where things live)
- `MainActivity` / `ui/MainScreen.kt` — the main screen; `ui/ModeSettingsDialog.kt` the cogs;
`ui/*Dialog.kt` the rest.
- `SessionActivity` — the Surface, input (touch, touchpad, pads, keyboard), the drawer, the HDR
decision, the compositor start (`wayland/CompositorHost`, `wayland/WaylandCompositor`).
- `session/SessionService` — proot + gamescope/labwc + PulseAudio + DirectAudio relay + network
link; binds (`/root/Storage`, `/root/ROMs`); env for the guest; teardown; `OrphanReaper`;
`SessionArtifacts` + `CrashHandler` + `SessionPaths` for the log folders; `SessionPrefs`.
- `files/` — Bannerlator's File Manager (`FileManagerScreen.kt` and its helpers), the picker
activity and `InAppFilePicker` intent API.
- `gpu/` — Turnip (bionic, for the compositor) and Linux Turnip (glibc, for the runtime) managers.
- `tools/linuxfs/overlay/usr/local/bin/bannerlator-*` — the guest scripts, staged into the
runtime by `SessionFiles` at every launch (CI packages only `bannerlator-*` names).
- `app/src/main/cpp/waylandcomp/` — the compositor (shared lineage with Bannerlator).
## Lessons learned (don't repeat these)
- Nothing in teardown may do bulk filesystem work on the main thread (0.1.1's ANR).
- A CI closure check on a multi-line variable must flatten it first (`libaaudio.so` refusal).
- A runtime script must be named `bannerlator-*` or CI never packages it.
- `gh release create --target` needs the full sha.
- A proot bind is invisible to a program's "Computer" list; put what users need under home.
- The compositor decides its driver and its HDR gate once per app process — anything that changes
them applies after the app is fully closed, and the UI must say so.
- Bannerlator's File Manager ports mechanically (`port_fm.py` anchors) once the container hooks
are cut; do not hand-edit 2,500 lines.
- Deleting a release asset on GitHub can drop a sibling asset — re-list and restore.
## Backlog / next
- Device-prove 0.1.4 on the FIT (list above).
- Thor Pro: trackpad tap; the 1–2 min in-game crash once a `crash.log` arrives.
- Xfce as a second desktop shell (Max's branch runs XFCE 4.20 on labwc) — a catalog package + a
shell choice in the Desktop cog; comfort, not performance.
- Quick Access Menu for non-Deck pads — no confirmed chord; needs research, not a guess.
- FlatOut shrink: try `vk_wsi_force_swapchain_to_current_extent=false` via `steamdeck-env`.
- Max's stricter `winnative-directaudio` guards; `winnative-epic-launch` (a feature).
- Rename before anything truly public: "SteamDeck" is Valve's mark.
+11 -538
View File
@@ -1,548 +1,21 @@
# SteamDeck
# DroidDeck
Valve's **native aarch64 Linux Steam client**, running on an Android phone or handheld: a glibc
rootfs under proot, gamescope as a Wayland client of an in-app Vulkan compositor, Xwayland and
Zink for the client's CEF, and the device's Turnip driver underneath. There is no Wine here, and
no x86 translation for the client itself — it is ARM code running as ARM code. Windows games it
launches go through Valve's own ARM64 Proton build, which is where FEX comes in.
Run Valve's native ARM64 Steam client on an Adreno Android device. Steam runs in a Linux runtime under proot, with gamescope and an in-app Vulkan compositor. Windows games use Valve's ARM64 Proton and FEX. A desktop with LXQt, Firefox, and emulators is also available.
One screen, one button: install the runtime, press Play, Big Picture comes up. Or start the
**desktop** instead — LXQt, a browser and a shelf of emulators, in the same session.
## Requirements and install
Around that: the Vulkan driver each half of the session draws with is yours to import and change,
audio can bypass PulseAudio and carry a microphone, the cores the client and a game get are
separate, and every session writes a folder of logs meant to be attached to a report as it is.
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). 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/The412Banner/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.
## Contributors
On Android 12+, if Steam exits without a log, turn off **Restrict child processes** in Developer options.
Building this alongside the owner. A pull request from either of them builds by itself and gets
a comment saying whether it merges, what conflicts, or what failed to compile.
## Build
- **MaxsTechReview** — [@maxjivi05](https://github.com/maxjivi05)
- **Kurt Himebauch** — [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx)
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`.
<!-- contributions:start -->
_From the repository's pull requests; rewritten when one is opened, merged or closed._
## Limits
| Contributor | Pull request | Opened | State |
|---|---|---|---|
| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#1](https://github.com/The412Banner/SteamDeck/pull/1) initial decky installer stuff | 2026-09-23 | draft |
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.
- **@xXJSONDeruloXx**: 1 submitted, 0 merged
<!-- contributions:end -->
## Credits and licence
## Requirements
- An **arm64 Android device with an Adreno GPU that Turnip supports** — in practice Adreno **730 or
newer** (Snapdragon 8 Gen 1 onward) and the **8xx** series. Mali, Xclipse and PowerVR are not
supported here, and neither is the low-end Adreno 710 (Snapdragon 6 Gen 1): Mesa has no entry
for it at all, so the session log ends at `device (chip_id = 7010000, gpu_id = 710) is
unsupported` before anything is drawn.
- **Android 9 or newer**, and roughly **3 GB free** for the runtime before any games. The desktop
and its emulator packages are another ~1.1 GB if you install them.
- **No root required.** Root only helps if you want to copy an existing Bannerlator runtime across
instead of downloading one.
## Installing
1. Install the APK from the release page.
2. Launch it and press **Install Linux runtime** — about 790 MB, once.
3. Press **Play**. On the very first run the app fetches Valve's client (a minute or two with
nothing on screen), then Big Picture comes up and you sign in.
For a desktop, open **Desktop & apps**, install the *Desktop* package, then press **Desktop** on
the main screen.
**Do this once per device if the client dies with nothing in its log**: Android 12 and later kill
the extra processes an app starts for itself, and a session is made of dozens of them. Some phones
have a **"restrict child processes"** switch in Developer options — turn it off. See
[If Steam dies with nothing in the log](#if-steam-dies-with-nothing-in-the-log).
## What works today
This is version 0.1.2, and most of it has been exercised on one or two devices only. An honest
ledger rather than a feature list — where something has not been run on hardware, it says so:
| | State |
|---|---|
| Steam sign-in, store, install, launch | ✅ proven — *FlatOut* at 144 Hz through ARM64 Proton |
| Importing and choosing a driver | ✅ proven — an imported Mesa 26.3.0 glibc Turnip drew a whole session, and an imported AdrenoTools build ran the compositor |
| Frame generation (LSFG 2×) | ✅ proven — 30 → 61, 60 → 118, 61 → 123 fps |
| Performance HUD | ✅ proven |
| Desktop, panel, file manager | ✅ proven at the panel's native resolution |
| Firefox and the network (IPv4 + IPv6) | ✅ proven — live pages at 93 fps |
| Controller as an Xbox 360 pad | ✅ proven |
| Foldable, opened and closed mid-session | ✅ proven |
| Background / foreground, wake locks | ✅ proven |
| The session log folder | ✅ proven — all of it written, including this app's own log |
| Session folder finished after a crash or a kill | ⚠️ built: crash handler + next-start sweep; not yet forced on a device |
| ROMs folder and Storage in the session's home | ⚠️ built; not yet opened from an emulator on a device |
| The File Manager (Bannerlator's) and its pick mode | ⚠️ built; not yet used on a device |
| The cog beside Play / Desktop: resolution, shape, HDR, drivers, touch, audio, renderer | ⚠️ built; the resolution cap and HDR not yet seen on a device |
| Leftover session processes swept at the next start | ⚠️ built; not yet forced on a device |
| The drawer's "Steam menu" (Guide) | ⚠️ built; not yet tried |
| Emulators launched under gamescope (▶ on the rail) | ✅ proven — God of War II HD in RPCS3, ~40 fps on the FIT |
| The front end (rail, games, focus outline, Running tile) | ⚠️ built and used on the FIT; the Running tile and Steam-game ▶ not yet exercised |
| The device's volume keys during a session | ⚠️ fixed (they were swallowed); not yet re-tried |
| Game storage: a second Steam library on a card or a folder | ⚠️ built; not yet seen installing to it |
| The client's games in the desktop menu, launching Steam's desktop UI under gamescope | ⚠️ built; not yet tried |
| Stopping a competing Steam client | ⚠️ it is asked to stop and says so, but one that restarts itself from boot wins the race — uninstall it |
| DirectAudio for games, and the microphone | ⚠️ wired and the modules load; voice not yet confirmed in Steam's tester |
| Client and game core masks | ⚠️ applied and logged; no measured difference yet |
| The four session fixes (xalia, proot seccomp, sysmem, Zink) | ⚠️ they set what they say; each is a hypothesis for a device that misbehaves |
| GE-Proton / proton-cachyos | ⚠️ adopted and registered on device; no game launched through one yet |
| The emulators | ⚠️ they build, publish and install — none has been run with a game |
| Starting Steam offline | ⚠️ built, not yet tested |
| The soft keyboard | ⚠️ built, not yet tested |
| Adreno 710 / 720 / 722 | ⚠️ a path exists through imported drivers; **nobody has run this on one yet** |
## Known limits
- **The desktop composites in software.** wlroots allocates through gbm on a real DRM render node,
and the Adreno stand-in is not one, so labwc falls back to pixman. Programs on the desktop still
reach the GPU themselves through Vulkan and Turnip — it is the desktop's own compositing that
runs on the CPU.
- **Firefox's child-process sandboxes are off.** Seccomp and namespace sandboxes cannot be set up
under proot, and its tabs crash otherwise. This is a genuine reduction in isolation: treat
browsing here as less protected than on an ordinary desktop.
- **The display shape changes at the next session**, not immediately — gamescope fixes its display
size when it starts.
- **No Switch emulator.** Eden's repository was taken down in February; there is nothing to mirror
or to build from.
- PS3 (RPCS3) and Wii U (Cemu) are demanding even on desktop hardware — expect light titles only.
## How it fits together
```
Android app (this repo)
├── libbannerwayland.so Wayland compositor, presents into the activity's Surface
│ on a bionic Turnip: bundled, or one you imported
├── PulseAudio 13 + AAudio the guest's audio server, on a socket bound into the session
├── directaudio relay owns AAudio out and the mic for the games that ask for it
├── fake-evdev rings a physical pad, republished as /dev/input/eventN for the client
├── frame generation Win-FG or LSFG, on gamescope's finished output
└── proot ── linuxfs (~790 MB, downloaded once)
├── gamescope ── Xwayland ── Zink
│ drawing on a glibc Turnip: the runtime's, or one you imported
└── Steam (steamrtarm64, fetched from Valve on first run) ── Proton ARM64 ── FEX
└── a game's audio: Proton's own, or DirectAudio straight to Android
```
The two Turnips are separate copies of the same driver and cannot be swapped: the compositor's is
built against Android's C library, the runtime's against glibc. That is why there are two import
lists rather than one.
The runtime image is the one published for Bannerlator (`linuxfs.json` in
`The412Banner/winlator-contents`); this app installs and updates it from that catalog. **No Valve
software is redistributed** — the client is fetched from Valve's own manifest on the device at
first use.
## Building
Nothing is built locally. Push, and the `Build APK` workflow produces `steamdeck-apk`, a
debug-signed arm64 apk. It cross-compiles the two glibc preloads the session needs
(`libfakeinput.so`, `libblsession.so`) from the sources in this repo before the app build, so
those can never drift from what ships.
## If Bannerlator is already installed
The runtime image is the same one Bannerlator installs, so on a device that already has it the
790 MB download can be skipped entirely — copy it across with root, once, before the first launch:
```sh
su -c 'cp -a /data/data/com.tencent.ig/files/linuxfs /data/data/com.steamdeck.launcher/files/ \
&& chown -R $(stat -c %u /data/data/com.steamdeck.launcher/files) \
/data/data/com.steamdeck.launcher/files/linuxfs \
&& chcon -R u:object_r:app_data_file:s0 /data/data/com.steamdeck.launcher/files/linuxfs'
```
The two runtimes are independent after that: each app has its own Steam install, its own login and
its own games.
## Controls
A physical controller is republished into the session as a synthetic Xbox 360 pad, which is the
identity SDL and Steam have a mapping for. With nothing attached, an on-screen pad appears —
d-pad, A/B/X/Y, shoulders, select/start and the Steam button — writing into the same rings, so the
client sees one pad either way. Connect a controller and the on-screen one disappears; unplug it
and it comes back. Anywhere the controls are not is a touchpad: touch moves the pointer and a tap
clicks, which is how the client's own on-screen keyboard is used to sign in.
## Desktop
Besides the Steam client the app can start a **desktop**: LXQt on labwc, in the same session and
the same compositor, with Xwayland for the X11 programs. It is a second choice on the main
screen, not a replacement — the Steam button still hands the session to gamescope.
What it is made of is downloaded on request from a catalog hosted beside the runtime, so the app
stays small and nothing ships that a given device will not use:
| Package | What it brings |
|---|---|
| **Desktop** (406 MB) | labwc, LXQt's panel, file manager, terminal, text and image viewers, Firefox, mpv |
| **Emulators** (694 MB) | PPSSPP and RetroArch with 28 cores — PS1, PS2, N64, SNES, NES, Game Boy, GBA, DS, Genesis, Dreamcast, Saturn, PC Engine, arcade — plus ScummVM, DOSBox, Mednafen, Snes9x |
| **RPCS3 · DuckStation · melonDS** | PS3, PS1 and DS, mirrored from the projects' own ARM64 builds |
| **PCSX2 · Dolphin · Cemu** | PS2, GameCube/Wii and Wii U. No ARM64 Linux build exists upstream for any of the three, so they are compiled from source against the same glibc the runtime uses |
The desktop composites in software: wlroots allocates through gbm on a real DRM render node, and
the Adreno stand-in is not one, so labwc runs on pixman. The programs on it still reach the GPU
themselves through Vulkan and Turnip — it is the desktop's own compositing that is on the CPU.
`Download/steamdeck-wlr-renderer` (`pixman`, `gles2` or `vulkan`) overrides the choice.
Programs whose child processes sandbox themselves with seccomp and namespaces cannot set those up
under proot; Firefox's are turned off in the session's environment, without which its tabs crash.
### The front end
The main screen is a launcher: a rail with **Steam** and its installed games (from the client's
own manifests, with the library-cache art), **Desktop** and the installed emulators (their own
icons) and each emulator's games from the ROMs folder (a system folder such as `ps3/` as a hint,
by extension, one folder deep - a dump comes as a folder named for the game) plus what RPCS3 has
installed on its own HDD; then Files, Desktop & apps, Compatibility tools, Frame generation,
Performance, ROMs, Session logs, Start offline, the runtime and credits. The chosen thing sits on
the right with its ▶. A Steam game launches its session with `steam://rungameid/…` directly; a
ROM launches its emulator under gamescope with the game on the command line (RPCS3 `--no-gui`,
Dolphin `-e`, Cemu `-g`, the rest plain). A session alive in the background is the first thing on
the rail; tapping it goes back to it. The controller's focus draws a thin outline. ✅ God of War
II HD booted in RPCS3 from the rail at ~40 fps on the Pocket FIT.
### Game storage: internal, an SD card, or a folder
The client's own library is internal storage and always there. An **SD card in the phone is
registered as a second library by itself** (through the app's own folder on it, the one place a
card lets this app write), so the choice is made where it is anywhere else - inside Steam, on
every Install. **Game storage** in the Steam cog is only for turning that off or pointing it at a
folder chosen in the File Manager instead. The session binds it at
`/mnt/bannerlator-sd` and `bannerlator-steam-library` registers it with the client under the name
the app gives it; the client then asks where to install every game, lists both on its Storage
page and moves games between them itself. Set to internal only, the entry is removed again. A
card and shared storage are FUSE-backed and stream slowly - keep games that stream video or big
assets internal. ⚠️ Built; not yet seen installing to a card.
### The client's games on the desktop
The Steam client writes a menu entry for every game it installs (Games category, the game's
icon), and the home folder has **Steam Games** and **SD Games** links to their files. Those
entries run `steam steam://rungameid/…`; on this desktop `steam` is a shim, because the client
cannot run where nothing can draw with Vulkan. It - and the menu's **Steam** / **Steam Big
Picture** entries - hand off to the app (`bannerlator-steam-launch`), which ends the desktop
session and starts a Steam session in its place: the client's **desktop UI** under gamescope, and the game straight away when one was
chosen. Back opens the drawer as in any session; Stop session returns to the app. The list is
rebuilt at every desktop start. ⚠️ Built; not yet tried on a device.
### Running an emulator with the GPU
The desktop composites in software (labwc on pixman) and so offers its programs no dma-buf: a
Vulkan program on it has no swapchain to draw into and dies at its first frame — RPCS3 with
`VK_ERROR_SURFACE_LOST`. The GPU path that works on this hardware is gamescope's, the one Steam's
games render through. So an emulator is started **as a session of its own**: ▶ beside an installed
emulator under **Desktop & apps** runs it fullscreen under gamescope (the session script's `run`
mode, an AppImage or a binary inside the runtime), with Steam's display, driver and HDR settings.
The desktop remains for files, Firefox and everything that draws in software. ⚠️ Built; not yet
seen with a game.
### The device's files, and the ROMs folder
Inside a session the phone's storage is at `/storage/emulated/0`, as it is outside - but no
program's file dialog will show it: they list "Computer" from `/proc/mounts`, where a proot bind
never appears, and open in the home folder, where nothing pointed at the phone. So the home
folder now carries two entries every session:
- **`Storage`** - all of internal storage.
- **`ROMs`** - the folder chosen with **ROMs folder** on the main screen, wherever it is on the
device, an SD card included (it is a bind, not a link). Point any emulator's game directory at
`/root/ROMs` once and it is the same folder in every session after.
Both are the live folders, not copies: a save an emulator writes next to its ROM lands on the
phone.
### Files
**Files** on the main screen is Bannerlator's File Manager, carried over whole: grid and list,
sort, search, multi-select, copy / cut / paste with conflict handling and a cancellable progress
bar, rename, delete, new folder, properties (read-only and hidden), favourites, and a locations
rail with every mounted card, Downloads, the ROMs folder and the session logs. Its pick mode is
what chooses the ROMs folder and imports a driver. What is not here is what has no meaning without
a Wine container: running a file, adding it to shortcuts, the C: / Z: drives and the archive
unpacker.
## Pointer
The app draws the arrow. On the desktop, touch is a **touchpad** (Bannerlator's desktop-container
feel): drag anywhere and the pointer moves from where it is; tap to click; a second finger tapping
while one holds, or a two-finger tap, right-clicks; two fingers scroll; hold still then drag, or
double-tap-and-hold, drags. In Steam, touch is **direct**: the pointer jumps under the finger. The
drawer's *Touch* switch pins either. A USB or Bluetooth mouse works as itself.
## Networking
The desktop's programs use the phone's network directly — proot makes no network namespace, so
IPv4 and IPv6 both pass through — and the app writes the runtime's `/etc/resolv.conf` from the
active network's own DNS servers whenever the link changes (IPv4 first, never a link-local IPv6 one,
public servers as fallback), which is what Firefox and everything else in the desktop resolve with.
## Keyboard
**Keyboard** in the drawer opens the soft keyboard over the session. Steam's UI and the games
under it are X11 clients of gamescope and know nothing of Wayland text input, so what the
keyboard produces is turned into the key presses that would have typed it — Shift included, so
capitals and the symbol row (`@`, `!`, `_` …) arrive as themselves — and fed to the compositor as
key events. A hardware keyboard works as it is.
## Starting Steam offline
The client runs without a connection — installed games launch, Proton and the frame generation
are local — provided it has credentials from a sign-in that succeeded earlier. **Start offline**
on the main screen is where that is decided, and not in the drawer, because the client reads the
two keys that control it once, while it starts: changed during a session it would write the file
and change nothing until the next launch. Without an earlier sign-in the button says so and does
nothing. The client rewrites those keys when it exits, so the app writes them again at every
session start. Games with always-online DRM will not start offline, and nothing can be installed
or updated.
## Audio
The Steam client always plays through **PulseAudio** — it is a native Linux program with no other
way to make a sound, and its menus, music and voice chat all go through it. **Audio** on the main
screen adds two things on top, each applying at the next session start:
- **DirectAudio for games** — replaces Wine's audio driver *inside the games the client launches*
with Bannerlator's DirectAudio, which talks straight to Android over a socket instead of through
PulseAudio: lower latency, and no resampling in between. The client is untouched. It only pairs
with a Wine 11 Proton (Valve's ARM64 builds, GE-Proton, proton-cachyos); on anything else the
wrapper leaves it off and says so in the log, because a mismatch would be silence rather than an
error. A game's first launch after turning it on still uses Proton's own audio — the driver is
named in the game's prefix, which does not exist until that first launch — and the second uses
DirectAudio.
- **Microphone** — the relay helper opens Android's input stream under the app's own uid (which is
what the recording permission is checked against) and PulseAudio exposes it to the client as a
source named **DirectAudioMic**, so Steam voice chat works. Asks for the recording permission when
turned on; off, nothing in the session can record and Android's indicator stays off.
The helper is a small Android program the app runs beside the session; the driver is a glibc build
staged into the runtime at every launch like the session scripts, so a fix reaches an installed
runtime without re-hosting it. Proven in Bannerlator's Linux session with voice in Steam's own
tester; ⚠️ not yet exercised in this app.
## Cores
**Performance** on the main screen carries the two core masks Bannerlator's Linux session has, kept
separate because they are wanted at the same time and suit different things:
- **Steam client cores** — Steam pins its own interface renderer to a subset of cores it chooses (on
one device 5 of 8, leaving out both little cores and the fastest), which is reasonable while a
game runs and makes Big Picture sluggish when the client is all there is. Turning the override
on pins the client, its UI helper and gamescope to the cores you tick, re-applied every few
seconds because the UI keeps spawning helpers that inherit Steam's choice. Every core ticked is
the usual fix — and, unlike the game mask, it *is* sent when it names every core, since undoing
Steam's pin is the whole point.
- **Game cores** — applied by the Proton wrapper, which execs the game through `taskset` so every
thread inherits the mask from its first instruction. Leaving every core ticked sends nothing;
untick the small cores to keep a heavy game off them.
Both apply at the next session start.
Under the same dialog, **session fixes** for a device the runtime does not sit well on, tried one at
a time. **Skip Steam's xalia helper** (`PROTON_USE_XALIA=0`) is the one to reach for when a session
dies seconds after Big Picture appears: xalia is an x86 Windows program Proton starts for gamepad
navigation, it cannot load the session's preload shim under FEX, and where the vendor's seccomp
answers its `socket()` and `memfd` calls with ENOSYS it storms until the session collapses (89
refusals then a broken pipe, in the log). It costs gamepad navigation in Windows programs that are
not games. Then two for a runtime that renders slowly: **Turnip: sysmem rendering** (`TU_DEBUG=sysmem`, Banners-Turnip's advice for an Adreno 8xx that
looks glitchy or slow; a 710/720/722 gets it on its own) and **Zink: lazy descriptors**
(`ZINK_DESCRIPTORS=lazy`, for Steam's menus, which Chromium draws through Zink — the first thing to
try when they are slow while games are fast). The `steamdeck-tu-debug` and `steamdeck-env` files in
Downloads still win over both.
## Networking and the client's own network page
The session uses the phone's connection directly - proot makes no network namespace, so IPv4 and
IPv6 both pass through - and the app writes the runtime's `/etc/resolv.conf` from the active
network's own DNS servers whenever the link changes.
The client's **Settings → Internet** page and its connection indicator are a different matter:
they are built on NetworkManager, which the client looks for on the system bus as it starts. Without
one, that page stopped with *"StartScanningForNetworks is not a function"* under a header showing no
connection even while downloads ran. The session now starts a small system bus and a **stand-in that
answers for NetworkManager**, reporting what is true and nothing more - one connection that is up
while Android has a route out - and refusing what it cannot do, since nothing in this runtime can
scan for or join a network. The session log says `network: system bus up, NetworkManager stand-in
running` when it is working.
That stand-in is **maxjivi05's**, from WinNative, by way of Bannerlator.
## Compatibility tools
**Compatibility tools** (main screen and drawer) installs GE-Proton or proton-cachyos — native
ARM64 builds — beside the ARM64 Proton Valve ships. The runtime's own registrar downloads and
installs a requested build when the next session starts (several hundred MB), patches it past
pressure-vessel like Valve's, and it then appears in Steam under Properties → Compatibility for
any game. The dialog queues or cancels a request and removes an installed build.
## The cog beside Play and Desktop
Each launch button has a cog, and everything that only matters for that one mode lives behind it,
so the main screen keeps only what applies to both: **resolution** (up to 720p by default; 900p, 1080p, or the
panel's own), the display **shape**, **HDR10 output**, the
**Linux runtime driver** for that mode and the shared **display driver**, **touch**, and for Steam
the on-screen controls and audio, for the desktop its renderer.
**HDR10** is a switch that is greyed out, with the reason, on a panel that does not list HDR10 —
the compositor's HDR gate is decided from the display's own word (`android.view.Display`), once,
when the compositor starts. On, the compositor offers `wp_color_manager_v1` with BT.2020 + PQ and
10-bit buffers, gamescope is started with `--hdr-enabled` and speaks the same protocol to it, and
games get `DXVK_HDR=1`; a game that renders HDR then reaches the panel as HDR on its own display
layer, tagged BT2020_PQ. The compositor lives for the whole app process, so a change applies after
the app is fully closed and opened again. ⚠️ Wired end to end; not yet seen on a device from this
app (Bannerlator proved the same compositor path on a Fold).
## Graphics drivers
Two drivers, two lists, because a zip for one cannot serve the other — the same split Bannerlator's
Contents screen makes. Both are chosen in the cog beside Play / Desktop:
- **Linux runtime driver** — the glibc Turnip everything *inside* the runtime renders on: the Steam
client's UI, every game it launches, and in desktop mode every program on the desktop. Import a
"-Linux" Turnip zip from Banners-Turnip; a plain Android or "-Wayland" Turnip is refused with the
reason, since a Linux process cannot load a bionic library. The choice is per mode, so Steam and
the desktop can run different builds, and it takes effect at the next session start. Nothing in
the runtime is modified: the session is handed the imported driver's ICD manifest and points the
Vulkan loader at it, and the runtime's own Turnip is what a removed or unreadable import falls
back to. This is how a driver fix reaches an installed runtime without a ~790 MB re-download.
- **Display driver (Android)** — the bionic Turnip the app's compositor puts the frame on the panel
with, the last step of every session. *Auto* picks one of the two bundled builds by GPU; an
imported AdrenoTools zip can be chosen instead. The compositor loads its driver once per app
process, so this one applies after the app is fully closed and started again.
Both imports open the app's own file picker rather than Android's document picker: an SD card is a
card in its drive menu, the folder last picked from is where it opens next time, and it hands back
a plain path.
## Frame generation
Extra frames are generated between the real ones on the way to the screen — inside the app's
compositor, on gamescope's final output — so it works for any game with nothing changed in the
runtime. **Win-FG** is built in. **LSFG** uses the shader chain from your own copy of Lossless
Scaling: install it from the Steam client in this app and the app reads its `Lossless.dll` from
the runtime's Steam library (parsed as data, never executed; nothing of it is redistributed).
Pick an engine and 2×/3×/4× on the main screen; it applies immediately, mid-game included.
## Background and foreground
A session belongs to a foreground service, not to the activity, so leaving Big Picture does not
end it: the process stays at perceptible priority, a partial wake lock keeps the CPU from dropping
the guest's threads, and a high-performance WiFi lock keeps a backgrounded download from being
throttled to nothing. **Back opens the drawer**: the performance HUD switch, frame generation, on-screen controls
(auto / always / never), the display shape (the panel's own, or a fixed 16:9 — the default on a
foldable, so opening or closing it mid-session only changes the bars, never the picture; takes
effect on the next session), *Send to background* and *Stop session*. Backgrounded, Steam keeps
running and the (silent) notification brings it back. A session ends only when you say so — the
drawer or the notification's **Stop session** — or when the app is swiped out of recents.
## Device switches
Files in `/sdcard/Download`, for a device that cannot be reached with a debugger:
| File | Effect |
|---|---|
| `steamdeck-no-pad` | Turns the controller feature off entirely (a true baseline) |
| `steamdeck-pad-log` | Traces every interposer call into the session log |
| `steamdeck-driver` | `a7xx`, `a8xx` or `system` — overrides the Turnip build the compositor loads |
| `steamdeck-no-hud` | Hides the top-right performance line (fps, and `base → generated` while frame generation runs) |
| `steamdeck-osc` | `always` or `never` — pins the on-screen controls instead of following what is attached |
| `steamdeck-env` | `KEY=VALUE` lines added to the session's environment as written, after the app's own — Zink and Turnip tunables (`ZINK_DESCRIPTORS=lazy`, `MESA_*`), gamescope's, the client's; `#` comments allowed |
| `steamdeck-tu-debug` | Turnip's `TU_DEBUG` for the runtime's driver, verbatim (`sysmem`, `sysmem,deck_emu`). Without it, an imported A710/A720/A722 driver gets `sysmem` on its own |
## Session logs
Every session writes a folder under `/sdcard/Download/SteamDeck/`, and it is the first thing to
look at when something does not start:
```
Download/SteamDeck/session-20260921-161256/
device.txt what this device is, and every setting the session ran with
session.log the guest session: proot, gamescope, the client's own output
wayland.log the app's compositor - what it presented, at what size, and how fast
app.log what the app itself decided and reported
crash.log Android's crash buffer, as it stood when the session ended
crash-app.txt the stack trace, if the app itself crashed
audio.log the PulseAudio daemon and the DirectAudio relay helper
network.txt the link, the DNS the runtime was given, and what it means
steam.log the Steam client's log, scrubbed (Steam mode)
steam/ the rest of the client's logs, scrubbed (Steam mode)
desktop.log labwc, the panel and the programs on it (desktop mode)
```
`app.log` is this app's own logcat, filtered to its process: which driver it chose and why, the
audio line, a rival Steam client being stopped, a helper that was missing, the session's exit
status. `crash.log` is Android's crash buffer at teardown, which is where a session the *system*
killed leaves its only trace - proot once died before `main` over a missing library and said so
there and nowhere else. `audio.log` is the PulseAudio daemon's own output plus the relay helper's,
so a module that refuses to load is visible instead of being a silent absence of sound.
`network.txt` records the transport, whether the link validated, the address families and the DNS
servers the runtime was actually given - **never the network's name**.
A folder is finished **however the session ends**, not only at a clean stop. Everything written
*during* a session is on disk as it happens; only the ending - the compositor's log, Steam's
scrubbed logs, the crash buffer - is gathered afterwards, and three things gather it: the ordinary
stop; the app's own crash handler, which writes `crash-app.txt` with the stack trace and finishes
the folder before the process dies; and a sweep at the next app start, for a folder whose process
was killed outright (Android's phantom-process killer, a native crash in the compositor, a dead
battery) - that folder gets `ended-without-teardown.txt` saying so, and `crash.log` as the buffer
stands then, which still holds the entry unless the device rebooted. A finished folder carries a
`.complete` marker; one without it is what the sweep looks for.
**Session logs** on the main screen turns the folder off. The session still writes its logs - the
scripts and the compositor need somewhere to - but into the app's cache, and they are deleted
when the session ends, so nothing accumulates in Downloads.
A session's process tree is also swept at the next start: proot, gamescope and Steam outlive an
app that was killed or crashed, holding the rootfs, the GPU and Steam's lock, and the next session
then never came up until the app was force-stopped (a Thor Pro report). Anything under the app's
uid other than the app itself is a leftover at that point, and `app.log` lists what was killed.
`device.txt` is written before the session starts, so a session that dies in its first second
still says what it ran on: model, SoC, Android and kernel, cores and their ceilings, RAM, the GPU
as KGSL names it, the panel, **which driver each half of the session chose**, the runtime version,
and every switch that was in effect. **Nothing identifying is collected** - no serial number, no
device or advertising id, no account name, no network names - and the Steam logs are scrubbed line
by line on the way in: session tokens, machine-auth GUIDs, WebAPI keys, Guard codes and e-mail
addresses are replaced, and a SteamID is masked to its last four digits so lines can still be
correlated. `loginusers.vdf`, `config.vdf` and the `ssfn` files are never copied at all. A session
folder is meant to be attachable to a bug report exactly as it is.
For more from the desktop than it normally says, create `~/.steamdeck-desktop-debug` inside the
runtime: labwc then logs at debug level, each program's window-protocol traffic is recorded, and the
session does a name lookup plus an IPv4 and an IPv6 fetch of its own — which is how to tell a broken
network apart from a program that only believes it has one.
### If Steam dies with nothing in the log
That is usually not Steam. **Android 12 and later kill the extra processes an app starts for
itself** once there are more than a few, and a session is made of dozens - proot, gamescope,
Xwayland, the client and its helpers, Wine, FEX. Where that is left on, the OS ends the session and
no log of ours says why, because nothing in the session did it. Some phones expose it in Developer
options as a **"restrict child processes"** switch; turn it off. Otherwise, once per device:
```sh
adb shell settings put global settings_enable_monitor_phantom_procs false
```
`device.txt` records what this phone reports, and the Performance dialog says so when it is not
disabled.
## Authors
- **The412Banner** — the app, the Wayland compositor, the runtime plumbing and the Steam session.
- **maxjivi05 (Max)** — the gamescope runtime this is built on: the proot session, the session
shim, the fake-evdev interposer and the controller work, from WinNative.
## Lineage and licence
GPL-3.0. The gamescope runtime, the session shim and the fake-evdev interposer come from
maxjivi05's WinNative work and from Bannerlator, both GPL-3.0; the Wayland compositor, the frame
generation engines and the runtime plumbing are Bannerlator's. Underneath all of it is Winlator
(brunodev85, GPL-3.0): the PulseAudio-on-AAudio audio stack, the gamepad state model and the shape
of a session's host-side components are his, and WinNative and Bannerlator are both Winlator
lineage. Valve, Steam and Proton are Valve Corporation's; this project
is not affiliated with Valve.
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
+58 -7
View File
@@ -4,22 +4,68 @@ plugins {
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
def gitValue = { List<String> arguments ->
try {
def output = providers.exec {
commandLine(['git'] + arguments)
workingDir rootProject.projectDir
ignoreExitValue = true
}
output.result.get().exitValue == 0 ? output.standardOutput.asText.get().trim() : null
} catch (Exception ignored) {
null
}
}
def buildLabel = providers.environmentVariable('DROIDDECK_BUILD_LABEL').getOrNull()
if (buildLabel == null || buildLabel.trim().isEmpty()) {
def eventName = providers.environmentVariable('GITHUB_EVENT_NAME').getOrNull()
def eventPath = providers.environmentVariable('GITHUB_EVENT_PATH').getOrNull()
if (eventName == 'pull_request_target' && eventPath) {
try {
def eventFile = layout.file(providers.provider { new File(eventPath) })
def eventText = providers.fileContents(eventFile).asText.get()
def pullRequest = new groovy.json.JsonSlurper().parseText(eventText).pull_request
if (pullRequest?.number && pullRequest?.head?.sha) {
buildLabel = "pr${pullRequest.number} at ${pullRequest.head.sha.take(7)}"
}
} catch (Exception ignored) {
}
}
if (buildLabel == null || buildLabel.trim().isEmpty()) {
def branch = providers.environmentVariable('GITHUB_REF_NAME').getOrNull()
?: gitValue(['branch', '--show-current']) ?: 'local'
def commit = providers.environmentVariable('GITHUB_SHA').getOrNull()
?.take(7) ?: gitValue(['rev-parse', '--short=7', 'HEAD']) ?: 'unknown'
buildLabel = "${branch} at ${commit}"
}
}
def buildOrigin = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true' ? 'remote' : 'local'
def treeState = providers.environmentVariable('DROIDDECK_BUILD_TREE_STATE').getOrNull()
if (!(treeState in ['clean', 'dirty'])) {
def status = gitValue(['status', '--porcelain'])
treeState = status == null ? 'unknown' : (status.isEmpty() ? 'clean' : 'dirty')
}
buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}"
def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"')
android {
namespace 'com.steamdeck.launcher'
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.0.12077973'
ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724'
defaultConfig {
applicationId "com.steamdeck.launcher"
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 5
versionName "0.1.4"
versionCode 8
versionName "0.1.7"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
ndk {
abiFilters 'arm64-v8a'
@@ -40,7 +86,7 @@ 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
// 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 {
@@ -69,7 +115,7 @@ 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 —
// 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'
}
@@ -80,11 +126,14 @@ android {
// libraries are read out of the apk by the class loader: both want the old,
// uncompressed-and-extracted layout.
useLegacyPackaging = true
// Prefer the PTY helper built by this app's NDK target over Termux's prebuilt copy.
pickFirsts += ['**/libtermux.so']
}
}
buildFeatures {
compose true
buildConfig true
}
compileOptions {
@@ -111,4 +160,6 @@ dependencies {
implementation 'androidx.compose.material:material-icons-extended'
implementation 'io.coil-kt:coil-compose:2.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// Android terminal view/emulator used by the in-session secondary-display Linux shell.
implementation 'com.termux.termux-app:terminal-view:0.118.0'
}
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<!-- Debug builds only. Android grants DUMP to adb shell; release APKs have no bridge. -->
<activity
android:name=".agent.AgentStartActivity"
android:exported="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:permission="android.permission.DUMP"
android:theme="@style/SessionTheme" />
<provider
android:name=".agent.AgentBridgeProvider"
android:authorities="${applicationId}.agent"
android:exported="true"
android:permission="android.permission.DUMP"
android:grantUriPermissions="false" />
</application>
</manifest>
@@ -0,0 +1,166 @@
package com.droiddeck.launcher.agent
import android.Manifest
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Binder
import android.os.Bundle
import android.os.Process
import com.droiddeck.launcher.BuildConfig
import com.droiddeck.launcher.runtime.LinuxRuntime
import com.droiddeck.launcher.runtime.LinuxRuntimeInstaller
import com.droiddeck.launcher.session.SessionArtifacts
import com.droiddeck.launcher.session.SessionEvents
import com.droiddeck.launcher.session.SessionPhase
import com.droiddeck.launcher.session.SessionPaths
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.session.SessionState
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/** A shell-only control surface, installed only by debug builds. */
class AgentBridgeProvider : ContentProvider() {
override fun onCreate(): Boolean {
val appContext = context?.applicationContext ?: return false
Thread({
try {
SessionArtifacts.finishAbandoned(appContext)
} finally {
recoveryComplete.countDown()
}
}, "agent-recover-artifacts").start()
return true
}
override fun call(method: String, arg: String?, extras: Bundle?): Bundle {
val context = requireNotNull(context)
if (Binder.getCallingUid() != Process.myUid() &&
context.checkCallingPermission(Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED
) {
throw SecurityException("android.permission.DUMP is required")
}
val response = try {
when (method) {
"state" -> state(context).put("ok", true)
"start" -> error("USE_DROIDDECKCTL", "Start sessions with tools/droiddeckctl so Android launches a visible Activity")
"stop" -> stop(context)
"resume" -> resume(context)
else -> error("UNKNOWN_COMMAND", "Unknown agent command '$method'")
}
} catch (e: Exception) {
error("COMMAND_FAILED", e.message ?: e.javaClass.simpleName)
}
return Bundle().apply { putString(RESULT_JSON, response.toString()) }
}
private fun state(context: Context): JSONObject {
val runtimeVersion = LinuxRuntimeInstaller.installedVersion(context)
val dir = SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: latestSessionDirectory()
val session = JSONObject()
.put("id", SessionState.sessionId ?: dir?.name ?: JSONObject.NULL)
.put("phase", SessionState.phase.name)
.put("running", SessionState.running)
.put("mode", SessionState.mode)
.put("program", SessionState.program ?: JSONObject.NULL)
.put("steamUi", SessionState.steamUi ?: JSONObject.NULL)
.put("steamUrl", SessionState.steamUrl ?: JSONObject.NULL)
.put("suspended", SessionState.suspended)
.put("firstFrame", SessionState.firstFrameSeen)
.put("output", JSONArray().put(SessionState.outputSize.first).put(SessionState.outputSize.second))
.put("refreshHz", SessionState.refreshHz.toDouble())
.put("lastTransitionAt", SessionState.lastTransitionAt)
.put("guestPid", SessionState.guestPid.takeIf { it > 1 } ?: JSONObject.NULL)
.put("installing", SessionState.installing ?: JSONObject.NULL)
.put("logDir", dir?.absolutePath ?: JSONObject.NULL)
.put("eventsFile", dir?.let { File(it, "events.jsonl").absolutePath } ?: JSONObject.NULL)
.put("artifactsAvailable", dir?.isDirectory ?: false)
.put("artifactsComplete", dir?.let { File(it, ".complete").isFile } ?: false)
val failure = if (SessionState.failureCode == null) JSONObject.NULL else JSONObject()
.put("code", SessionState.failureCode)
.put("message", SessionState.failureMessage ?: JSONObject.NULL)
.put("status", SessionState.failureStatus ?: JSONObject.NULL)
session.put("failure", failure)
return JSONObject()
.put("schema", 1)
.put("build", BuildConfig.BUILD_LABEL)
.put("appVersion", context.packageManager.getPackageInfo(context.packageName, 0).versionName)
.put("runtime", JSONObject()
.put("installed", LinuxRuntime.isInstalled(context))
.put("version", runtimeVersion ?: JSONObject.NULL))
.put("session", session)
}
private fun latestSessionDirectory(): File? {
val parent = LinuxRuntime.debugLogDir()
return parent.listFiles { file -> file.isDirectory && file.name.startsWith("session-") }
?.maxWithOrNull(compareBy<File> { it.lastModified() }.thenBy { it.name })
}
private fun stop(context: Context): JSONObject {
if (SessionState.running) {
SessionService.stop(context)
return JSONObject().put("ok", true).put("command", "stop")
}
if (SessionState.phase == SessionPhase.STOPPING) {
return JSONObject().put("ok", true).put("command", "stop")
}
if (SessionState.phase in STARTING_PHASES) {
SessionState.stopRequested = true
SessionEvents.transition(SessionPhase.STOPPING, "session.stop_requested")
return JSONObject().put("ok", true).put("command", "stop")
}
return error("NO_ACTIVE_SESSION", "There is no session to stop")
}
private fun resume(context: Context): JSONObject {
if (!SessionState.running || !SessionState.suspended) {
return error("SESSION_NOT_SUSPENDED", "There is no suspended session to resume")
}
SessionService.resume(context)
return JSONObject().put("ok", true).put("command", "resume")
}
private fun error(code: String, message: String) = JSONObject()
.put("ok", false)
.put("error", JSONObject().put("code", code).put("message", message))
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int = 0
companion object {
const val RESULT_JSON = "json"
private val recoveryComplete = CountDownLatch(1)
fun awaitRecovery() {
try {
if (!recoveryComplete.await(90, TimeUnit.SECONDS)) {
throw IllegalStateException("Session artifact recovery did not finish")
}
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
throw IllegalStateException("Interrupted while recovering session artifacts", e)
}
}
private val STARTING_PHASES = setOf(
SessionPhase.PREPARING,
SessionPhase.INSTALLING_RUNTIME,
SessionPhase.STARTING_COMPOSITOR,
SessionPhase.STARTING_GUEST,
SessionPhase.STARTING_STEAM,
)
}
}
@@ -0,0 +1,99 @@
package com.droiddeck.launcher.agent
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Base64
import android.util.Log
import com.droiddeck.launcher.SessionActivity
import com.droiddeck.launcher.session.SessionService
import org.json.JSONArray
import org.json.JSONObject
/** Shell-launched trampoline: Android permits the shell to start this protected debug Activity. */
class AgentStartActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val request = decodeRequest(intent.getStringExtra(EXTRA_REQUEST))
if (request == null) {
finish()
return
}
Thread({
try {
AgentBridgeProvider.awaitRecovery()
runOnUiThread {
if (!isFinishing) startSession(request)
}
} catch (e: Exception) {
Log.e(TAG, "waiting for agent recovery", e)
runOnUiThread { finish() }
}
}, "agent-start-recovery").start()
}
private fun decodeRequest(encoded: String?): JSONObject? {
if (encoded.isNullOrBlank()) {
Log.e(TAG, "missing agent start request")
return null
}
return try {
JSONObject(String(Base64.decode(encoded, Base64.DEFAULT), Charsets.UTF_8))
} catch (e: Exception) {
Log.e(TAG, "invalid agent start request", e)
null
}
}
private fun startSession(request: JSONObject) {
val requestedMode = request.optString("mode")
val sessionMode = when (requestedMode) {
"steam" -> SessionService.MODE_STEAM
"desktop" -> SessionService.MODE_DESKTOP
"run" -> SessionService.MODE_RUN
else -> {
Log.e(TAG, "invalid agent session mode: $requestedMode")
finish()
return
}
}
val program = if (sessionMode == SessionService.MODE_RUN) {
request.optString("program").takeIf { it.isNotBlank() }
} else {
null
}
if (sessionMode == SessionService.MODE_RUN && program == null) {
Log.e(TAG, "run mode requires a program path")
finish()
return
}
val sessionIntent = Intent(this, SessionActivity::class.java).apply {
action = SessionService.ACTION_AGENT_START
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
putExtra(SessionService.EXTRA_MODE, sessionMode)
if (program != null) {
val args = request.optJSONArray("programArgs") ?: JSONArray()
putExtra(SessionService.EXTRA_PROGRAM, program)
putExtra(SessionService.EXTRA_PROGRAM_ARGS, Array(args.length()) { index -> args.optString(index) })
}
if (sessionMode == SessionService.MODE_STEAM) {
request.optString("steamUi").takeIf { it == "desktop" }?.let {
putExtra(SessionService.EXTRA_STEAM_UI, it)
}
request.optString("steamUrl").takeIf { it.startsWith("steam://") }?.let {
putExtra(SessionService.EXTRA_STEAM_URL, it)
}
}
}
startActivity(sessionIntent)
finish()
}
companion object {
const val EXTRA_REQUEST = "request"
private const val TAG = "AgentStartActivity"
}
}
+55 -4
View File
@@ -39,8 +39,8 @@
<!-- 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
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" />
@@ -54,6 +54,26 @@
</intent-filter>
</activity>
<!-- Android owns the Home key. This small translucent Home handler routes the resulting
Home launch either back into a running Steam session or to the regular front end.
Off until the user turns on "Use as a Home screen" (Setup): while it is disabled the
app is not a Home app at all - not offered, not in the "choose Home" list. -->
<activity
android:name=".HomeActivity"
android:enabled="false"
android:exported="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:taskAffinity="${applicationId}.home"
android:screenOrientation="sensorLandscape"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.HOME" />
</intent-filter>
</activity>
<!--
A session keeps a Surface, a compositor thread and a proot tree alive; a configuration
change must not tear that down and rebuild it, so the activity handles them itself.
@@ -61,9 +81,17 @@
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
fixed-shape window instead and STRETCHES it onto the other panel - the app never sees a
size change, so nothing it does can help.
-->
<activity
android:name=".input.ControllerEditorActivity"
android:exported="false"
android:theme="@style/SessionTheme"
android:screenOrientation="sensorLandscape"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".files.FileManagerActivity"
android:exported="false"
@@ -79,6 +107,7 @@
<activity
android:name=".SessionActivity"
android:exported="false"
android:launchMode="singleTop"
android:theme="@style/SessionTheme"
android:preferMinimalPostProcessing="true"
android:screenOrientation="sensorLandscape"
@@ -86,7 +115,7 @@
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<!--
Owns the session — gamescope, the Steam client, audio — so it survives the activity
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.
@@ -95,5 +124,27 @@
android:name=".session.SessionService"
android:exported="false"
android:stopWithTask="false" />
<!-- Hands a zipped session folder to the share sheet; nothing else is exposed. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.logs"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/log_share_paths" />
</provider>
<!-- The app's data folder in Android's file picker ("Open from" > DroidDeck). Only the
system's documents UI may bind it; apps get a file only when the user picks it. -->
<provider
android:name=".files.AppFilesProvider"
android:authorities="${applicationId}.documents"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
</manifest>
Binary file not shown.
+11 -7
View File
@@ -1,7 +1,7 @@
# 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(steamdeck C CXX)
project(droiddeck C CXX)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2 -Wno-unused-function -Wimplicit-function-declaration")
@@ -24,12 +24,12 @@ add_subdirectory(thirdparty/dxbc)
# rot. The compositor's frame-generation leg (waylandcomp/CMakeLists.txt) stamps both in.
set(WINFG_UPSTREAM "v0.3.0")
set(WINFG_CHAIN_SRC
winlator/winfg/framegen.cpp
winlator/winfg/framegen.hpp
winlator/winfg/record_impl.inc
winlator/winfg/config.hpp
winlator/winfg/embedded_shaders.hpp
winlator/winfg/winfg_engine.cpp
framegen/winfg/framegen.cpp
framegen/winfg/framegen.hpp
framegen/winfg/record_impl.inc
framegen/winfg/config.hpp
framegen/winfg/embedded_shaders.hpp
framegen/winfg/winfg_engine.cpp
)
set(WINFG_HASH_INPUT "")
foreach(f ${WINFG_CHAIN_SRC})
@@ -43,3 +43,7 @@ message(STATUS "win-fg chain: ${WINFG_UPSTREAM} (src ${WINFG_CHAIN_HASH})")
add_subdirectory(waylandcomp)
add_library(sdnative SHARED sdnative/sdnative.c)
target_link_libraries(sdnative log)
# 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)
+8 -5
View File
@@ -294,7 +294,8 @@ send_vibration(int strong, int weak, uint16_t duration_ms, uint16_t slot) {
if (!fake_vibration_enabled())
return;
// Rumble is best effort. A full Android listener backlog must never park
// Rumble is best effort: the app listens on the abstract socket below (RumbleComponent) and
// drives the phone's vibrator. A full listener backlog must never park
// winebus (or every input hook through controller_mutex()), and a closing
// listener must not terminate the guest with SIGPIPE.
int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
@@ -303,7 +304,7 @@ send_vibration(int strong, int weak, uint16_t duration_ms, uint16_t slot) {
struct sockaddr_un addr = {};
addr.sun_family = AF_UNIX;
const char *name = "winlator_vibration";
const char *name = "droiddeck-rumble";
memcpy(addr.sun_path + 1, name, strlen(name));
socklen_t addrlen = offsetof(struct sockaddr_un, sun_path) + 1 + strlen(name);
@@ -1230,6 +1231,8 @@ EXPORT int close(int fd) {
return my_close(fd);
}
static constexpr int kMaxRingWaitMs = 2;
EXPORT ssize_t read(int fd, void *buf, size_t count) {
std::unique_lock<std::recursive_mutex> guard(controller_mutex());
auto controller = controller_map().find(fd);
@@ -1322,7 +1325,7 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) {
int result = nanosleep(&sleep_time, nullptr);
guard.lock();
if (result < 0) return -1;
if (backoff_ns < 16 * 1000 * 1000) backoff_ns *= 2;
if (backoff_ns < kMaxRingWaitMs * 1000 * 1000) backoff_ns *= 2;
}
}
@@ -1468,7 +1471,7 @@ static int poll_fake(struct pollfd *fds, nfds_t nfds, int timeout,
if (deadline_ms >= 0 && monotonic_ms() >= deadline_ms)
return 0;
if (backoff_ms < 16)
if (backoff_ms < kMaxRingWaitMs)
backoff_ms *= 2;
}
}
@@ -1643,7 +1646,7 @@ EXPORT int select(int nfds, fd_set *readfds, fd_set *writefds,
if (deadline_ms >= 0 && monotonic_ms() >= deadline_ms)
return 0;
if (backoff_ms < 16)
if (backoff_ms < kMaxRingWaitMs)
backoff_ms *= 2;
}
}
@@ -108,10 +108,10 @@ struct VkTable {
#include <android/log.h>
#include <string>
#define WLOG_TAG "Winlator_Renderer"
#define WLOG_TAG "DroidDeck_Renderer"
#define RLOG(...) if(verboseLog) __android_log_print(ANDROID_LOG_DEBUG,WLOG_TAG,__VA_ARGS__)
#define RLOG_E(...) __android_log_print(ANDROID_LOG_ERROR,WLOG_TAG,__VA_ARGS__)
#define SCANOUT_LOG(...) __android_log_print(ANDROID_LOG_DEBUG,"Winlator_Scanout",__VA_ARGS__)
#define SCANOUT_LOG(...) __android_log_print(ANDROID_LOG_DEBUG,"DroidDeck_Scanout",__VA_ARGS__)
#include <vulkan/vulkan_android.h>
#include <android/hardware_buffer.h>
@@ -129,7 +129,7 @@ struct VkTable {
// Included after SCANOUT_LOG is defined above; its own definition is guarded.
#include "../scanout/ScanoutContext.h"
// Native (compositor-side) LSFG frame generation — capability gate.
// Native (compositor-side) LSFG frame generation - capability gate.
#include "lsfg/lsfg_probe.h"
#include <memory>
#include <string>
@@ -139,8 +139,6 @@ namespace winfg { class Engine; }
static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2;
struct WindowPushConstants { float ndcX0, ndcY0, ndcX1, ndcY1; int useTexAlpha; };
// 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.
@@ -164,11 +162,6 @@ struct RcasPushConstants { // 40 bytes
uint32_t con[4]; // con.x = bit-packed sharpness
float outW, outH;
};
struct DownscalePushConstants { // 32 bytes
float ndc[4];
float srcW, srcH; // render (source) resolution
float dstW, dstH; // display (output) resolution
};
// 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
@@ -493,8 +486,8 @@ private:
// 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
// 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 {
@@ -741,7 +734,6 @@ private:
NisPushConstants nisPC{};
EasuPushConstants easuPC{};
RcasPushConstants rcasPC{};
DownscalePushConstants dsPC{};
CasPushConstants casPC{};
HdrPushConstants hdrPC{};
FxaaPushConstants fxaaPC{};
@@ -315,7 +315,7 @@ 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
// 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) {
@@ -1,12 +1,12 @@
#pragma once
// ============================================================================
// lsfg_dll — extract the Lossless Scaling frame-generation shader chain from
// 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
// 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. Bannerlator does the same, and
// bundles nothing.
//
@@ -18,8 +18,8 @@
// 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
// 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.
//
@@ -95,7 +95,7 @@ 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.
// reported as valid - the translator handles it.
DllStatus validateDll(const std::string& dllPath);
// Which producer would be used for this DLL.
@@ -1,11 +1,11 @@
#pragma once
// ============================================================================
// lsfg_dxbc — DXBC -> SPIR-V for the Lossless Scaling shader chain.
// 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
// 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
@@ -16,8 +16,8 @@
// 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
// 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.
@@ -30,7 +30,7 @@ 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
// 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).
@@ -1,6 +1,6 @@
#pragma once
// ============================================================================
// lsfg_engine — the compositor's handle on native LSFG frame generation.
// 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:
@@ -1,6 +1,6 @@
#pragma once
// ============================================================================
// lsfg_governor — decides whether an extra generated frame is actually WORTH
// 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
@@ -23,25 +23,25 @@ std::string toStdString(JNIEnv* env, jstring s) {
} // namespace
extern "C" JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeValidateDll(JNIEnv* env, jclass, jstring dllPath) {
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeValidateDll(JNIEnv* env, jclass, jstring dllPath) {
return (jint)lsfg::validateDll(toStdString(env, dllPath));
}
extern "C" JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeDllVariant(JNIEnv* env, jclass, jstring dllPath,
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeDllVariant(JNIEnv* env, jclass, jstring dllPath,
jboolean preferFp16) {
return (jint)lsfg::dllVariant(toStdString(env, dllPath), preferFp16 == JNI_TRUE);
}
extern "C" JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeBuildCache(JNIEnv* env, jclass, jstring dllPath,
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeBuildCache(JNIEnv* env, jclass, jstring dllPath,
jstring cachePath, jboolean preferFp16) {
return (jint)lsfg::buildCache(toStdString(env, dllPath), toStdString(env, cachePath),
preferFp16 == JNI_TRUE);
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeCacheMatchesSource(JNIEnv* env, jclass,
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeCacheMatchesSource(JNIEnv* env, jclass,
jstring cachePath,
jstring dllPath) {
bool matches = false;
@@ -51,7 +51,7 @@ Java_com_steamdeck_launcher_gpu_LsfgNative_nativeCacheMatchesSource(JNIEnv* env,
}
extern "C" JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeCacheVariant(JNIEnv* env, jclass, jstring cachePath) {
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeCacheVariant(JNIEnv* env, jclass, jstring cachePath) {
lsfg::Variant variant = lsfg::Variant::None;
if (lsfg::cacheVariant(toStdString(env, cachePath), variant) != lsfg::DllStatus::Ok)
return (jint)lsfg::Variant::None;
@@ -59,11 +59,11 @@ Java_com_steamdeck_launcher_gpu_LsfgNative_nativeCacheVariant(JNIEnv* env, jclas
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeStatusName(JNIEnv* env, jclass, jint status) {
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeStatusName(JNIEnv* env, jclass, jint status) {
return env->NewStringUTF(lsfg::statusName((lsfg::DllStatus)status));
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_steamdeck_launcher_gpu_LsfgNative_nativeVariantName(JNIEnv* env, jclass, jint variant) {
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeVariantName(JNIEnv* env, jclass, jint variant) {
return env->NewStringUTF(lsfg::variantName((lsfg::Variant)variant));
}
@@ -110,7 +110,7 @@ bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt) {
// 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 —
// 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
@@ -1,6 +1,6 @@
#pragma once
// ============================================================================
// lsfg_probe — capability gate for native (compositor-side) LSFG frame
// lsfg_probe - capability gate for native (compositor-side) LSFG frame
// generation.
//
// The Lossless Scaling chain is 25 compute shaders that DXVK's DXBC translator
@@ -18,7 +18,7 @@
// 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
// 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
@@ -85,7 +85,7 @@ struct Caps {
// 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
// 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
@@ -1,6 +1,6 @@
#pragma once
// ============================================================================
// lsfg_vkd — the Vulkan entry points the LSFG chain calls, as a single global.
// lsfg_vkd - the Vulkan entry points the LSFG chain calls, as a single global.
//
// Two dispatch styles meet here. Bannerlator's compositor resolves Vulkan
// through a per-context VkTable (`vk_`), because an adrenotools-loaded driver
@@ -64,7 +64,7 @@ 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
// 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();
@@ -1,4 +1,4 @@
# NOTICE — FidelityFX Optical Flow (model 3)
# NOTICE - FidelityFX Optical Flow (model 3)
win-fg "model 3" (FSR3 Optical Flow) embeds compute shaders that are an
adaptation of the **AMD FidelityFX SDK FSR3 Optical Flow** algorithm.
@@ -1,4 +1,4 @@
// win-fg — runtime config: enable gate, model, multiplier, and the synthesis
// 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
@@ -17,7 +17,7 @@ struct Config {
// 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
// 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 debug = false;
@@ -25,11 +25,11 @@ struct Config {
// 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").
// 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
// 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
@@ -49,7 +49,7 @@ struct Config {
// 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
// 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 extraImages = 2;
@@ -62,7 +62,7 @@ struct Config {
// 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 /
// 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
@@ -76,8 +76,8 @@ struct Config {
// 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
// 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.
@@ -91,7 +91,7 @@ struct Config {
// 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
// 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
@@ -107,7 +107,7 @@ struct Config {
// 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 —
// (hudRect="x0,y0,x1,y1"). Pattern is Isygold's Vegas DXVK framegen -
// host knows where the HUD is, tell the shader to skip it.
float hudX0 = 0.0f;
float hudY0 = 0.0f;
@@ -116,11 +116,11 @@ struct Config {
// ── 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,
// 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
// 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;
@@ -197,7 +197,7 @@ static inline int parse_capmode(const std::string& v, int dflt) {
return dflt;
}
// key=value TOML-lite reader (flat keys, '#' comments) — enough for our knobs.
// 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;
@@ -303,7 +303,7 @@ 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
// "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,17 +321,17 @@ static inline Config load_config() {
return c;
}
// UBO layout — MUST match wfg_synth.comp binding 0.
// 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).
// 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 /
// 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).
@@ -345,13 +345,13 @@ struct GMUBO {
float appT[4]; // tx,ty, pad, pad
};
// C2 flow-regularization UBO — MUST match binding 0 (vec4 params) in
// 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; // 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
};
} // namespace winfg
@@ -1,4 +1,4 @@
// GENERATED by tools/embed_spv.py — do not edit by hand.
// 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>
@@ -1,4 +1,4 @@
// win-fg — compute frame-generation engine implementation.
// win-fg - compute frame-generation engine implementation.
#include "framegen.hpp"
#include "embedded_shaders.hpp"
#include "log.hpp"
@@ -51,7 +51,7 @@ void FrameGen::destroyImage(Img& i) {
// C1: allocate the per-slot host-visible SSBOs the LK reduce writes partials into.
// Host-visible + coherent so the CPU can read them back after the owning frame's
// fence signals (no staging copy needed; the buffers are tiny — kGmThreads*kGmStride
// fence signals (no staging copy needed; the buffers are tiny - kGmThreads*kGmStride
// floats each, ~64 KB). Persistently mapped.
bool FrameGen::makeGmBuffers() {
const VkDeviceSize sz = (VkDeviceSize)kGmThreads * kGmStride * sizeof(float);
@@ -167,7 +167,7 @@ bool FrameGen::init(const DeviceDispatch* dd, const InstanceDispatch* id,
if (!makePipe(pGmPrewarp_, embedded::OF3_GM_PREWARP, unused)) return false;
if (!makePipe(pFlowReg_, embedded::OF3_FLOWREG, unused)) return false;
if (!makePipe(pSynth_, embedded::WFG_SYNTH, unused)) return false;
if (!makeGmBuffers()) { WFG_LOGE("framegen: GM SSBO alloc failed — C1 disabled"); }
if (!makeGmBuffers()) { WFG_LOGE("framegen: GM SSBO alloc failed - C1 disabled"); }
WFG_LOGI("framegen init ok (queueFamily=%u, 10 pipelines, model=%d, C1 gm=%d, C2 flow_reg=%d iters=%d)",
queueFamily_, cfg_.model, cfg_.gmMode, cfg_.frMode, cfg_.frIters);
return true;
@@ -192,7 +192,7 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force)
const VkImageUsageFlags lumaUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
// flowLvl_ is cleared every frame (per-frame reset) and C2 copies the cleaned
// finest level back into it — both need TRANSFER_DST. The C2 ping-pong images
// finest level back into it - both need TRANSFER_DST. The C2 ping-pong images
// are the copy SOURCE (TRANSFER_SRC).
const VkImageUsageFlags flowUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
const VkImageUsageFlags regUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
@@ -203,7 +203,7 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force)
if (!makeImage(pyrB_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
if (!makeImage(flowLvl_[l], e, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false;
}
// C1: stabilized curr luma — only the levels the dense flow reads (finest..coarsest).
// C1: stabilized curr luma - only the levels the dense flow reads (finest..coarsest).
pyrAs_.resize(kLevels);
for (int l = flowFinest_; l < kLevels; ++l)
if (!makeImage(pyrAs_[l], levelExtent(extent, l), VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
@@ -1,4 +1,4 @@
// win-fg — compute frame-generation engine.
// win-fg - compute frame-generation engine.
// Owns the compute pipelines (our of3_* optical flow + wfg_synth) and the
// per-resolution intermediate images, and records the flow+synth graph that
// turns (prev, curr) into an interpolated frame. Written for win-fg; no code
@@ -17,7 +17,7 @@ public:
// Applies a new config. Recomputes the active finest flow level from
// cfg_.perfPreset; if it changed while resources are live, safely rebuilds the
// affected per-size flow images (like a resize) and resets the flow predictor,
// so a perf_preset change is FULLY LIVE — no FG toggle needed. Defined in
// so a perf_preset change is FULLY LIVE - no FG toggle needed. Defined in
// record_impl.inc (needs the scratch-state teardown). See flowFinestForPreset.
void configure(const Config& c);
// (Re)build per-resolution resources for a swapchain of the given size/format.
@@ -30,7 +30,7 @@ public:
// gmSlot selects which C1 global-motion SSBO to reduce into / read back; the
// caller MUST pass the ring-slot index of the FrameCtx it just fence-waited
// (that guarantees this slot's PREVIOUS reduce has completed and is safe to
// read on the host — see the decoupled-LK note in record_impl.inc).
// read on the host - see the decoupled-LK note in record_impl.inc).
void record(VkCommandBuffer cmd, VkImageView prevView, VkImageView currView,
VkImageView outView, float alpha, uint32_t gmSlot = 0);
void destroy();
@@ -54,7 +54,7 @@ public:
// 2 Performance=3). kFlowFinestDefault is the Balanced value (today's behaviour);
// the ACTIVE level lives in flowFinest_ and is read through flowFinest(). onResize
// sizes the stabilized-curr pyramid + C2 scratch from it, and record_impl.inc
// wires the flow/expand inputs to it — both must agree, so they all read
// wires the flow/expand inputs to it - both must agree, so they all read
// flowFinest()/flowFinest_ (never a hard-coded 2). (Was a static constexpr = 2.)
static constexpr int kFlowFinestDefault = 2; // Balanced (perf_preset=1)
// Map perf_preset -> finest solved level. Out-of-range -> Balanced. A higher
@@ -1,4 +1,4 @@
// win-fg — logging. On Android goes to logcat (tag "win-fg") so device bring-up
// win-fg - logging. On Android goes to logcat (tag "win-fg") so device bring-up
// can trace the layer; elsewhere falls back to stderr.
#pragma once
#if defined(__ANDROID__)
@@ -12,7 +12,7 @@
#endif
// Runtime-gated granular debug trace. The gate is a RUNTIME bool (Config::debug),
// not a compile-time switch — a shipping .so carries the full trail but pays only
// not a compile-time switch - a shipping .so carries the full trail but pays only
// a predicted-not-taken bool test per step when debug is off (no logcat write, no
// arg evaluation of the message). Same "win-fg" tag so the app captures it under
// its own UID. Used to step the present path so a freeze leaves an obvious last
@@ -1,4 +1,4 @@
// win-fg — FrameGen::record() and its lazy per-size descriptor/UBO setup.
// win-fg - FrameGen::record() and its lazy per-size descriptor/UBO setup.
// Included from framegen.cpp (has access to FrameGen privates). Written for
// win-fg; standard Vulkan compute wiring, no third-party code.
//
@@ -12,12 +12,12 @@
// the global affine back so wfg_synth is unchanged and the final warp is correct.
//
// DECOUPLED (stall-free) SCHEDULING: a true in-frame CPU solve would need a
// mid-frame GPU submit+readback stall — unacceptable on a GPU-bound title. So we
// mid-frame GPU submit+readback stall - unacceptable on a GPU-bound title. So we
// read back the reduce SSBO the caller ALREADY fence-waited (its ring slot from a
// couple of presents ago) and run ONE Gauss-Newton step per frame, warm-started
// from the running estimate. Camera motion is temporally smooth, so a ~1-2 frame
// old affine is an excellent predictor; and because the SAME affine is removed
// (prewarp) and added back (expand), the result is EXACT for any affine we pick —
// (prewarp) and added back (expand), the result is EXACT for any affine we pick -
// an approximate/stale one only changes how much residual the search must absorb,
// never correctness. Identity affine ⇒ byte-identical to the pre-C1 pipeline, so
// the bail-to-identity safety path can never be worse than "no pre-warp".
@@ -31,7 +31,7 @@ struct ScratchState {
// C2: flow-regularization UBO (shared by all iterations; refreshed per frame).
VkBuffer flowRegUbo = VK_NULL_HANDLE; VkDeviceMemory flowRegUboMem = VK_NULL_HANDLE; void* flowRegUboPtr = nullptr;
// C1: per-slot global-motion UBO (double-buffered by ring slot so a live GPU
// read is never overwritten by the next frame — see the header note).
// read is never overwritten by the next frame - see the header note).
VkBuffer gmUbo[winfg::FrameGen::kGmSlots]{};
VkDeviceMemory gmUboMem[winfg::FrameGen::kGmSlots]{};
void* gmUboPtr[winfg::FrameGen::kGmSlots]{};
@@ -170,7 +170,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
// Tear down the lazily-built scratch (all UBO buffers + every descriptor set) so a
// live perf_preset rebuild neither leaks them nor exhausts the descriptor pool.
// The CALLER must have made the device idle first — no in-flight command buffer may
// The CALLER must have made the device idle first - no in-flight command buffer may
// still reference these. Resetting the pool frees every set FrameGen allocated (all
// sets come from this pool); the mapped UBO memory is unmapped + freed explicitly.
static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorPool pool) {
@@ -191,8 +191,8 @@ static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorP
}
// ── perf_preset apply / live rebuild ─────────────────────────────────────────
// Sets the config, resolves the perf_preset -> finest-solved-level, and — when
// that level changed while resources are already live — safely rebuilds the
// Sets the config, resolves the perf_preset -> finest-solved-level, and - when
// that level changed while resources are already live - safely rebuilds the
// affected per-size flow images (a resize) + scratch descriptor sets and resets
// the flow predictor. This makes a perf_preset change FULLY LIVE per-present (the
// conf.toml hot-reload path calls this), so no FG toggle / bg-fg cycle is needed.
@@ -205,7 +205,7 @@ void FrameGen::configure(const Config& c) {
const int maxFinest = kLevels - 2; // keep >= 2 solved levels
if (want > maxFinest) {
WFG_LOGE("perf-preset=%d wants kFlowFinest=%d but only %d pyramid levels exist "
"(need >=2 solved) — clamping to %d", cfg_.perfPreset, want, kLevels, maxFinest);
"(need >=2 solved) - clamping to %d", cfg_.perfPreset, want, kLevels, maxFinest);
want = maxFinest;
}
if (want < 1) want = 1; // never solve full-res (level 0)
@@ -224,13 +224,13 @@ void FrameGen::configure(const Config& c) {
dd_->DeviceWaitIdle(device_); // no in-flight cmd buf may touch these
destroyScratch(dd_, device_, descPool_); // free old scratch (UBOs + all sets)
if (!onResize(extent_, VK_FORMAT_UNDEFINED, /*force=*/true))
WFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u — FG disabled until next resize",
WFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u - FG disabled until next resize",
extent_.width, extent_.height);
// Reset the persistent flow predictor so stale, differently-scaled state never
// feeds the new resolution (avoids a melt/garbage frame on switch). The per-frame
// flow field (flowLvl_) is already cleared every frame; the cross-frame state is
// the C1 global-motion estimate + its per-slot reduce partials (dispatched against
// the old pyramid) — drop both back to identity.
// the old pyramid) - drop both back to identity.
for (int i = 0; i < 6; ++i) gmRun_[i] = 0.0;
for (int s = 0; s < kGmSlots; ++s) { gmSlotValid_[s] = false; for (int i = 0; i < 6; ++i) gmLinAt_[s][i] = 0.0; }
gmHaveEst_ = false; gmEngaged_ = false; gmStableCount_ = 0; gmLastTransPx_ = 0.0f;
@@ -387,7 +387,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
// Reset the flow field to zero every frame. The coarsest level uses it as its
// search predictor; carrying it across frames lets a spurious vector feed
// forward until it diverges and melts the image (the bg/fg pause was resetting
// it — that's why artifacts vanished on resume). Fresh zero every frame =
// it - that's why artifacts vanished on resume). Fresh zero every frame =
// stable, standard pyramid flow, no feedback loop.
{
VkClearColorValue zero{};
@@ -436,7 +436,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
disp(pGmPrewarp_, g_scratch.sGmPrewarp[gmSlot][l], levelExtent(extent_, l));
bar(pyrAs_[l]);
}
// 3) coarse->fine flow (only down to flowFinest_ — reduced res for perf; the
// 3) coarse->fine flow (only down to flowFinest_ - reduced res for perf; the
// finest level is perf_preset-selectable). Curr input is now the STABILIZED
// pyramid (pyrAs), wired in ensureScratch.
for (int l = kLevels - 1; l >= flowFinest_; --l) {
@@ -575,7 +575,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
wImg(dd, dev, S.sFlow[l], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowLvl[l].view, VK_NULL_HANDLE, GEN);
}
// C1: reduce sets (one per slot) — prev/curr raw luma at kGmLevel + SSBO + GMUBO.
// C1: reduce sets (one per slot) - prev/curr raw luma at kGmLevel + SSBO + GMUBO.
if (gmSsbo) {
for (int s = 0; s < FrameGen::kGmSlots; ++s) {
if (!gmSsbo[s]) continue;
@@ -587,7 +587,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
}
}
// C1: prewarp sets (one per slot, per solved flow level) — raw curr in, stabilized out.
// C1: prewarp sets (one per slot, per solved flow level) - raw curr in, stabilized out.
for (int s = 0; s < FrameGen::kGmSlots; ++s) {
S.sGmPrewarp[s].assign(kLevels, VK_NULL_HANDLE);
for (int l = fg->flowFinest(); l < kLevels; ++l) {
@@ -637,7 +637,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
S.sSynth = allocSet(dd, dev, pool, pSynth.setLayout);
wUbo(dd, dev, S.sSynth, 0, S.synthUbo, sizeof(SynthUBO));
// GENERAL (not SR): these internal flow images stay in GENERAL across the whole
// record() (written as storage by expand, sampled here) — see the discard
// record() (written as storage by expand, sampled here) - see the discard
// transition at the top of record().
wImg(dd, dev, S.sSynth, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expA.view, samp, GEN);
wImg(dd, dev, S.sSynth, 35, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expB.view, samp, GEN);
@@ -1,4 +1,4 @@
// win-fg — minimal Vulkan layer dispatch tables.
// win-fg - minimal Vulkan layer dispatch tables.
// Structure follows the public Khronos loader/layer interface and the
// renderdoc "Vulkan layer guide" skeleton (learning references, Apache-2.0 /
// CC-BY). No code from bionic-fg / lsfg / GameScope.
@@ -21,10 +21,10 @@ struct InstanceDispatch {
PFN_vkGetPhysicalDeviceMemoryProperties GetPhysicalDeviceMemoryProperties = nullptr;
PFN_vkGetPhysicalDeviceQueueFamilyProperties GetPhysicalDeviceQueueFamilyProperties = nullptr;
// Device-context logging (affected-device fingerprint). Properties2 is core 1.1;
// may be null on a 1.0 instance — always null-check before calling.
// may be null on a 1.0 instance - always null-check before calling.
PFN_vkGetPhysicalDeviceProperties GetPhysicalDeviceProperties = nullptr;
PFN_vkGetPhysicalDeviceProperties2 GetPhysicalDeviceProperties2 = nullptr;
// Surface caps — used to clamp the extra-image-headroom request to the surface's
// Surface caps - used to clamp the extra-image-headroom request to the surface's
// maxImageCount so the bump never exceeds what the driver can grant.
PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR GetPhysicalDeviceSurfaceCapabilitiesKHR = nullptr;
};
@@ -38,7 +38,7 @@ struct DeviceDispatch {
PFN_vkQueueSubmit QueueSubmit = nullptr;
// Synchronization2 submit path (core 1.3 + KHR alias). DXVK may use either the
// legacy QueueSubmit or QueueSubmit2/2KHR; both are hooked for the guest-submit
// diagnostic. May be null if the device did not enable them — always null-check.
// diagnostic. May be null if the device did not enable them - always null-check.
PFN_vkQueueSubmit2 QueueSubmit2 = nullptr;
PFN_vkQueueSubmit2KHR QueueSubmit2KHR = nullptr;
PFN_vkQueueWaitIdle QueueWaitIdle = nullptr;
@@ -50,8 +50,8 @@ struct DeviceDispatch {
PFN_vkGetSwapchainImagesKHR GetSwapchainImagesKHR = nullptr;
PFN_vkAcquireNextImageKHR AcquireNextImageKHR = nullptr;
// Alternate acquire entry (VK_KHR_swapchain 1.1+ / device-group). DXVK may use
// either AcquireNextImageKHR or AcquireNextImage2KHR — both hooked for the
// guest-acquire diagnostic. May be null — always null-check.
// either AcquireNextImageKHR or AcquireNextImage2KHR - both hooked for the
// guest-acquire diagnostic. May be null - always null-check.
PFN_vkAcquireNextImage2KHR AcquireNextImage2KHR = nullptr;
PFN_vkQueuePresentKHR QueuePresentKHR = nullptr;
@@ -84,7 +84,7 @@ struct DeviceDispatch {
PFN_vkDestroyPipeline DestroyPipeline = nullptr;
PFN_vkCreateDescriptorPool CreateDescriptorPool = nullptr;
PFN_vkDestroyDescriptorPool DestroyDescriptorPool = nullptr;
// Frees every set in the pool at once — used by FrameGen::configure() to reclaim
// Frees every set in the pool at once - used by FrameGen::configure() to reclaim
// the old scratch descriptor sets when a perf_preset change forces a live rebuild
// (otherwise repeated preset switches would exhaust the pool). May be null in
// theory; the rebuild path null-checks before calling.
@@ -1,6 +1,6 @@
#pragma once
// ============================================================================
// winfg_engine — win-fg (Bannerlator's own frame generation) running inside
// winfg_engine - win-fg (Bannerlator's own frame generation) running inside
// the compositor, behind the same contract the LSFG engine uses:
//
// prepare(w, h, format) build/rebuild the chain for this size
@@ -1,6 +1,6 @@
#pragma once
// ============================================================================
// winfg_vkd — fill win-fg's own dispatch tables from the renderer's VkTable.
// winfg_vkd - fill win-fg's own dispatch tables from the renderer's VkTable.
//
// win-fg's chain (framegen.cpp / record_impl.inc) was written as a Vulkan
// LAYER and calls through winfg::DeviceDispatch / InstanceDispatch. Inside the
+1 -1
View File
@@ -22,7 +22,7 @@
#include <cstdint>
#ifndef SCANOUT_LOG
#define SCANOUT_LOG(...) __android_log_print(ANDROID_LOG_DEBUG,"Winlator_Scanout",__VA_ARGS__)
#define SCANOUT_LOG(...) __android_log_print(ANDROID_LOG_DEBUG,"DroidDeck_Scanout",__VA_ARGS__)
#endif
class ScanoutContext {
+1 -1
View File
@@ -10,7 +10,7 @@
#include <stdatomic.h>
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_input_FakeInputWriter_nativeStoreFence(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_input_FakeInputWriter_nativeStoreFence(JNIEnv *env, jclass clazz) {
(void)env;
(void)clazz;
atomic_thread_fence(memory_order_release);
-133
View File
@@ -1,133 +0,0 @@
// 1116.2.0
#pragma once
const uint32_t downscale_code[] = {
0x07230203,0x00010000,0x0008000b,0x000000b5,0x00000000,0x00020011,0x00000001,0x00020011,
0x00000032,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,
0x00000000,0x00000001,0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x00000046,
0x000000ab,0x00030010,0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,
0x00000004,0x6e69616d,0x00000000,0x00060005,0x0000000a,0x636e616c,0x32736f7a,0x3b316628,
0x00000000,0x00030005,0x00000009,0x00000078,0x00030005,0x0000001d,0x00007870,0x00040005,
0x00000031,0x6c616373,0x00000065,0x00030005,0x00000033,0x00004350,0x00040006,0x00000033,
0x00000000,0x0063646e,0x00050006,0x00000033,0x00000001,0x53637273,0x00657a69,0x00050006,
0x00000033,0x00000002,0x53747364,0x00657a69,0x00030005,0x00000035,0x00006370,0x00050005,
0x00000041,0x53766e69,0x656c6163,0x00000000,0x00030005,0x00000044,0x00000063,0x00060005,
0x00000046,0x67617266,0x43786554,0x64726f6f,0x00000000,0x00040005,0x0000004d,0x65736162,
0x00000000,0x00030005,0x00000051,0x00007a73,0x00030005,0x00000055,0x00637273,0x00030005,
0x0000005c,0x00636361,0x00040005,0x0000005e,0x6d757377,0x00000000,0x00030005,0x00000060,
0x00007964,0x00030005,0x0000006a,0x00007864,0x00030005,0x00000072,0x00000069,0x00030005,
0x00000078,0x00000074,0x00030005,0x00000081,0x00000077,0x00040005,0x00000082,0x61726170,
0x0000006d,0x00040005,0x00000088,0x61726170,0x0000006d,0x00030005,0x00000093,0x00006969,
0x00050005,0x000000ab,0x4374756f,0x726f6c6f,0x00000000,0x00030047,0x00000033,0x00000002,
0x00050048,0x00000033,0x00000000,0x00000023,0x00000000,0x00050048,0x00000033,0x00000001,
0x00000023,0x00000010,0x00050048,0x00000033,0x00000002,0x00000023,0x00000018,0x00040047,
0x00000046,0x0000001e,0x00000000,0x00040047,0x00000055,0x00000021,0x00000000,0x00040047,
0x00000055,0x00000022,0x00000000,0x00040047,0x000000ab,0x0000001e,0x00000000,0x00020013,
0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,0x00040020,
0x00000007,0x00000007,0x00000006,0x00040021,0x00000008,0x00000006,0x00000007,0x0004002b,
0x00000006,0x0000000f,0x3727c5ac,0x00020014,0x00000010,0x0004002b,0x00000006,0x00000014,
0x3f800000,0x0004002b,0x00000006,0x00000017,0x40000000,0x0004002b,0x00000006,0x0000001b,
0x00000000,0x0004002b,0x00000006,0x0000001e,0x40490fdb,0x0004002b,0x00000006,0x00000025,
0x3f000000,0x00040017,0x0000002f,0x00000006,0x00000002,0x00040020,0x00000030,0x00000007,
0x0000002f,0x00040017,0x00000032,0x00000006,0x00000004,0x0005001e,0x00000033,0x00000032,
0x0000002f,0x0000002f,0x00040020,0x00000034,0x00000009,0x00000033,0x0004003b,0x00000034,
0x00000035,0x00000009,0x00040015,0x00000036,0x00000020,0x00000001,0x0004002b,0x00000036,
0x00000037,0x00000001,0x00040020,0x00000038,0x00000009,0x0000002f,0x0004002b,0x00000036,
0x0000003b,0x00000002,0x0005002c,0x0000002f,0x0000003f,0x00000014,0x00000014,0x00040020,
0x00000045,0x00000001,0x0000002f,0x0004003b,0x00000045,0x00000046,0x00000001,0x00040017,
0x0000004b,0x00000036,0x00000002,0x00040020,0x0000004c,0x00000007,0x0000004b,0x00090019,
0x00000052,0x00000006,0x00000001,0x00000000,0x00000000,0x00000000,0x00000001,0x00000000,
0x0003001b,0x00000053,0x00000052,0x00040020,0x00000054,0x00000000,0x00000053,0x0004003b,
0x00000054,0x00000055,0x00000000,0x0004002b,0x00000036,0x00000057,0x00000000,0x00040017,
0x0000005a,0x00000006,0x00000003,0x00040020,0x0000005b,0x00000007,0x0000005a,0x0006002c,
0x0000005a,0x0000005d,0x0000001b,0x0000001b,0x0000001b,0x00040020,0x0000005f,0x00000007,
0x00000036,0x0004002b,0x00000036,0x00000061,0xfffffffc,0x0004002b,0x00000036,0x00000068,
0x00000004,0x0005002c,0x0000002f,0x0000007b,0x00000025,0x00000025,0x00040015,0x00000083,
0x00000020,0x00000000,0x0004002b,0x00000083,0x00000084,0x00000000,0x0004002b,0x00000083,
0x00000089,0x00000001,0x0005002c,0x0000004b,0x00000095,0x00000057,0x00000057,0x0005002c,
0x0000004b,0x00000097,0x00000037,0x00000037,0x00040020,0x000000aa,0x00000003,0x00000032,
0x0004003b,0x000000aa,0x000000ab,0x00000003,0x00050036,0x00000002,0x00000004,0x00000000,
0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000030,0x00000031,0x00000007,0x0004003b,
0x00000030,0x00000041,0x00000007,0x0004003b,0x00000030,0x00000044,0x00000007,0x0004003b,
0x0000004c,0x0000004d,0x00000007,0x0004003b,0x0000004c,0x00000051,0x00000007,0x0004003b,
0x0000005b,0x0000005c,0x00000007,0x0004003b,0x00000007,0x0000005e,0x00000007,0x0004003b,
0x0000005f,0x00000060,0x00000007,0x0004003b,0x0000005f,0x0000006a,0x00000007,0x0004003b,
0x0000004c,0x00000072,0x00000007,0x0004003b,0x00000030,0x00000078,0x00000007,0x0004003b,
0x00000007,0x00000081,0x00000007,0x0004003b,0x00000007,0x00000082,0x00000007,0x0004003b,
0x00000007,0x00000088,0x00000007,0x0004003b,0x0000004c,0x00000093,0x00000007,0x00050041,
0x00000038,0x00000039,0x00000035,0x00000037,0x0004003d,0x0000002f,0x0000003a,0x00000039,
0x00050041,0x00000038,0x0000003c,0x00000035,0x0000003b,0x0004003d,0x0000002f,0x0000003d,
0x0000003c,0x00050088,0x0000002f,0x0000003e,0x0000003a,0x0000003d,0x0007000c,0x0000002f,
0x00000040,0x00000001,0x00000028,0x0000003e,0x0000003f,0x0003003e,0x00000031,0x00000040,
0x0004003d,0x0000002f,0x00000042,0x00000031,0x00050088,0x0000002f,0x00000043,0x0000003f,
0x00000042,0x0003003e,0x00000041,0x00000043,0x0004003d,0x0000002f,0x00000047,0x00000046,
0x00050041,0x00000038,0x00000048,0x00000035,0x00000037,0x0004003d,0x0000002f,0x00000049,
0x00000048,0x00050085,0x0000002f,0x0000004a,0x00000047,0x00000049,0x0003003e,0x00000044,
0x0000004a,0x0004003d,0x0000002f,0x0000004e,0x00000044,0x0006000c,0x0000002f,0x0000004f,
0x00000001,0x00000008,0x0000004e,0x0004006e,0x0000004b,0x00000050,0x0000004f,0x0003003e,
0x0000004d,0x00000050,0x0004003d,0x00000053,0x00000056,0x00000055,0x00040064,0x00000052,
0x00000058,0x00000056,0x00050067,0x0000004b,0x00000059,0x00000058,0x00000057,0x0003003e,
0x00000051,0x00000059,0x0003003e,0x0000005c,0x0000005d,0x0003003e,0x0000005e,0x0000001b,
0x0003003e,0x00000060,0x00000061,0x000200f9,0x00000062,0x000200f8,0x00000062,0x000400f6,
0x00000064,0x00000065,0x00000000,0x000200f9,0x00000066,0x000200f8,0x00000066,0x0004003d,
0x00000036,0x00000067,0x00000060,0x000500b3,0x00000010,0x00000069,0x00000067,0x00000068,
0x000400fa,0x00000069,0x00000063,0x00000064,0x000200f8,0x00000063,0x0003003e,0x0000006a,
0x00000061,0x000200f9,0x0000006b,0x000200f8,0x0000006b,0x000400f6,0x0000006d,0x0000006e,
0x00000000,0x000200f9,0x0000006f,0x000200f8,0x0000006f,0x0004003d,0x00000036,0x00000070,
0x0000006a,0x000500b3,0x00000010,0x00000071,0x00000070,0x00000068,0x000400fa,0x00000071,
0x0000006c,0x0000006d,0x000200f8,0x0000006c,0x0004003d,0x0000004b,0x00000073,0x0000004d,
0x0004003d,0x00000036,0x00000074,0x0000006a,0x0004003d,0x00000036,0x00000075,0x00000060,
0x00050050,0x0000004b,0x00000076,0x00000074,0x00000075,0x00050080,0x0000004b,0x00000077,
0x00000073,0x00000076,0x0003003e,0x00000072,0x00000077,0x0004003d,0x0000004b,0x00000079,
0x00000072,0x0004006f,0x0000002f,0x0000007a,0x00000079,0x00050081,0x0000002f,0x0000007c,
0x0000007a,0x0000007b,0x0004003d,0x0000002f,0x0000007d,0x00000044,0x00050083,0x0000002f,
0x0000007e,0x0000007c,0x0000007d,0x0004003d,0x0000002f,0x0000007f,0x00000041,0x00050085,
0x0000002f,0x00000080,0x0000007e,0x0000007f,0x0003003e,0x00000078,0x00000080,0x00050041,
0x00000007,0x00000085,0x00000078,0x00000084,0x0004003d,0x00000006,0x00000086,0x00000085,
0x0003003e,0x00000082,0x00000086,0x00050039,0x00000006,0x00000087,0x0000000a,0x00000082,
0x00050041,0x00000007,0x0000008a,0x00000078,0x00000089,0x0004003d,0x00000006,0x0000008b,
0x0000008a,0x0003003e,0x00000088,0x0000008b,0x00050039,0x00000006,0x0000008c,0x0000000a,
0x00000088,0x00050085,0x00000006,0x0000008d,0x00000087,0x0000008c,0x0003003e,0x00000081,
0x0000008d,0x0004003d,0x00000006,0x0000008e,0x00000081,0x000500b4,0x00000010,0x0000008f,
0x0000008e,0x0000001b,0x000300f7,0x00000091,0x00000000,0x000400fa,0x0000008f,0x00000090,
0x00000091,0x000200f8,0x00000090,0x000200f9,0x0000006e,0x000200f8,0x00000091,0x0004003d,
0x0000004b,0x00000094,0x00000072,0x0004003d,0x0000004b,0x00000096,0x00000051,0x00050082,
0x0000004b,0x00000098,0x00000096,0x00000097,0x0008000c,0x0000004b,0x00000099,0x00000001,
0x0000002d,0x00000094,0x00000095,0x00000098,0x0003003e,0x00000093,0x00000099,0x0004003d,
0x00000053,0x0000009a,0x00000055,0x0004003d,0x0000004b,0x0000009b,0x00000093,0x00040064,
0x00000052,0x0000009c,0x0000009a,0x0007005f,0x00000032,0x0000009d,0x0000009c,0x0000009b,
0x00000002,0x00000057,0x0008004f,0x0000005a,0x0000009e,0x0000009d,0x0000009d,0x00000000,
0x00000001,0x00000002,0x0004003d,0x00000006,0x0000009f,0x00000081,0x0005008e,0x0000005a,
0x000000a0,0x0000009e,0x0000009f,0x0004003d,0x0000005a,0x000000a1,0x0000005c,0x00050081,
0x0000005a,0x000000a2,0x000000a1,0x000000a0,0x0003003e,0x0000005c,0x000000a2,0x0004003d,
0x00000006,0x000000a3,0x00000081,0x0004003d,0x00000006,0x000000a4,0x0000005e,0x00050081,
0x00000006,0x000000a5,0x000000a4,0x000000a3,0x0003003e,0x0000005e,0x000000a5,0x000200f9,
0x0000006e,0x000200f8,0x0000006e,0x0004003d,0x00000036,0x000000a6,0x0000006a,0x00050080,
0x00000036,0x000000a7,0x000000a6,0x00000037,0x0003003e,0x0000006a,0x000000a7,0x000200f9,
0x0000006b,0x000200f8,0x0000006d,0x000200f9,0x00000065,0x000200f8,0x00000065,0x0004003d,
0x00000036,0x000000a8,0x00000060,0x00050080,0x00000036,0x000000a9,0x000000a8,0x00000037,
0x0003003e,0x00000060,0x000000a9,0x000200f9,0x00000062,0x000200f8,0x00000064,0x0004003d,
0x0000005a,0x000000ac,0x0000005c,0x0004003d,0x00000006,0x000000ad,0x0000005e,0x0007000c,
0x00000006,0x000000ae,0x00000001,0x00000028,0x000000ad,0x0000000f,0x00060050,0x0000005a,
0x000000af,0x000000ae,0x000000ae,0x000000ae,0x00050088,0x0000005a,0x000000b0,0x000000ac,
0x000000af,0x00050051,0x00000006,0x000000b1,0x000000b0,0x00000000,0x00050051,0x00000006,
0x000000b2,0x000000b0,0x00000001,0x00050051,0x00000006,0x000000b3,0x000000b0,0x00000002,
0x00070050,0x00000032,0x000000b4,0x000000b1,0x000000b2,0x000000b3,0x00000014,0x0003003e,
0x000000ab,0x000000b4,0x000100fd,0x00010038,0x00050036,0x00000006,0x0000000a,0x00000000,
0x00000008,0x00030037,0x00000007,0x00000009,0x000200f8,0x0000000b,0x0004003b,0x00000007,
0x0000001d,0x00000007,0x0004003d,0x00000006,0x0000000c,0x00000009,0x0006000c,0x00000006,
0x0000000d,0x00000001,0x00000004,0x0000000c,0x0003003e,0x00000009,0x0000000d,0x0004003d,
0x00000006,0x0000000e,0x00000009,0x000500b8,0x00000010,0x00000011,0x0000000e,0x0000000f,
0x000300f7,0x00000013,0x00000000,0x000400fa,0x00000011,0x00000012,0x00000013,0x000200f8,
0x00000012,0x000200fe,0x00000014,0x000200f8,0x00000013,0x0004003d,0x00000006,0x00000016,
0x00000009,0x000500be,0x00000010,0x00000018,0x00000016,0x00000017,0x000300f7,0x0000001a,
0x00000000,0x000400fa,0x00000018,0x00000019,0x0000001a,0x000200f8,0x00000019,0x000200fe,
0x0000001b,0x000200f8,0x0000001a,0x0004003d,0x00000006,0x0000001f,0x00000009,0x00050085,
0x00000006,0x00000020,0x0000001e,0x0000001f,0x0003003e,0x0000001d,0x00000020,0x0004003d,
0x00000006,0x00000021,0x0000001d,0x0006000c,0x00000006,0x00000022,0x00000001,0x0000000d,
0x00000021,0x00050085,0x00000006,0x00000023,0x00000017,0x00000022,0x0004003d,0x00000006,
0x00000024,0x0000001d,0x00050085,0x00000006,0x00000026,0x00000024,0x00000025,0x0006000c,
0x00000006,0x00000027,0x00000001,0x0000000d,0x00000026,0x00050085,0x00000006,0x00000028,
0x00000023,0x00000027,0x0004003d,0x00000006,0x00000029,0x0000001d,0x0004003d,0x00000006,
0x0000002a,0x0000001d,0x00050085,0x00000006,0x0000002b,0x00000029,0x0000002a,0x00050088,
0x00000006,0x0000002c,0x00000028,0x0000002b,0x000200fe,0x0000002c,0x00010038
};
-49
View File
@@ -1,49 +0,0 @@
// 1116.3.0
#pragma once
const uint32_t window_frag_code[] = {
0x07230203,0x00010000,0x0008000b,0x00000033,0x00000000,0x00020011,0x00000001,0x0006000b,
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x00000011,0x00000015,0x00030010,
0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,
0x00000000,0x00030005,0x00000009,0x00000063,0x00050005,0x0000000d,0x53786574,0x6c706d61,
0x00007265,0x00060005,0x00000011,0x67617266,0x43786554,0x64726f6f,0x00000000,0x00050005,
0x00000015,0x4374756f,0x726f6c6f,0x00000000,0x00030005,0x0000001a,0x00004350,0x00050006,
0x0000001a,0x00000000,0x5863646e,0x00000030,0x00050006,0x0000001a,0x00000001,0x5963646e,
0x00000030,0x00050006,0x0000001a,0x00000002,0x5863646e,0x00000031,0x00050006,0x0000001a,
0x00000003,0x5963646e,0x00000031,0x00060006,0x0000001a,0x00000004,0x54657375,0x6c417865,
0x00616870,0x00030005,0x0000001c,0x00006370,0x00040047,0x0000000d,0x00000021,0x00000000,
0x00040047,0x0000000d,0x00000022,0x00000000,0x00040047,0x00000011,0x0000001e,0x00000000,
0x00040047,0x00000015,0x0000001e,0x00000000,0x00030047,0x0000001a,0x00000002,0x00050048,
0x0000001a,0x00000000,0x00000023,0x00000000,0x00050048,0x0000001a,0x00000001,0x00000023,
0x00000004,0x00050048,0x0000001a,0x00000002,0x00000023,0x00000008,0x00050048,0x0000001a,
0x00000003,0x00000023,0x0000000c,0x00050048,0x0000001a,0x00000004,0x00000023,0x00000010,
0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,
0x00040017,0x00000007,0x00000006,0x00000004,0x00040020,0x00000008,0x00000007,0x00000007,
0x00090019,0x0000000a,0x00000006,0x00000001,0x00000000,0x00000000,0x00000000,0x00000001,
0x00000000,0x0003001b,0x0000000b,0x0000000a,0x00040020,0x0000000c,0x00000000,0x0000000b,
0x0004003b,0x0000000c,0x0000000d,0x00000000,0x00040017,0x0000000f,0x00000006,0x00000002,
0x00040020,0x00000010,0x00000001,0x0000000f,0x0004003b,0x00000010,0x00000011,0x00000001,
0x00040020,0x00000014,0x00000003,0x00000007,0x0004003b,0x00000014,0x00000015,0x00000003,
0x00040017,0x00000016,0x00000006,0x00000003,0x00040015,0x00000019,0x00000020,0x00000001,
0x0007001e,0x0000001a,0x00000006,0x00000006,0x00000006,0x00000006,0x00000019,0x00040020,
0x0000001b,0x00000009,0x0000001a,0x0004003b,0x0000001b,0x0000001c,0x00000009,0x0004002b,
0x00000019,0x0000001d,0x00000004,0x00040020,0x0000001e,0x00000009,0x00000019,0x0004002b,
0x00000019,0x00000021,0x00000000,0x00020014,0x00000022,0x00040020,0x00000024,0x00000007,
0x00000006,0x00040015,0x00000028,0x00000020,0x00000000,0x0004002b,0x00000028,0x00000029,
0x00000003,0x0004002b,0x00000006,0x0000002d,0x3f800000,0x00050036,0x00000002,0x00000004,
0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000008,0x00000009,0x00000007,
0x0004003b,0x00000024,0x00000025,0x00000007,0x0004003d,0x0000000b,0x0000000e,0x0000000d,
0x0004003d,0x0000000f,0x00000012,0x00000011,0x00050057,0x00000007,0x00000013,0x0000000e,
0x00000012,0x0003003e,0x00000009,0x00000013,0x0004003d,0x00000007,0x00000017,0x00000009,
0x0008004f,0x00000016,0x00000018,0x00000017,0x00000017,0x00000000,0x00000001,0x00000002,
0x00050041,0x0000001e,0x0000001f,0x0000001c,0x0000001d,0x0004003d,0x00000019,0x00000020,
0x0000001f,0x000500ab,0x00000022,0x00000023,0x00000020,0x00000021,0x000300f7,0x00000027,
0x00000000,0x000400fa,0x00000023,0x00000026,0x0000002c,0x000200f8,0x00000026,0x00050041,
0x00000024,0x0000002a,0x00000009,0x00000029,0x0004003d,0x00000006,0x0000002b,0x0000002a,
0x0003003e,0x00000025,0x0000002b,0x000200f9,0x00000027,0x000200f8,0x0000002c,0x0003003e,
0x00000025,0x0000002d,0x000200f9,0x00000027,0x000200f8,0x00000027,0x0004003d,0x00000006,
0x0000002e,0x00000025,0x00050051,0x00000006,0x0000002f,0x00000018,0x00000000,0x00050051,
0x00000006,0x00000030,0x00000018,0x00000001,0x00050051,0x00000006,0x00000031,0x00000018,
0x00000002,0x00070050,0x00000007,0x00000032,0x0000002f,0x00000030,0x00000031,0x0000002e,
0x0003003e,0x00000015,0x00000032,0x000100fd,0x00010038
};
-68
View File
@@ -1,68 +0,0 @@
// 1116.3.0
#pragma once
const uint32_t window_vert_code[] = {
0x07230203,0x00010000,0x0008000b,0x0000004a,0x00000000,0x00020011,0x00000001,0x0006000b,
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
0x0008000f,0x00000000,0x00000004,0x6e69616d,0x00000000,0x0000000a,0x0000003a,0x00000044,
0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,0x00000000,0x00030005,
0x00000008,0x00006978,0x00060005,0x0000000a,0x565f6c67,0x65747265,0x646e4978,0x00007865,
0x00030005,0x0000000f,0x00006979,0x00030005,0x00000014,0x00000078,0x00030005,0x0000001b,
0x00004350,0x00050006,0x0000001b,0x00000000,0x5863646e,0x00000030,0x00050006,0x0000001b,
0x00000001,0x5963646e,0x00000030,0x00050006,0x0000001b,0x00000002,0x5863646e,0x00000031,
0x00050006,0x0000001b,0x00000003,0x5963646e,0x00000031,0x00030005,0x0000001d,0x00006370,
0x00030005,0x00000027,0x00000079,0x00060005,0x00000038,0x505f6c67,0x65567265,0x78657472,
0x00000000,0x00060006,0x00000038,0x00000000,0x505f6c67,0x7469736f,0x006e6f69,0x00070006,
0x00000038,0x00000001,0x505f6c67,0x746e696f,0x657a6953,0x00000000,0x00070006,0x00000038,
0x00000002,0x435f6c67,0x4470696c,0x61747369,0x0065636e,0x00070006,0x00000038,0x00000003,
0x435f6c67,0x446c6c75,0x61747369,0x0065636e,0x00030005,0x0000003a,0x00000000,0x00060005,
0x00000044,0x67617266,0x43786554,0x64726f6f,0x00000000,0x00040047,0x0000000a,0x0000000b,
0x0000002a,0x00030047,0x0000001b,0x00000002,0x00050048,0x0000001b,0x00000000,0x00000023,
0x00000000,0x00050048,0x0000001b,0x00000001,0x00000023,0x00000004,0x00050048,0x0000001b,
0x00000002,0x00000023,0x00000008,0x00050048,0x0000001b,0x00000003,0x00000023,0x0000000c,
0x00030047,0x00000038,0x00000002,0x00050048,0x00000038,0x00000000,0x0000000b,0x00000000,
0x00050048,0x00000038,0x00000001,0x0000000b,0x00000001,0x00050048,0x00000038,0x00000002,
0x0000000b,0x00000003,0x00050048,0x00000038,0x00000003,0x0000000b,0x00000004,0x00040047,
0x00000044,0x0000001e,0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,
0x00040015,0x00000006,0x00000020,0x00000001,0x00040020,0x00000007,0x00000007,0x00000006,
0x00040020,0x00000009,0x00000001,0x00000006,0x0004003b,0x00000009,0x0000000a,0x00000001,
0x0004002b,0x00000006,0x0000000c,0x00000001,0x00030016,0x00000012,0x00000020,0x00040020,
0x00000013,0x00000007,0x00000012,0x00020014,0x00000016,0x0006001e,0x0000001b,0x00000012,
0x00000012,0x00000012,0x00000012,0x00040020,0x0000001c,0x00000009,0x0000001b,0x0004003b,
0x0000001c,0x0000001d,0x00000009,0x0004002b,0x00000006,0x0000001e,0x00000002,0x00040020,
0x0000001f,0x00000009,0x00000012,0x0004002b,0x00000006,0x00000023,0x00000000,0x0004002b,
0x00000006,0x0000002d,0x00000003,0x00040017,0x00000034,0x00000012,0x00000004,0x00040015,
0x00000035,0x00000020,0x00000000,0x0004002b,0x00000035,0x00000036,0x00000001,0x0004001c,
0x00000037,0x00000012,0x00000036,0x0006001e,0x00000038,0x00000034,0x00000012,0x00000037,
0x00000037,0x00040020,0x00000039,0x00000003,0x00000038,0x0004003b,0x00000039,0x0000003a,
0x00000003,0x0004002b,0x00000012,0x0000003d,0x00000000,0x0004002b,0x00000012,0x0000003e,
0x3f800000,0x00040020,0x00000040,0x00000003,0x00000034,0x00040017,0x00000042,0x00000012,
0x00000002,0x00040020,0x00000043,0x00000003,0x00000042,0x0004003b,0x00000043,0x00000044,
0x00000003,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,
0x0004003b,0x00000007,0x00000008,0x00000007,0x0004003b,0x00000007,0x0000000f,0x00000007,
0x0004003b,0x00000013,0x00000014,0x00000007,0x0004003b,0x00000013,0x00000018,0x00000007,
0x0004003b,0x00000013,0x00000027,0x00000007,0x0004003b,0x00000013,0x0000002a,0x00000007,
0x0004003d,0x00000006,0x0000000b,0x0000000a,0x000500c3,0x00000006,0x0000000d,0x0000000b,
0x0000000c,0x000500c7,0x00000006,0x0000000e,0x0000000d,0x0000000c,0x0003003e,0x00000008,
0x0000000e,0x0004003d,0x00000006,0x00000010,0x0000000a,0x000500c7,0x00000006,0x00000011,
0x00000010,0x0000000c,0x0003003e,0x0000000f,0x00000011,0x0004003d,0x00000006,0x00000015,
0x00000008,0x000500aa,0x00000016,0x00000017,0x00000015,0x0000000c,0x000300f7,0x0000001a,
0x00000000,0x000400fa,0x00000017,0x00000019,0x00000022,0x000200f8,0x00000019,0x00050041,
0x0000001f,0x00000020,0x0000001d,0x0000001e,0x0004003d,0x00000012,0x00000021,0x00000020,
0x0003003e,0x00000018,0x00000021,0x000200f9,0x0000001a,0x000200f8,0x00000022,0x00050041,
0x0000001f,0x00000024,0x0000001d,0x00000023,0x0004003d,0x00000012,0x00000025,0x00000024,
0x0003003e,0x00000018,0x00000025,0x000200f9,0x0000001a,0x000200f8,0x0000001a,0x0004003d,
0x00000012,0x00000026,0x00000018,0x0003003e,0x00000014,0x00000026,0x0004003d,0x00000006,
0x00000028,0x0000000f,0x000500aa,0x00000016,0x00000029,0x00000028,0x0000000c,0x000300f7,
0x0000002c,0x00000000,0x000400fa,0x00000029,0x0000002b,0x00000030,0x000200f8,0x0000002b,
0x00050041,0x0000001f,0x0000002e,0x0000001d,0x0000002d,0x0004003d,0x00000012,0x0000002f,
0x0000002e,0x0003003e,0x0000002a,0x0000002f,0x000200f9,0x0000002c,0x000200f8,0x00000030,
0x00050041,0x0000001f,0x00000031,0x0000001d,0x0000000c,0x0004003d,0x00000012,0x00000032,
0x00000031,0x0003003e,0x0000002a,0x00000032,0x000200f9,0x0000002c,0x000200f8,0x0000002c,
0x0004003d,0x00000012,0x00000033,0x0000002a,0x0003003e,0x00000027,0x00000033,0x0004003d,
0x00000012,0x0000003b,0x00000014,0x0004003d,0x00000012,0x0000003c,0x00000027,0x00070050,
0x00000034,0x0000003f,0x0000003b,0x0000003c,0x0000003d,0x0000003e,0x00050041,0x00000040,
0x00000041,0x0000003a,0x00000023,0x0003003e,0x00000041,0x0000003f,0x0004003d,0x00000006,
0x00000045,0x00000008,0x0004006f,0x00000012,0x00000046,0x00000045,0x0004003d,0x00000006,
0x00000047,0x0000000f,0x0004006f,0x00000012,0x00000048,0x00000047,0x00050050,0x00000042,
0x00000049,0x00000046,0x00000048,0x0003003e,0x00000044,0x00000049,0x000100fd,0x00010038
};
+216
View File
@@ -0,0 +1,216 @@
// Upstream: termux/termux-app v0.118.0, terminal-emulator/src/main/jni/termux.c.
// Licensed under the repository's GPL-3.0 license (see LICENSE).
#include <dirent.h>
#include <fcntl.h>
#include <jni.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
#define TERMUX_UNUSED(x) x __attribute__((__unused__))
#ifdef __APPLE__
# define LACKS_PTSNAME_R
#endif
static int throw_runtime_exception(JNIEnv* env, char const* message)
{
jclass exClass = (*env)->FindClass(env, "java/lang/RuntimeException");
(*env)->ThrowNew(env, exClass, message);
return -1;
}
static int create_subprocess(JNIEnv* env,
char const* cmd,
char const* cwd,
char* const argv[],
char** envp,
int* pProcessId,
jint rows,
jint columns)
{
int ptm = open("/dev/ptmx", O_RDWR | O_CLOEXEC);
if (ptm < 0) return throw_runtime_exception(env, "Cannot open /dev/ptmx");
#ifdef LACKS_PTSNAME_R
char* devname;
#else
char devname[64];
#endif
if (grantpt(ptm) || unlockpt(ptm) ||
#ifdef LACKS_PTSNAME_R
(devname = ptsname(ptm)) == NULL
#else
ptsname_r(ptm, devname, sizeof(devname))
#endif
) {
return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx");
}
// Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display.
struct termios tios;
tcgetattr(ptm, &tios);
tios.c_iflag |= IUTF8;
tios.c_iflag &= ~(IXON | IXOFF);
tcsetattr(ptm, TCSANOW, &tios);
/** Set initial winsize. */
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns };
ioctl(ptm, TIOCSWINSZ, &sz);
pid_t pid = fork();
if (pid < 0) {
return throw_runtime_exception(env, "Fork failed");
} else if (pid > 0) {
*pProcessId = (int) pid;
return ptm;
} else {
// Clear signals which the Android java process may have blocked:
sigset_t signals_to_unblock;
sigfillset(&signals_to_unblock);
sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0);
close(ptm);
setsid();
int pts = open(devname, O_RDWR);
if (pts < 0) exit(-1);
dup2(pts, 0);
dup2(pts, 1);
dup2(pts, 2);
DIR* self_dir = opendir("/proc/self/fd");
if (self_dir != NULL) {
int self_dir_fd = dirfd(self_dir);
struct dirent* entry;
while ((entry = readdir(self_dir)) != NULL) {
int fd = atoi(entry->d_name);
if (fd > 2 && fd != self_dir_fd) close(fd);
}
closedir(self_dir);
}
clearenv();
if (envp) for (; *envp; ++envp) putenv(*envp);
if (chdir(cwd) != 0) {
char* error_message;
// No need to free asprintf()-allocated memory since doing execvp() or exit() below.
if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()";
perror(error_message);
fflush(stderr);
}
execvp(cmd, argv);
// Show terminal output about failing exec() call:
char* error_message;
if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()";
perror(error_message);
_exit(1);
}
}
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_createSubprocess(
JNIEnv* env,
jclass TERMUX_UNUSED(clazz),
jstring cmd,
jstring cwd,
jobjectArray args,
jobjectArray envVars,
jintArray processIdArray,
jint rows,
jint columns)
{
jsize size = args ? (*env)->GetArrayLength(env, args) : 0;
char** argv = NULL;
if (size > 0) {
argv = (char**) malloc((size + 1) * sizeof(char*));
if (!argv) return throw_runtime_exception(env, "Couldn't allocate argv array");
for (int i = 0; i < size; ++i) {
jstring arg_java_string = (jstring) (*env)->GetObjectArrayElement(env, args, i);
char const* arg_utf8 = (*env)->GetStringUTFChars(env, arg_java_string, NULL);
if (!arg_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for argv");
argv[i] = strdup(arg_utf8);
(*env)->ReleaseStringUTFChars(env, arg_java_string, arg_utf8);
}
argv[size] = NULL;
}
size = envVars ? (*env)->GetArrayLength(env, envVars) : 0;
char** envp = NULL;
if (size > 0) {
envp = (char**) malloc((size + 1) * sizeof(char *));
if (!envp) return throw_runtime_exception(env, "malloc() for envp array failed");
for (int i = 0; i < size; ++i) {
jstring env_java_string = (jstring) (*env)->GetObjectArrayElement(env, envVars, i);
char const* env_utf8 = (*env)->GetStringUTFChars(env, env_java_string, 0);
if (!env_utf8) return throw_runtime_exception(env, "GetStringUTFChars() failed for env");
envp[i] = strdup(env_utf8);
(*env)->ReleaseStringUTFChars(env, env_java_string, env_utf8);
}
envp[size] = NULL;
}
int procId = 0;
char const* cmd_cwd = (*env)->GetStringUTFChars(env, cwd, NULL);
char const* cmd_utf8 = (*env)->GetStringUTFChars(env, cmd, NULL);
int ptm = create_subprocess(env, cmd_utf8, cmd_cwd, argv, envp, &procId, rows, columns);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_utf8);
(*env)->ReleaseStringUTFChars(env, cmd, cmd_cwd);
if (argv) {
for (char** tmp = argv; *tmp; ++tmp) free(*tmp);
free(argv);
}
if (envp) {
for (char** tmp = envp; *tmp; ++tmp) free(*tmp);
free(envp);
}
int* pProcId = (int*) (*env)->GetPrimitiveArrayCritical(env, processIdArray, NULL);
if (!pProcId) return throw_runtime_exception(env, "JNI call GetPrimitiveArrayCritical(processIdArray, &isCopy) failed");
*pProcId = procId;
(*env)->ReleasePrimitiveArrayCritical(env, processIdArray, pProcId, 0);
return ptm;
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyWindowSize(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd, jint rows, jint cols)
{
struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) cols };
ioctl(fd, TIOCSWINSZ, &sz);
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_setPtyUTF8Mode(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fd)
{
struct termios tios;
tcgetattr(fd, &tios);
if ((tios.c_iflag & IUTF8) == 0) {
tios.c_iflag |= IUTF8;
tcsetattr(fd, TCSANOW, &tios);
}
}
JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint pid)
{
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
return -WTERMSIG(status);
} else {
// Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value".
return 0;
}
}
JNIEXPORT void JNICALL Java_com_termux_terminal_JNI_close(JNIEnv* TERMUX_UNUSED(env), jclass TERMUX_UNUSED(clazz), jint fileDescriptor)
{
close(fileDescriptor);
}
+9 -9
View File
@@ -38,19 +38,19 @@ target_include_directories(bannerwayland PRIVATE
prebuilt/include
src
# The X11 Vulkan renderer's committed SPIR-V headers (*_frag.h / upscale_vert.h, made by
# winlator/gen_shaders.sh): the screen-effect chain (src/effects_chain.c) runs the same bytes.
# Bannerlator's gen_shaders.sh): the screen-effect chain (src/effects_chain.c) runs the same bytes.
${CMAKE_CURRENT_SOURCE_DIR}/../shaders
)
# ── Frame generation (feat/wayland-framegen) ─────────────────────────────────
# The two native engines the X11 renderer runs (LSFG Native: winlator/lsfg, Win-FG Native:
# winlator/winfg) are compiled into this library unmodified and driven by
# The two native engines the X11 renderer runs (LSFG Native: framegen/lsfg, Win-FG Native:
# framegen/winfg) are compiled into this library unmodified and driven by
# src/framegen_engine.cpp through the compositor's own Turnip entry points. They need the
# winlator tree and the vendored DXBC translator (thirdparty/dxbc, for the LSFG cache format),
# framegen tree and the vendored DXBC translator (thirdparty/dxbc, for the LSFG cache format),
# which only the app build has: the standalone build (standalone/) gets the stub, which reports
# every engine unavailable and generates nothing.
set(BANNER_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../winlator/lsfg)
set(BANNER_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../winlator/winfg)
set(BANNER_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg)
set(BANNER_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/winfg)
if (TARGET dxbc AND EXISTS ${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
AND EXISTS ${BANNER_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp)
message(STATUS "bannerwayland: frame-generation engines built in (LSFG Native, Win-FG Native)")
@@ -79,8 +79,8 @@ if (TARGET dxbc AND EXISTS ${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
${BANNER_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp
)
target_include_directories(bannerwayland PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/.. # <thirdparty/...> as the winlator target sees it
${CMAKE_CURRENT_SOURCE_DIR}/../winlator # VulkanRendererContext.h (VkTable), lsfg/, winfg/
${CMAKE_CURRENT_SOURCE_DIR}/.. # <thirdparty/...> as the frame-generation engines see it
${CMAKE_CURRENT_SOURCE_DIR}/../framegen # VulkanRendererContext.h (VkTable), lsfg/, winfg/
)
target_compile_options(bannerwayland PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17 -fvisibility=hidden>)
@@ -96,7 +96,7 @@ endif()
add_library(wayland-server SHARED IMPORTED)
set_target_properties(wayland-server PROPERTIES IMPORTED_LOCATION
${CMAKE_CURRENT_SOURCE_DIR}/prebuilt/lib/libwayland-server.so)
# Vulkan is loaded at runtime via adrenotools (Turnip), not linked directly — so the
# Vulkan is loaded at runtime via adrenotools (Turnip), not linked directly - so the
# compositor uses Turnip's driver, which supports dmabuf import (drm_format_modifier).
target_link_libraries(bannerwayland
wayland-server
-910
View File
@@ -1,910 +0,0 @@
# HDR on the Wayland backend — reconnaissance
> **2026-09-14: round 1 of Phase A is built** (branch `feat/wayland-hdr`, opt-in, testing only) —
> see **§10** at the end for what was built, how it is gated and how a tester's log proves it.
> The recon below is kept as written.
Written 2026-09-13. **Recon only: no product code was changed, nothing was built, nothing was
installed on the device.** Branch surveyed: app `feat/wayland-phase2`, proton-wine
`feat/winewayland-desktop-11.0-2`, Banners-Turnip `wayland` (Mesa `7cda7850`, v26.3.0).
Device reads are `dumpsys` / `getprop` / `strings` only.
Companion docs: `ZERO_COPY_SPIKE.md` (buffer path), `WAYLAND_RUNTIME.md`.
---
## 0. Answer in one paragraph
**The game half of HDR is already shipping and nobody noticed.** The Wayland Turnip inside
wcp v6 (`Proton-11.0-2.1-arm64ec-6/lib/libvulkan_freedreno_wayland.so`, verified on the device)
already contains Mesa's full `wp_color_manager_v1` client implementation and advertises
`VK_EXT_swapchain_colorspace` + `VK_EXT_hdr_metadata`; the shipping DXVK v3.1-gplasync already
carries `dxgi.enableHDR` / `DXVK_HDR` and the `VK_COLOR_SPACE_HDR10_ST2084_EXT` path;
`winewayland.drv` is a transparent 121-line shim that neither adds nor removes colour
information. **Everything that is missing is in our compositor**, which advertises no colour
protocol, offers only four 8-bit DRM formats over `zwp_linux_dmabuf_v1`, takes
`surfaceFormats[0]` for its own swapchain, runs an 8-bit scene/effects chain, and never calls
`ASurfaceTransaction_setBufferDataSpace`. The work is therefore **app-side only** — the memory
note in `project_bannerlator_wayland_phase4` that HDR "does need a new wcp" is **wrong for the
HDR10 (10-bit PQ) path**; a new wcp is only needed for scRGB/FP16 zero-copy. Estimated
**4–6 engineer-days** to a plumbed, log-verifiable HDR10 path. But the Pocket FIT's panel
reports `supportedHdrTypes=[]`, `supportedColorModes=[0]` (NATIVE only), `hdrSdrRatio
not_available` and `displayBrightnessNits=-1`, so **nothing of it can be seen on this device**,
and turning it on there would actively *cost* us the zero-copy DEVICE-composition win. Verdict
in §8: **not now.**
---
## 1. Game side — how far an HDR request gets today
### 1.1 What a Windows game does
| Call | Where it lands in DXVK | Notes |
|---|---|---|
| `IDXGIOutput6::GetDesc1` → `DXGI_OUTPUT_DESC1::ColorSpace` | `src/dxgi/dxgi_output.cpp:220-259` | **Never queries Vulkan.** Reports `RGB_FULL_G2084_NONE_P2020` purely because `dxgi.enableHDR` is on. |
| `IDXGISwapChain3::CheckColorSpaceSupport` | `src/dxgi/dxgi_swapchain.cpp:655-667` | This *is* backed by Vulkan (below). Note it is a swapchain method, not a factory method. |
| `IDXGISwapChain3::SetColorSpace1` | `dxgi_swapchain.cpp:672-688` → `d3d11_swapchain.cpp:306-331` → `Presenter::supportsColorSpace` (`dxvk_presenter.cpp:319-343`) | Returns `E_INVALIDARG` if the Vulkan surface does not list the colour space. |
| `IDXGISwapChain4::SetHDRMetaData` | `dxgi_swapchain.cpp:690-717` → `vkSetHdrMetadataEXT` | Only `DXGI_HDR_METADATA_TYPE_HDR10`. Silently dropped if `VK_EXT_hdr_metadata` is absent (`dxvk_presenter.cpp:143-149`). |
DXGI→Vulkan mapping (`src/d3d11/d3d11_swapchain.cpp:21-28`):
```
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 -> VK_COLOR_SPACE_HDR10_ST2084_EXT
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 -> VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT (scRGB)
DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709 -> VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
```
Swapchain formats DXVK will accept per colour space (`Presenter::pickFormat`,
`dxvk_presenter.cpp:1043-1081`):
* `HDR10_ST2084` → `A2R10G10B10_UNORM_PACK32`, `A2B10G10R10_UNORM_PACK32`,
`E5B9G9R9_UFLOAT_PACK32`, `R16G16B16A16_UNORM`, `R16G16B16A16_SFLOAT`
* `EXTENDED_SRGB_LINEAR` (scRGB) → **`R16G16B16A16_SFLOAT` only**
`VK_EXT_hdr_metadata` is optional in both DXVK and vkd3d-proton. `VK_EXT_swapchain_colorspace`
is *not* requested as an instance extension by DXVK's win32 WSI — it simply trusts the driver to
report the extra `VkColorSpaceKHR` values from `vkGetPhysicalDeviceSurfaceFormatsKHR`. Mesa's
Wayland WSI does exactly that (§2.3), so this is not a problem for us.
**vkd3d-proton (D3D12)** ships its own swapchain (`libs/vkd3d/swapchain.c`), same colour-space
mapping (`convert_color_space`, :1717-1730), but **no fallback table**: if the requested
HDR colour space is not in `vkGetPhysicalDeviceSurfaceFormatsKHR`, `dxgi_vk_swap_chain_select_format`
(:1773-1791) returns `false` with the comment *"Refuse to present unsupported HDR since it will
look completely bogus"*, and the caller skips swapchain recreation for that cycle. A D3D12 game
that force-sets HDR without checking can therefore **stall presentation** rather than degrade.
DXGI enumeration for D3D12 titles is still DXVK's `dxgi.dll`, so `DXVK_HDR` still drives
`DXGI_OUTPUT_DESC1`.
**Verified in our shipped binaries** (device, `strings`):
`contents/DXVK/v3.1-gplasync-0/system32/dxgi.dll` contains `dxgi.enableHDR`, `DXVK_HDR`,
`VK_COLOR_SPACE_HDR10_ST2084_EXT`; `d3d11.dll`/`d3d9.dll`/`dxgi.dll` contain
`VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT`; `contents/VKD3D/3.1.0-wave64-relax-1/.../d3d12core.dll`
contains `VK_EXT_hdr_metadata`. Nothing needs rebuilding on the D3D side.
Proton-level gating: GE's `proton` script maps `PROTON_ENABLE_HDR` / `PROTON_USE_HDR` to
`DXVK_HDR=1` and nothing else. For us that is one container env var — no launcher work.
### 1.2 The failure mode today, exactly
DXVK's `DXGI_OUTPUT_DESC1` will claim HDR10 as soon as `DXVK_HDR=1`, regardless of Vulkan.
`SetColorSpace1` then fails with `E_INVALIDARG` because our compositor advertises no colour
management, so Mesa lists only `SRGB_NONLINEAR` + `PASS_THROUGH` (§2.3). Well-behaved games fall
back to SDR. Games that trust `GetDesc1` without checking `CheckColorSpaceSupport` can render an
HDR-authored image into an SDR swapchain (washed out) — this is a pre-existing foot-gun, not
something we introduce. **Recommendation regardless of whether we do HDR: do not expose a
`DXVK_HDR` container toggle until the compositor answers truthfully**, or we invite exactly that
bug class.
### 1.3 `winewayland.drv` — carries nothing for colour, and blocks nothing
`/home/claude-user/proton-wine-wayland2-wt/dlls/winewayland.drv/vulkan.c` is 121 lines:
`wayland_vulkan_surface_create` wraps the `wl_surface` in a `VkSurfaceKHR`,
`wayland_map_instance_extensions` aliases `VK_KHR_win32_surface` ⇄ `VK_KHR_wayland_surface`.
There is **no surface-format or colour-space filtering**, so whatever Mesa advertises reaches
DXVK unmodified. A grep for `color|colour|hdr|st2084|bt2020|gamut|10.?bit` across the whole
driver returns only GDI colour-bitmap helpers — nothing colorimetric.
One real consequence: `wayland_output.c` publishes no EDID, and nothing in the tree writes the
`EDID` registry value `win32u/sysparams.c:815-818` reads. DXVK's
`src/wsi/win32/wsi_monitor_win32.cpp:288-361` therefore finds no EDID and falls back to its dummy
HDR luminances (`wsi_edid.h:38-56`: min 0.01 / max 1499 / maxFullFrame 799 nits). A game will
size its tonemapper for a 1499-nit display. Correctable later by synthesising an EDID in
`winewayland.drv` (that *would* need a new wcp), but not a blocker.
### 1.4 Our bundled Turnip already speaks colour management — verified on the device
```
$ strings .../Proton/11.0-2.1-arm64ec-6/lib/libvulkan_freedreno_wayland.so
wp_color_manager_v1 PRESENT
wp_color_management_surface_v1 PRESENT
wp_image_description_creator_params_v1 PRESENT
banner_ahb_v1 PRESENT (our zero-copy patch)
VK_EXT_swapchain_colorspace PRESENT
VK_EXT_hdr_metadata PRESENT
```
The plain adrenotools driver the compositor itself loads
(`contents/adrenotools/Mesa Turnip v26.3.0-7cda785 (Android + Wayland)/libvulkan_freedreno.so`)
has the same colour-management symbols. Our own WSI patch is already aware of the protocol —
`patches/wayland/banner_ahb_wsi.py` anchors one of its edits on Mesa's
`if (display->color_manager) wp_color_manager_v1_destroy(...)` teardown line.
**And the zero-copy patch already maps 10-bit to gralloc** — `banner_ahb_wsi.py:99-100, 225-237`:
```c
#define BANNER_AHB_FORMAT_R8G8B8A8_UNORM 1u
#define BANNER_AHB_FORMAT_R10G10B10A2_UNORM 0x2bu /* AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM */
...
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: return BANNER_AHB_FORMAT_R10G10B10A2_UNORM;
```
So a 10-bit HDR10 swapchain can already be gralloc-backed and go onto the display layer with the
**shipped** wcp. `VK_FORMAT_R16G16B16A16_SFLOAT` is *not* mapped, so `banner_ahb_skip_format`
(:239-242, applied at `:606-617`) hides FP16 surface formats whenever zero-copy is on — scRGB
zero-copy is the one thing that needs a driver change (one enum: `AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT = 0x16`).
### 1.5 X11 has no path — confirmed
`app/src/main/cpp/winlator/VulkanRendererContext.cpp:439` hardcodes
`swapchainFmt = VK_FORMAT_R8G8B8A8_UNORM` and `:492` hardcodes
`imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR`. Upstream of that, the game presents through
`winex11.drv` into our pure-Java X11 server, which has no colour extension of any kind. HDR on X11
would mean inventing a private X extension *and* teaching DXVK to use it. Correctly out of scope.
---
## 2. Wayland side
### 2.1 Protocol status
`wp_color_management_v1` is **merged and stable-ish**: wayland-protocols MR !14, merged
2025-02-13, shipped in **wayland-protocols 1.41**, file
`staging/color-management/color-management-v1.xml`. Interfaces are at **version 3** upstream today
(v2 added `get_image_description`/`preferred_changed2`; v3 added `create_windows_bt2100` and the
`absolute_no_adaptation` intent). Predecessors: `zwp_linux_color_management_v1` →
`xx_color_management_v4` (the name most 2023–2025 shipping code targeted) → `wp_*` on landing.
`frog_color_management_v1` (Valve) is still vendored by gamescope alongside the real one and is
still functionally load-bearing on the Deck.
Implementers: KWin (reference impl, most mature), Mutter (merged 2025-02-25 but still does not
advertise enough features for automatic HDR), wlroots 0.20 / Sway 1.12 (HDR10 on the Vulkan
renderer), gamescope (v1 semantics + frog), Weston (experimental, no tone mapping).
Proton-CachyOS's changelog notes that its `winewayland.drv` `windows_bt2100` path needs protocol
**v3** and that *"no compositor supports it yet"* — everybody is converged on **v1**.
**We only need v1.** That is what Mesa binds.
### 2.2 Minimum viable compositor subset
At bind, on the `wp_color_manager_v1` global, send one event per supported item then `done`:
* `supported_intent`: `perceptual` (mandatory anyway — it is all Mesa ever asks for)
* `supported_feature`: `parametric` (gates `create_parametric_creator`); optionally
`set_mastering_display_primaries`, and `extended_target_volume` **only if** we want scRGB
* `supported_primaries_named`: `bt2020` (HDR10), `srgb` (needed for scRGB and for Mesa's
"use a colour surface for sRGB too" branch), `display_p3` (wide gamut)
* `supported_tf_named`: `st2084_pq` (HDR10), `ext_linear` (scRGB), `srgb`
Then implement: `create_parametric_creator` → `wp_image_description_creator_params_v1`
(`set_primaries_named`, `set_tf_named`, `set_mastering_display_primaries`,
`set_mastering_luminance`, `set_max_cll`, `set_max_fall`) → `create` → emit
`wp_image_description_v1.ready` (or `failed`) → `wp_color_manager_v1.get_surface` →
`wp_color_management_surface_v1.set_image_description(desc, intent)`, double-buffered on
`wl_surface.commit`.
**Not needed:** ICC creator, `wp_color_management_surface_feedback_v1`,
`wp_image_description_info_v1`, output image descriptions, v2/v3 requests. Mesa uses none of them.
### 2.3 What Mesa's WSI does — read from the exact commit we build
Read from `src/vulkan/wsi/wsi_common_wayland.c` at Mesa `7cda7850` (4121 lines — the commit
recorded in `banners-turnip-wayland/mesa_hash.txt`). Landed upstream as MR !32038 (Xaver Hugl,
merged 2025-02-25, Mesa 25.1).
* **Bind:** `registry_handle_global` binds `wp_color_manager_v1` at **version 1** unconditionally
when advertised (`:1536-1538`), and installs `color_manager_listener` (`:1217-1223`).
* **Colour-space list:** `wsi_wl_display_determine_colorspaces` (`:1122-1160`) always adds
`VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` and `VK_COLOR_SPACE_PASS_THROUGH_EXT`; everything else comes
from the static `colorspace_mapping[]` table (`:1038-1118`) and is added **only if** the
compositor advertised that entry's primaries *and* transfer function (and, for entries flagged
`needs_extended_range`, `feature.extended_target_volume`). Table entries include
`HDR10_ST2084` (bt2020 + st2084_pq), `EXTENDED_SRGB_LINEAR` (srgb + ext_linear + extended range),
`DISPLAY_P3_NONLINEAR`, `DISPLAY_P3_LINEAR`, `BT709_LINEAR`, `BT709_NONLINEAR`, `BT2020_LINEAR`,
`HDR10_HLG`, `ADOBERGB_LINEAR`.
* **Surface formats:** `wsi_wl_surface_get_formats2` (`:2205-2246`) emits the **full cross product** of
`display.colorspaces` × `display.formats` with **no bit-depth gating**:
```c
u_vector_foreach(cs, &display.colorspaces) {
u_vector_foreach(disp_fmt, &display.formats) {
if (!(disp_fmt->flags & WSI_WL_FMT_ALPHA) || !(disp_fmt->flags & WSI_WL_FMT_OPAQUE))
continue;
out_fmt->surfaceFormat.format = disp_fmt->vk_format;
out_fmt->surfaceFormat.colorSpace = *cs;
}
}
```
Consequence: if we advertise BT.2020 + PQ but keep only 8-bit DRM formats, Mesa will happily
offer `R8G8B8A8_UNORM` + `HDR10_ST2084` and DXVK will **not** take it (`pickFormat`'s HDR10 list
is 10-bit/16-bit only) — so there is no accidental 8-bit-PQ trap, but there is also no HDR
unless we also advertise a 10-bit DRM format.
* **Apply:** `wsi_wl_swapchain_update_colorspace` (`:1323-1455`) builds the parametric creator,
blocks on `ready`/`failed` (it dispatches the queue in a loop), sets mastering primaries /
luminance / max_cll / max_fall when `should_use_hdr_metadata` is set for that colour space, and
calls `set_image_description(..., RENDER_INTENT_PERCEPTUAL)`. `is_hdr_metadata_legal`
(`:1277-1299`) drops illegal combinations rather than risking a protocol error. On `failed`
without `extended_target_volume` it retries once without metadata.
* **`VK_EXT_hdr_metadata`:** `wsi_wl_swapchain_set_hdr_metadata` (`:1459-1463`) just stashes
`VkHdrMetadataEXT`; the real work is in `update_colorspace` above.
**Nothing to build in the driver for HDR10.** Mesa is done.
### 2.4 Our compositor advertises nothing colorimetric
Globals (`compositor.c:2527-2538`, plus `ahb_swapchain.c:406`,
`wl_clipboard.c:522-523`, `wl_text_input.c:275`, `wl_toplevel_icon.c:35`):
`wl_compositor` v6, `wl_subcompositor`, `wp_viewporter`, `wl_output` v2, `xdg_wm_base`,
`zwp_linux_dmabuf_v1` **v3**, `wl_seat` v5, `banner_desktop_v1`, `wp_presentation` v2,
pointer constraints, relative pointer, `banner_ahb_v1` v2, data device/control, text input,
toplevel icon. **No `wp_color_manager_v1`.** `protocols/` has no colour-management XML, and
protocol C glue is pre-generated into `generated/` (no `wayland-scanner` at app build time —
`CMakeLists.txt:1-4`), so adding a protocol means generating and committing the files offline.
The dmabuf format table is 8-bit only (`compositor.c:1323-1326`):
```c
#define DMABUF_NFMT 4
static struct { uint32_t fmt; uint64_t mods[DMABUF_NMOD]; int n; } g_dmabuf_fmts[DMABUF_NFMT] = {
{DRM_ARGB8888}, {DRM_XRGB8888}, {DRM_ABGR8888}, {DRM_XBGR8888}};
```
Good news: the machinery around it is already format-agnostic.
`vkp_dmabuf_modifiers` (`vk_present.c`) takes a DRM fourcc and asks the compositor's own Turnip via
`VkDrmFormatModifierPropertiesListEXT`, and `drm_to_vk` (`vk_present.c:105-123`) **already** maps
`AB30`/`XB30` → `A2B10G10R10_UNORM_PACK32`, `AR30`/`XR30` → `A2R10G10B10_UNORM_PACK32`, and
`AB4H`/`XB4H` → `R16G16B16A16_SFLOAT`. Adding 10-bit is a table edit plus the matching
`FOURCC` defines next to `compositor.c:415-421`.
The compositor's own output swapchain is oblivious (`vk_present.c:301-311, 437-466`): two instance
extensions (`VK_KHR_surface`, `VK_KHR_android_surface` — **no `VK_EXT_swapchain_colorspace`**),
and `VkSurfaceFormatKHR chosen = fmts[0];` (`:441`) — the first format the Android WSI happens to
return, with whatever colour space rides along. The scene image and the whole 13-pass effects
chain are 8-bit (`vk_present.c:54` `SCENE_FMT = VK_FORMAT_R8G8B8A8_UNORM`; `effects_chain.c:29`
`FX_FORMAT = VK_FORMAT_R8G8B8A8_UNORM`).
> **Naming collision, flag it in any UI work.** The drawer already has an "HDR" effect. It is
> `effects_chain.c:197` `P_HDR` / `app/src/main/cpp/winlator/hdr.frag` — a dual-radius bloom +
> contrast lift ported from `HDREffect.java`, `HDRPower = 1.30`, entirely SDR and entirely fake.
> Real HDR must be named something else ("HDR output", "HDR10") or users will conflate them.
### 2.5 Is there a protocol-free shortcut?
No. A `DRM_FORMAT_ABGR2101010` buffer is equally valid as SDR BT.709 or as PQ BT.2020; the format
and modifier describe byte layout, not colorimetry. That ambiguity is precisely why
`wp_color_management_v1` exists. We *could* add a `set_dataspace` request to our own
`banner_ahb_v1` and skip the standard protocol — but Mesa would never call it, so that would need
a driver patch (new wcp) to buy something Mesa already gives us for free. **Implement
`wp_color_manager_v1` v1 server-side; it is strictly cheaper.**
---
## 3. Android side
### 3.1 The real symbols, their API levels, and what the device has
`frameworks/native/include/android/surface_control.h`:
| Symbol | API level | Present in `/system/lib64/libandroid.so` on the Pocket FIT (API 34)? |
|---|---|---|
| `ASurfaceTransaction_setBufferDataSpace(tx, sc, ADataSpace)` | **29** | **PRESENT** |
| `ASurfaceTransaction_setHdrMetadata_smpte2086(tx, sc, AHdrMetadata_smpte2086*)` | **29** | **PRESENT** |
| `ASurfaceTransaction_setHdrMetadata_cta861_3(tx, sc, AHdrMetadata_cta861_3*)` | **29** | **PRESENT** |
| `ASurfaceTransaction_setExtendedRangeBrightness(tx, sc, float cur, float desired)` | **34** | **PRESENT** |
| `ASurfaceTransaction_setDesiredHdrHeadroom(tx, sc, float)` | **35** | absent (expected — device is 34) |
| `ASurfaceTransaction_setLuts` | 36 | n/a |
| `ASurfaceTransaction_setColorSpace` | **does not exist** | — |
| `ASurfaceTransaction_setLuma` | **does not exist** | — |
(Checked with `strings` on the device's own `libandroid.so`; `setEnableBackPressure` and
`setFrameRateWithChangeStrategy` confirmed present alongside, matching what `sc_layer.c` already
dlsyms.)
Our dlsym table is `sc_layer.c:82-97` — 15 entries, **none of them colorimetric**. The additions
needed are `setBufferDataSpace` (the only mandatory one), plus optionally
`setHdrMetadata_smpte2086` / `_cta861_3` and `setExtendedRangeBrightness`. All four are API ≤ 34,
i.e. inside the range `sc_layer.c` already targets, so the existing "missing symbol → path
unavailable" pattern covers older devices unchanged.
### 3.2 Dataspaces we would emit
`ADataSpace` = `STANDARD (63<<16) | TRANSFER (31<<22) | RANGE (7<<27)`, all in the NDK since 29:
| Constant | Value | Maps from |
|---|---|---|
| `ADATASPACE_BT2020_PQ` | 163971072 | `VK_COLOR_SPACE_HDR10_ST2084_EXT` |
| `ADATASPACE_BT2020_HLG` | 168165376 | `VK_COLOR_SPACE_HDR10_HLG_EXT` |
| `ADATASPACE_SCRGB_LINEAR` | 406913024 | `VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT` |
| `ADATASPACE_DISPLAY_P3` | 143261696 | `VK_COLOR_SPACE_DISPLAY_P3_NONLINEAR_EXT` |
| `ADATASPACE_SRGB` | 142671872 | `VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` (today's implicit default) |
Every layer on the device right now reports `dataspace=UNKNOWN (0)`, which SF treats as sRGB.
### 3.3 Buffer formats
`AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM = 0x2b` (API 26) ↔ `VK_FORMAT_A2B10G10R10_UNORM_PACK32`
— the normal 10-bit scanout format on Adreno DPUs. QTI gralloc on this device knows it:
`strings /vendor/lib64/libqdMetaData.so` lists `RGBA_1010102`, `RGBX_1010102`, `BGRA_1010102`,
`BGRX_1010102`. `AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT = 0x16` (API 26) exists but is a
compositing intermediate, not normally a scanout format — it was **not** found in the vendor
gralloc strings, so treat FP16 gralloc allocation on this SoC as unproven.
`AHardwareBuffer_isSupported()` (API 29) is the correct runtime probe, and we should use it rather
than assume.
The compositor's own AHB pool is hardcoded 8-bit (`sc_layer.c:302`
`.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM`) and the imported view is built with
`DRM_ABGR8888` (`sc_layer.c:326`). That is the *copy* path; the zero-copy path
(`sc_layer_present_ahb`, `sc_layer.c:430`) takes the game's own buffer and so already carries
whatever format the game allocated — it just never labels it.
### 3.4 What SurfaceFlinger does with an HDR layer on an SDR panel
From `services/surfaceflinger/CompositionEngine/src/Output.cpp`, `getBestDataspace()` /
`pickColorProfile()`:
* A `BT2020_PQ` layer sets `outHdrDataSpace = BT2020_PQ` but the output only *adopts* it when
`DisplayColorProfile::hasLegacyHdrSupport(BT2020_PQ)` is true — i.e. when the Composer HAL
reported HDR types for that display. Otherwise the output dataspace stays `DISPLAY_P3` with
`COLORIMETRIC` intent, and the layer is **tone-mapped, not rejected**.
* Because the HWC for an SDR-only display cannot consume a PQ buffer, the standard
`validateDisplay()` negotiation marks that layer **CLIENT** instead of **DEVICE**, and
RenderEngine/Skia composites it with the `libtonemap` SkSL gain shader
(`libtonemap_LookupTonemapGain`, uniforms `in_libtonemap_displayMaxLuminance` /
`in_libtonemap_inputMaxLuminance`).
* **This is the trap for us.** Our whole zero-copy win is that `banner_wayland_game` composites
DEVICE (proven 2026-09-13: DEVICE with ROT_90 + 1.5× scale at 144 Hz, −4 pts GPU busy, −3 %
power). Tagging that layer `BT2020_PQ` on a panel with no HDR types is very likely to drop it to
CLIENT — i.e. HDR would **buy a washed-out tone-map and pay for it with the zero-copy saving**.
Any implementation must gate the dataspace on the *display's* reported HDR types, not on the
game's request.
* `setExtendedRangeBrightness` is a different mechanism: it applies only to `RANGE_EXTENDED`
(scRGB) layers and works by having SF dim the *other* layers by `whitePointNits / maxWhitePoint`
(`renderengine/LayerSettings.h`) while leaving the extended layer undimmed, so the boosted layer
reads brighter by simultaneous contrast without exceeding the panel ceiling.
**It needs the display to expose an HDR/SDR ratio.** This panel does not (§4).
* `setDesiredHdrHeadroom` (API 35) is the PQ/HLG equivalent and overrides/`is overridden by`
`setExtendedRangeBrightness` — last call wins. Not available on the test device at all.
### 3.5 `has_HDR_display` vs `getHdrCapabilities()`
`ro.surface_flinger.has_HDR_display` is a **build-time** declaration (`TARGET_HAS_HDR_DISPLAY`)
that the device *has* a wide-gamut, high-luminance panel; it gates SF-internal colour-management
machinery. `Display.getHdrCapabilities().getSupportedHdrTypes()` is the **runtime, per-display**
answer, built from the Composer HAL's `getHdrCapabilities()` for that connector, itself derived
from the EDID CTA-861.3 Static Metadata Data Block. **They can disagree, and on this device they
do.** An empty list does not mean rejection — it means "tone-map it."
### 3.6 External display
On hotplug, the framework queries `getDisplayConfigs`, `getColorModes`, `getHdrCapabilities`,
`getDisplayCapabilities` for the new display handle exactly as for the internal panel, so
`Display.getHdrCapabilities()` on an external `Display` *is* architecturally wired to reflect the
monitor's EDID. Whether it actually does is a vendor-HWC question; empty lists for genuinely
HDR10 USB-C monitors are a commonly reported Snapdragon gap. **Unproven for the Pocket FIT — the
only way to know is to plug one in.**
---
## 4. What the test device can and cannot show
All read today from the Pocket FIT (Adreno 750, Android 14 / API 34), read-only:
```
DisplayDeviceInfo{"Built-in Screen": 1080 x 1920, ...
supportedModes [... supportedHdrTypes=[] (all four modes) ],
colorMode 0, supportedColorModes [0],
hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0,
mMaxAverageLuminance=500.0, mMinLuminance=0.0},
hdrSdrRatio NaN, brightnessMaximum 1.0 ...}
mBaseDisplayInfo: ... hdrSdrRatio not_available, userDisabledHdrTypes []
```
```
dumpsys SurfaceFlinger:
Wide-Color information:
Device supports wide color: 1
Device uses color management: 1
DisplayColorSetting: Managed
Display 4630946852602928515 color modes:
ColorMode::NATIVE (0)
Current color mode: ColorMode::NATIVE (0)
colorMode=NATIVE (0) renderIntent=COLORIMETRIC (0) dataspace=UNKNOWN (0) targetDataspace=UNKNOWN (0)
displayBrightnessNits=-1.000000 sdrWhitePointNits=-1.000000 displayBrightness=nullopt
(every layer) dataspace=UNKNOWN (0) hdr metadata types=0
```
```
getprop:
[ro.surface_flinger.has_HDR_display]: [true]
[ro.surface_flinger.has_wide_color_display]: [true]
[ro.surface_flinger.wcg_composition_dataspace]: [143261696] = ADATASPACE_DISPLAY_P3
[persist.sys.sf.color_mode]: [9] = ColorMode::DISPLAY_P3
[vendor.display.enable_hdr10_gpu_target]: [1]
```
Reading:
* The **build** declares an HDR, wide-colour device and even asks for `DISPLAY_P3` as the system
colour mode (`persist.sys.sf.color_mode=9`), but the **HWC reports `supportedColorModes=[0]`
(NATIVE only) and no HDR types**, so SF runs the display in NATIVE with COLORIMETRIC intent and
the P3 request is inert. The props are aspirational; the HAL is the truth.
* `hdrSdrRatio not_available` / `NaN` and `displayBrightnessNits = -1` mean
`Display.isHdrSdrRatioAvailable()` is false → **`setExtendedRangeBrightness` has nothing to act
on here.** The "brighten highlights on an SDR panel" trick is unavailable on this device.
* `supportedColorModes=[0]` also means **Display-P3 wide gamut is not available either** — not
just HDR. A P3-tagged layer would be colour-converted into the panel's native gamut, i.e. into
whatever the panel natively is, with no way for us to know what that is.
* 500 nits max luminance is below the ~540-nit bar AOSP's own `has_HDR_display` doc cites, and
there is no HDR type to signal anyway.
**Conclusion: visual HDR cannot be demonstrated on the Pocket FIT's internal panel, and neither
can wide gamut, and neither can extended-range brightness.** The only things provable on this
device are (a) 10-bit buffers end to end (banding reduction, measurable), and (b) plumbing
correctness by logs and `dumpsys`.
Vulkan-side, for completeness (device `strings`):
| Library | `VK_EXT_swapchain_colorspace` | `VK_EXT_hdr_metadata` |
|---|---|---|
| `/system/lib64/libvulkan.so` (platform loader — implements the Android swapchain) | **PRESENT** | **PRESENT** |
| `/vendor/lib64/hw/vulkan.adreno.so` (Qualcomm blob) | absent | absent |
| `/vendor/lib64/hw/vulkan.turnip.so` (vendor-shipped Turnip) | **PRESENT** | **PRESENT** |
| wcp v6 `libvulkan_freedreno_wayland.so` (game driver) | **PRESENT** | **PRESENT** |
The platform loader contains the string `native_window_set_buffers_data_space(%d) failed: %s (%d)`
— i.e. Android's own swapchain implementation is what turns a `VkColorSpaceKHR` into a surface
dataspace. So for the **copy path**, the compositor's output HDR is genuinely just "enable
`VK_EXT_swapchain_colorspace` at instance creation and pick a better surface format than
`fmts[0]`". No driver work.
---
## 5. The gap list, ordered, with sizes
Sizes are engineer-days for someone who already knows this code. "Prove" = device-visible in logs
or `dumpsys`, not by eye. **App-only unless marked.**
### Phase A — make the truth reachable (no user-visible HDR yet)
| # | Repo | Work | Size |
|---|---|---|---|
| A1 | app | **Advertise 10-bit DRM formats over `zwp_linux_dmabuf_v1`.** Add `AB30`/`XB30` (and optionally `AR30`/`XR30`, `AB4H`/`XB4H`) to `compositor.c:1323-1326` + `FOURCC` defines near `:415-421`. `vkp_dmabuf_modifiers` and `drm_to_vk` already handle them, so this is a table edit + a log line. Risk: a client picking a 10-bit format while the scene/effects chain stays 8-bit — the import works (`drm_to_vk`), the blit downconverts. Gate behind a flag until A4. | **0.5 d** |
| A2 | app | **Generate + commit `color-management-v1` protocol glue.** Fetch `staging/color-management/color-management-v1.xml` from wayland-protocols ≥ 1.41, run `wayland-scanner` offline, commit XML + `generated/color-management-v1-{protocol.c,server-protocol.h}`, add to `CMakeLists.txt`. Mechanical. | **0.5 d** |
| A3 | app | **Implement `wp_color_manager_v1` v1 server-side** (new `wl_color_mgmt.c`, ~400–500 lines): the global + bind-time feature/intent/primaries/tf events; the parametric creator with `set_primaries_named`/`set_tf_named`/`set_max_cll`/`set_max_fall`/`set_mastering_*`; image-description objects with `ready`/`failed`; `get_surface` → `wp_color_management_surface_v1` and `set_image_description` stored double-buffered on our `struct surface`. **Watch the blocking loop:** Mesa dispatches its queue waiting for `ready`, so we must send `ready` from the request handler's own flush, not defer it to the next frame, or a game stalls at swapchain creation. Advertise only what the *current output* can actually honour (see A5). | **2 d** |
| A4 | app | **Carry the image description onto the Android layer.** dlsym `ASurfaceTransaction_setBufferDataSpace` (+ `setHdrMetadata_smpte2086` / `_cta861_3`) in `sc_layer.c:82-97`; map surface image description → `ADataSpace` + `AHdrMetadata_*`; apply in `sc_layer_present_ahb` (`sc_layer.c:430-470`, next to the existing `setBuffer`). Plumb the per-surface description from `compositor.c` through `ahb_swapchain.c:219`. Also set an explicit `ADATASPACE_SRGB` on the existing 8-bit path so we stop relying on `UNKNOWN`. | **1 d** |
| A5 | app | **Capability gate.** Read `Display.getHdrCapabilities()` / `getSupportedHdrTypes()` / `isHdrSdrRatioAvailable()` (and the external `Display` when `ExternalDisplayController` has one) in Java, pass down over JNI, and *only then* advertise `bt2020`/`st2084_pq` from A3. Without this we advertise HDR on a panel that will tone-map it and drop the layer to CLIENT (§3.4). Include a `banner_log("color", ...)` line naming exactly what was advertised and why. | **0.5 d** |
**Phase A total: ~4.5 days. App-only. No new wcp.** End state: a game with `DXVK_HDR=1` on a
genuinely HDR display gets an HDR10 swapchain, its buffer reaches an `ASurfaceControl` layer
tagged `BT2020_PQ` with real mastering metadata, and `dumpsys SurfaceFlinger` proves it.
### Phase B — the copy path and the effects chain (only if A is kept)
| # | Repo | Work | Size |
|---|---|---|---|
| B1 | app | **10-bit output swapchain.** Add `VK_EXT_swapchain_colorspace` to `vk_present.c:301-311` (`enabledExtensionCount` 2 → 3), replace `chosen = fmts[0]` (`:441`) with a real preference walk (HDR10 + `A2B10G10R10` when the display supports it, else `R8G8B8A8_UNORM` + sRGB). The Android platform loader does the rest. | **0.5 d** |
| B2 | app | **Widen the scene + effects chain.** `SCENE_FMT` (`vk_present.c:54`) and `FX_FORMAT` (`effects_chain.c:29`) → `A2B10G10R10_UNORM_PACK32` (or FP16 for real HDR headroom). Every one of the 13 passes samples/writes it; the debanding pass in particular must be re-tuned because its strength is calibrated for 8-bit quantisation. Memory and bandwidth go up ~25 % (or 100 % for FP16). This is the expensive item and it regresses SDR perf if done unconditionally. | **2 d** |
| B3 | app | **Layer pool at 10-bit.** `sc_layer.c:302` pool format → `R10G10B10A2_UNORM` when the chain is 10-bit, guarded by `AHardwareBuffer_isSupported()`, with the existing `sniff_modifier`/linear-fallback retry extended. | **0.5 d** |
**Phase B total: ~3 days. App-only. No new wcp.**
### Phase C — scRGB, and the one thing that needs a wcp
| # | Repo | Work | Size |
|---|---|---|---|
| C1 | **driver (new wcp)** | Map `VK_FORMAT_R16G16B16A16_SFLOAT` → `AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT` (0x16) in `banner_ahb_format_for` (`patches/wayland/banner_ahb_wsi.py:225-237`) so FP16 swapchains are not hidden by `banner_ahb_skip_format` when zero-copy is on. One enum + a rebuild of eight drivers. **This is the only wcp-requiring item, and only for scRGB zero-copy.** Blocked on proving FP16 gralloc allocation works on Adreno at all (§3.3). | **0.5 d + a driver build cycle** |
| C2 | app | Advertise `extended_target_volume` + `ext_linear` + `AB4H`/`XB4H`, emit `ADATASPACE_SCRGB_LINEAR`, and wire `ASurfaceTransaction_setExtendedRangeBrightness` from `Display.getHdrSdrRatio()`. | **1 d** |
| C3 | **proton-wine (new wcp)** | *Optional, quality only.* Synthesise an EDID with a CTA-861.3 HDR static metadata block in `winewayland.drv` so DXVK reports the real panel luminance in `DXGI_OUTPUT_DESC1` instead of its 1499-nit dummy. | **1–2 d** |
---
## 6. Cheap partial wins — and which are visible on a 500-nit panel
| Idea | Verdict on the Pocket FIT | Notes |
|---|---|---|
| **10-bit output (banding)** | **Visible, and the best value here.** | This is the one real win. B1 alone (0.5 d) gets a 10-bit output swapchain; A1 (0.5 d) lets the *game* render 10-bit. Adreno 750 + the DPU handle `RGBA_1010102` natively, and `vendor.display.enable_hdr10_gpu_target=1` says the GPU target path expects it. Pairs with the existing debanding pass — arguably makes it unnecessary for 10-bit-capable games, which saves a full-screen pass. Visible on gradients (skyboxes, fog, dark interiors) where our deband pass currently earns its keep. |
| **Display-P3 wide gamut on SDR** | **Not visible. Do not build.** | `supportedColorModes=[0]` / `ColorMode::NATIVE` — the HWC exposes no P3 mode. `persist.sys.sf.color_mode=9` asks for P3 and is ignored. Tagging a layer `ADATASPACE_DISPLAY_P3` would make SF convert P3 → the panel's native gamut, which on a NATIVE-mode display is an unknown-to-us transform. Net effect on this device: at best nothing, at worst a colour shift. |
| **`setExtendedRangeBrightness` to lift highlights** | **Not available.** | `hdrSdrRatio not_available` / `NaN`, `displayBrightnessNits=-1`, `sdrWhitePointNits=-1`. `Display.isHdrSdrRatioAvailable()` is false, so there is no headroom to claim and SF has no nits data to dim the other layers against. |
| **`setDesiredHdrHeadroom`** | **Not available.** | API 35; device is API 34. Absent from `libandroid.so`. |
| **Set `ADATASPACE_SRGB` explicitly on our layers** | **Invisible but worth doing anyway (part of A4, ~0 extra cost).** | Every layer currently reports `dataspace=UNKNOWN (0)`. It works today because SF treats UNKNOWN as sRGB, but it is an accident we are relying on, and it will become wrong the moment any colour management is enabled. |
**So: of the three "cheap wins" in the brief, exactly one — 10-bit — is real on this hardware,
and it is worth about 1 engineer-day (A1 + B1) on its own, independent of HDR.**
---
## 7. How we would ever prove it
### 7.1 Which games actually emit HDR under DXVK / VKD3D
Requirements: a flip-model swapchain at `R10G10B10A2_UNORM` or `R16G16B16A16_FLOAT`, an
`IDXGISwapChain3::SetColorSpace1` call, and `DXVK_HDR=1` in the environment. Known-good
candidates, easiest first:
* **D3D11 + scRGB:** *Shadow of the Tomb Raider*, *Resident Evil 2/3 Remake*, *Devil May Cry 5*,
*Mass Effect Legendary Edition*. RE Engine titles are the usual smoke test on Linux because they
call `SetColorSpace1` early and unconditionally when DXGI reports HDR.
* **D3D12 + HDR10:** *Cyberpunk 2077*, *Forza Horizon 5*, *Horizon Zero Dawn*. These go through
vkd3d-proton's stricter path (§1.1) — good for catching a half-done implementation, since a
wrong answer **stalls presentation** instead of degrading.
* **Sanity harness, no game needed:** anything that calls `vkGetPhysicalDeviceSurfaceFormatsKHR`
under the layer. `vulkaninfo` is not available in the prefix, but a tiny Win32 Vulkan probe
built against the layer's headers, or simply DXVK's own `VK_INSTANCE_LAYERS`-free log with
`DXVK_LOG_LEVEL=info` and `DXVK_HDR=1`, will say whether the colour space was accepted. **Start
here** — it is the cheapest signal and needs no HDR display at all.
### 7.2 Platform-side verification (not by eye)
Order of proof, each step gating the next:
1. **Compositor log.** New `banner_log("color", ...)` lines: which primaries/tf we advertised and
why; every image description created (primaries, tf, max_cll, max_fall); every dataspace set on
the layer. This is the only step that works on any device.
2. **Guest log.** `DXVK_HDR=1` + `DXVK_LOG_LEVEL=info` → DXVK prints the chosen colour space and
format at swapchain creation. If `SetColorSpace1` was refused, DXVK says so. Mesa's WSI prints
`Not using HDR metadata to avoid protocol errors` when `is_hdr_metadata_legal` rejects metadata
— catching that string is a direct test of A3's metadata plumbing.
3. **`dumpsys SurfaceFlinger | grep banner_wayland_game`.** Today it prints
`dataspace=UNKNOWN (0) hdr metadata types=0`. Success = `dataspace:0x9c60000` (`BT2020_PQ`, 163971072 =
`STANDARD_BT2020|TRANSFER_ST2084|RANGE_FULL`) and `hdr metadata types=` non-zero. **Also record `composition:` on the same line**
— if it flips DEVICE → CLIENT, HDR cost us the zero-copy win (§3.4) and that is a failure, not a
success.
4. **`dumpsys display`.** On whatever display is in use: `supportedHdrTypes=[...]` non-empty,
`colorMode` ≠ 0, `hdrSdrRatio` ≠ `not_available`. This is the gate for steps 5–6; on the Pocket
FIT's internal panel it will never pass.
5. **`dumpsys SurfaceFlinger` Wide-Color section.** `Current color mode` should move off
`ColorMode::NATIVE (0)`, and the output line's `dataspace=`/`targetDataspace=` should stop being
`UNKNOWN (0)`.
6. **Only then, by eye.**
### 7.3 Getting a display that can actually show it — three options, ranked
1. **External HDR display over USB-C (recommended, cheapest).** The app already reparents the game
into a `Presentation` on a secondary display —
`app/src/main/java/com/winlator/star/display/ExternalDisplayController.java`,
`findPresentationDisplay()` picks a `DISPLAY_CATEGORY_PRESENTATION` display that is not the
activity's own. In the Wayland session the compositor's `SurfaceView` is the reparented view, so
`sc_layer.c`'s `ASurfaceControl_createFromWindow(vkp_window(), ...)` follows it automatically —
**no new plumbing to test on an external panel.** Risk: `Display.getHdrCapabilities()` for a
DP-alt-mode display depends on the vendor HWC reading the external EDID, and empty lists for
genuinely HDR10 monitors are a common Snapdragon gap (§3.6). **Step zero is a five-minute,
zero-code test: plug an HDR monitor in and run `dumpsys display | grep -A2 hdrCapabilities`.**
If the external display reports HDR types, everything else follows. If it does not, options 2
and 3 are all that remain.
2. **The user's Galaxy Fold (Adreno 840).** Samsung foldables/flagships do report HDR10+ on the
internal panel, so this is the most likely device to show real HDR with no extra hardware. But
the Fold is *already* the outstanding unknown for basic Wayland bring-up (checkpoint item 1:
"Other GPUs… the Galaxy Fold (A840) logs are still outstanding"). Chasing HDR there before
plain Wayland is proven there inverts the priority order.
3. **Plumbing-only verification on SDR.** Everything in §7.2 steps 1–3 works on the Pocket FIT.
This proves the code is *correct* but proves nothing about whether it *looks right* — and
tone-mapping quality is the entire user-visible payload of HDR. **Do not ship on this evidence
alone.**
---
## 8. Verdict
**Not now. Do the 10-bit slice; park HDR behind the Fold.**
The reasoning, plainly:
* **The effort is genuinely small and smaller than we thought.** ~4.5 days to a plumbed HDR10
path, **app-only, no new wcp** — because Mesa's colour-management client is already inside the
shipped wcp v6 driver and our zero-copy WSI patch already maps 10-bit to gralloc. The phase-4
memory note saying HDR "does need a new wcp" should be corrected: only scRGB/FP16 zero-copy (C1)
does, and that is optional.
* **But the payload is zero on the only device we can test on.** The Pocket FIT reports no HDR
types, no colour modes but NATIVE, no HDR/SDR ratio and no brightness nits. We could not see the
feature, could not tune the tone-mapping, and could not tell a correct implementation from a
subtly wrong one. Shipping HDR proven only by log lines would be the exact opposite of this
project's "device-proven, not CI-green" rule.
* **On this device it would be a net negative if enabled.** §3.4: an HDR-dataspace layer on a
display with no HDR types drops from DEVICE to CLIENT composition. That trades the measured
zero-copy win (−4 pts GPU busy, −3 % power) for a GPU tone-map pass and a washed-out image. A
capability gate (A5) prevents the harm, but a gate that is always closed on our test hardware is
a feature nobody can exercise.
* **It is also out of order.** The pre-4 checkpoint's ranked list puts "other GPUs" and "prove
zero-copy where it can win, then make it the default" first, with multi-layer presentation in
flight. HDR competes with those for the same `sc_layer.c` surface area — and multi-layer will
rewrite exactly the code A4 touches. Landing HDR into `sc_layer.c` now means merging it twice.
**What the user would actually see for the effort, honestly:**
* **Today, Pocket FIT, full HDR (7.5 days):** nothing. Identical picture, plus a `dumpsys` line
that says `BT2020_PQ`, plus a risk of losing zero-copy.
* **Today, Pocket FIT, the 10-bit slice only (A1 + B1, ~1 day):** *slightly* smoother gradients in
dark scenes and skyboxes for games that offer a 10-bit swapchain, and a plausible route to
retiring the debanding pass (one fewer fullscreen pass) for those games. Small, real, and it is
the foundation Phase A sits on anyway — so it is not wasted work if HDR later goes ahead.
* **Later, on an HDR display (external monitor or the Fold), full Phase A:** genuinely brighter
highlights and a wider gamut in the ~15 PC titles that implement HDR properly, on the one Android
emulator that can do it. That is a real headline feature and a real differentiator — but only
once there is a panel to show it on.
**Recommended sequence:**
1. **Five minutes, zero code:** plug an HDR monitor into the Pocket FIT's USB-C and run
`dumpsys display | grep -A2 hdrCapabilities`. This single reading decides everything —
it is the difference between "we can develop and prove HDR today on hardware we own" and
"HDR is blocked on the Fold."
2. **~1 day, independent of HDR:** the 10-bit slice (A1 + B1), gated and A/B'd for banding and
perf. Ship it as a banding/quality improvement on its own merits.
3. **Park Phase A (A2–A5)** until either step 1 comes back positive or the Fold is in the loop
and plain Wayland is proven there. Revisit *after* multi-layer presentation lands, so the
`sc_layer.c` dataspace work is written once against the final layer API.
---
## 9. What I could not establish, and why
* **Whether FP16 (`R16G16B16A16_FLOAT`) AHardwareBuffers are allocatable on this SoC.** Not in the
vendor gralloc strings, and I did not install or run a probe binary (other agents are testing on
the device). Decides whether scRGB zero-copy (C1) is possible at all. Cheap to settle later with
a one-call `AHardwareBuffer_isSupported()` check inside the compositor's existing startup log.
* **Whether an external HDR display over USB-C actually reports HDR types on the Pocket FIT.**
No external display was connected and I did not connect one. This is the single highest-value
unknown in the whole report (§8, step 1).
* **Whether an HDR-tagged layer really drops DEVICE → CLIENT on this HWC.** The AOSP code path is
clear (§3.4) but it is vendor-HWC behaviour and I did not test it — testing it would mean
launching a game, which was out of scope.
* **What the Galaxy Fold (Adreno 840, Android version unknown to me) reports.** Not connected.
Its `getHdrCapabilities()` and `isHdrSdrRatioAvailable()` are the other decisive readings.
* **Exact Mesa line numbers.** Cited against `wsi_common_wayland.c` fetched from
`gitlab.freedesktop.org/mesa/mesa` at `7cda7850edd103ace21aac37d416d2fdf7a282e1`, the commit in
`banners-turnip-wayland/mesa_hash.txt`. They are accurate for that file; they will drift if the
Turnip base moves.
* **DXVK/vkd3d-proton line numbers** are cited against upstream `doitsujin/dxvk@8759acd1` and
`HansKristian-Work/vkd3d-proton@2a38e799` (master as of today), not against the exact
v3.1-gplasync / 3.1.0-wave64-relax builds we ship. The feature presence in *our* builds was
verified independently by `strings` on the device (§1.1); the line numbers are for orientation.
* **Whether any of the games in §7.1 launch and run correctly under our Wayland backend at all.**
Untested. Picking the HDR test title is its own small exercise and should start from games
already known to work in a Wayland container.
---
## 10. Round 1 — as built (2026-09-14, `feat/wayland-hdr`)
Phase A (A1–A5) for HDR10, opt-in, proven by the app's own logs because the first HDR panel in the
loop is a tester's non-rooted Galaxy Fold (Adreno 840, Android API 37: HDR10/HLG/HDR10+, 1351 nits,
HDR/SDR ratio available). App-only; no layer change.
### 10.1 The gate (`wl_color_mgmt.c`, `banner_color_init`)
Open only when ALL hold when the compositor starts:
| Input | Where it comes from |
|---|---|
| `BANNER_WAYLAND_HDR=1` (or `true`/`on`) | container or shortcut env (`XServerDisplayActivity.resolvedWaylandHdrMode`) |
| the game's display lists **HDR10** | `DisplayHdrInfo.supportsHdr10` for `hdrTargetDisplay()` (the TV when the game is on it) |
| display layers with dataspace control | `sc_layer_can_tag_hdr()` = SurfaceControl API + `ASurfaceTransaction_setBufferDataSpace` |
| the zero-copy global | `ahb_swapchain_advertised()` |
`BANNER_WAYLAND_HDR=force` skips the display row only (testing the negotiation on an SDR panel —
SurfaceFlinger then tone-maps the layer). A **closed** gate advertises nothing (no
`wp_color_manager_v1` global, no 10-bit dma-buf rows), writes `color … HDR gate CLOSED: <why>` and
`HDR on screen: no, because <why>`, and the session is otherwise byte-for-byte the old one. With the
switch unset the module is silent unless the display lists HDR10 or `DXVK_HDR=1` is set (one hint line).
On an HDR10 display the app also turns **zero-copy presentation on** for the session (an HDR frame is
only right on the game's own layer) and exports `BANNER_WSI_AHB=1` to match.
### 10.2 What an open gate offers (§2.2, exactly what Mesa binds)
`wp_color_manager_v1` **version 1**: intent `perceptual`; features `parametric` +
`set_mastering_display_primaries` (Mesa only sends mastering luminance inside that feature's branch);
primaries `bt2020`; transfer `st2084_pq`. Not advertised: `srgb` (so Mesa creates no colour surface for
SDR swapchains at all), HLG (no DXGI swapchain uses it), `ext_linear`/`extended_target_volume` (scRGB =
Phase C, needs FP16 gralloc + a layer change), ICC, `windows_scrgb`. `zwp_linux_dmabuf_v1` gains
`AB30`/`XB30` (A2B10G10R10 — Mesa lists a VkFormat only when both the alpha and opaque fourcc are there),
appended after the four 8-bit rows. Protocol glue generated from wayland-protocols **1.41** with
wayland-scanner 1.24.0 (`protocols/color-management-v1.xml`).
Strictness: protocol errors only where a conforming client can never hit them (un-advertised
feature/intent/primaries/tf, a property set twice, an incomplete set, a not-ready description).
Everything a conforming client can produce on a bad day is logged and absorbed instead — out-of-range
HDR metadata is dropped from what Android gets (and `max_cll`/`max_fall` of 0 = "unknown", which Mesa
sends as-is), a second colour surface for the same `wl_surface` replaces the first, requests on inert
objects are ignored. `ready` is sent from inside `create` (Mesa blocks its swapchain on it).
Output and surface-feedback image descriptions answer `failed` (Mesa uses neither).
### 10.3 The layer path (`sc_layer.c`)
`setBufferDataSpace` (+ `setHdrMetadata_smpte2086` / `_cta861_3` when present) ride the same
transaction as the buffer, per layer, only on change, re-sent on every new SurfaceControl (recovery
swap, window change). A layer that was never tagged is never touched; once tagged, an untagged frame
puts it back to `UNKNOWN` and clears the metadata. The zero-copy frame gets the surface's current
description; the 8-bit pool copy of an HDR frame keeps the PQ tag (colours right, 8-bit precision,
logged once); the effects pass and the overlay layer are always untagged.
### 10.4 When an HDR frame cannot be zero-copy — round 1's decision
No tone-mapping anywhere (the copy path is `vkCmdBlitImage` into an 8-bit sRGB swapchain; a PQ→SDR
shader pass is its own project). Instead:
- **Effects on / frame generation on:** an HDR fullscreen game **keeps its display layer**; the
effects chain and frame generation are skipped for it (both are 8-bit SDR and would wash it out),
and the base surface's black frame skips them too (`vkp_render_plain`) so generated presents cannot
pace the loop. Logged when it starts and when it ends.
- **A window above the game on a display that cannot compose a second layer** (phase 5's
rotated+scaled rule is untouched), **a windowed HDR game**, **zero-copy switched off**, **layer
unavailable:** the scene goes through the copy path and the HDR frames are shown **untone-mapped
(washed out)**; every such scene is counted and the reason logged.
### 10.5 Proof without root — what a tester's logs say
`Download/Wayland-logs/wayland-*.log`, tag `color` (plus the existing `display` capability line):
| Line | Means |
|---|---|
| `HDR gate OPEN: … "Built-in Screen" … reports HDR types HDR10, …` | the offer is on, with its inputs and which Android colour calls exist |
| `<program> bound wp_color_manager_v1 version 1 …` | Mesa saw it: `VK_COLOR_SPACE_HDR10_ST2084_EXT` is now listed |
| `<window>: colour-management surface created` | a swapchain asked for a non-sRGB colour space |
| `image description #N from <program>: BT.2020, ST 2084 (PQ); mastering …; max CLL …, max FALL …` | the game's colour space + `vkSetHdrMetadataEXT` values |
| `<window> presents WxH buffers in XB30 (10-bit A2B10G10R10) …, gralloc RGBA1010102 (10-bit)` | the 10-bit swapchain really arrived |
| `banner_wayland_game: dataspace BT2020_PQ (0x9c60000) set on the display layer …` | the tag + metadata SurfaceFlinger received |
| `HDR last 10 s: N frames on the display layer tagged BT2020_PQ …` | steady state |
| `display HDR/SDR ratio X (was Y; HDR frames on screen: yes …)` | `Display.getHdrSdrRatio()` — rises above 1.00 only when Android grants HDR headroom |
| `HDR on screen: yes / tagged but NOT confirmed / no, because …` | the verdict (written on changes, at game exit and at session end; the last one counts) |
DXVK's own `<exe>_dxgi.log` / `<exe>_d3d11.log` add `Color space: VK_COLOR_SPACE_HDR10_ST2084_EXT` and
the 10-bit format at swapchain creation; Mesa's `Not using HDR metadata to avoid protocol errors`
(stderr → `wine_debug.log`) means the game's metadata failed Mesa's own legality check.
## 11. Round 2 — as built (2026-09-14, `feat/wayland-hdr`; CI only, not device-proven yet)
Round 1 is proven on the tester's Fold (God of War, DXVK v3.1: 10-bit zero-copy, `BT2020_PQ` on the
layer, HDR/SDR ratio 1.00 → 2.51, `HDR on screen: yes`). Round 2 makes it a setting and removes
round 1's big limitation (§10.4: no tone-mapping, effects/frame generation skipped, washed-out copies).
The gate itself is unchanged (§10.1) apart from its first input.
### 11.1 The setting, and what the launch does with it
- `Container.isWaylandHdr()` (extraData `waylandHdr`) and the shortcut extra `waylandHdr` (`1`/`0`/unset
= the container's) — one resolver, `display/WaylandHdr.effective()`, used by the three editors
(container, game shortcut, XMB game settings) and the launch path. `BANNER_WAYLAND_HDR` in the
environment still overrides it (`1` / `0` / `force`). Editors grey the row with the reason when the
built-in screen does not list HDR10 (`WaylandHdr.unavailableReason`), and say it applies from the
next launch.
- HDR on for the session (setting resolved on AND HDR10 display, or `force`): `DXVK_HDR=1` is exported
after the user's env merge unless the user set `DXVK_HDR` themselves.
- Every Wayland session whose display lists **HDR10** exports `BANNER_WAYLAND_HDR_MAX_NITS`,
`…_MAX_AVG_NITS`, `…_MIN_NITS` (decimal nits from `Display.getHdrCapabilities()`; unknown values and
a max of 0 are left out) — the contract the Wayland layer from versionCode 10 turns into EDID HDR
metadata for DXGI (checked by the Wine side against DXVK's own EDID parser: the Fold's 1351/1351/0 →
DXGI max 1345.43, max full-frame 1345.43, min 0.01). Never on an SDR display, whose EDID would then
claim PQ support. One `session environment:` line logs them. Android has no live absolute nits; the
live evidence stays the HDR/SDR ratio.
- A "DXVK before 3.0 needs layer v10" warning was built and then **removed**: its premise was wrong
(Wine already exposes `VK_EXT_swapchain_colorspace` at instance level; DXVK ≥ 2.1 looks for it in the
device list, logs 0 on every Mesa system — the working v3.1 run logs 0 too — and never uses it for the
HDR decision). Why God of War offered no HDR option on DXVK 2.4.1 is open (Fold A/B).
### 11.2 HDR-aware composition (`hdr_compose.c`, `hdr_encode.frag`, compositor.c `hdr_plan`)
Every scene with an HDR draw takes one of four routes (logged on each change, with the reason):
| Route | When | What reaches the display |
|---|---|---|
| 0 | the HDR game alone fullscreen, zero-copy on, no effects, nothing above it (or one window the overlay layer can carry) | the game's own 10-bit gralloc frames on its layer, `BT2020_PQ` (round 1) |
| 1 | effects on, a window above on a display that cannot take a second layer, a windowed game, zero-copy off | **the HDR picture**: every draw blitted 1:1 into a 10-bit "mixed" image (each pixel still in its own encoding), ONE encode pass to PQ BT.2020 (SDR pixels sRGB → linear → BT.2020, placed at 203 nits, `BANNER_WAYLAND_HDR_SDR_NITS` overrides), the effects chain run on it in 10-bit (`vkp_effects_set_formats`), copied into a 10-bit (`RGBA1010102`) layer buffer, tagged with the game's description; 8-bit buffers if gralloc refuses 10-bit (logged) |
| 2 | frame generation on | composed into PQ in the 8-bit scene image, through the frame-generation engine, presented through an **HDR10 swapchain** (`VK_EXT_swapchain_colorspace` enabled on the compositor instance in HDR sessions only; first `A2B10G10R10` else FP16 pair the surface lists with `HDR10_ST2084`); where the surface lists none, **tone-mapped** into the ordinary swapchain. 8 bits through the engine: gradients may band |
| 3 | an HDR frame the compositor could not import | layer only; effects/frame generation skipped for it (round 1's behaviour, now the rare case) |
The tone map (mode 1 of `hdr_encode.frag`): PQ → nits → BT.2020 → BT.709, relative to SDR white, maxRGB
curve linear to a 0.8 knee then an exponential roll-off that puts the content peak (the game's max CLL,
else mastering max, else 1000 nits) at 98 %, sRGB-encoded. Which pixels are HDR comes from up to six
top-first scene rects + a mask in the push constants (more windows over the game → the lowest are
treated as SDR, logged once). A composed frame that cannot reach the layer falls back to route 2 for as
long as that lasts (logged once each way).
### 11.3 The live switch (lead's addition): drawer → Graphics → "HDR output"
Shown only while the gate is open (the activity's 1 s HDR sampler flips `waylandHdrAvailable`). Per
session, starts on, nothing saved. Off (`banner_color_set_output(0)`, posted through the host queue
to the compositor thread, logged, redraw): route 0 becomes route 1 with the tone map, the picture goes
on the layer **untagged** on 8-bit buffers (the layer's dataspace returns to `UNKNOWN`); route 2
rebuilds the swapchain as SDR and tone-maps; route 3 cannot (nothing can read those frames) and stays
HDR, said in the log. The game is told nothing (its DXVK_HDR and colour-manager offer were decided at
launch). The HUD reads `Wayland · HDR` while HDR frames are on screen (tagged in the last 1.5 s and
ratio > 1.01 where reported), `Wayland · HDR off` while the switch is off. Frames tone-mapped by the
switch are counted separately in the verdict.
### 11.4 Live headroom (lead's addition after the second Fold run)
The Fold's second run (brightness slider at maximum) showed the ratio at 3.23 for a minute and then
**1.00 for 3+ minutes with HDR frames on screen the whole time**, while the verdict kept saying "rose to
3.23". Android simply gave the layer no headroom (SDR white already at the panel's limit). Now, from the
ratio samples the app already sends (1 s + the display listener): the time with HDR frames on screen and
the part of it with ratio > 1.01; a no-headroom streak (HDR frames on screen, ratio <= 1.01) that after
5 s writes `no HDR headroom for 5 s … the screen brightness is probably at maximum …` (and `HDR headroom
is back …` when it ends), tags the 10 s line and the steady ratio line, puts the HUD on `Wayland · HDR
(no headroom)` and the drawer row on the same hint (`banner_color_hdr_state` = 2); and the verdict
reports `headroom above 1.00 for P% of the HDR time (a of b s), now R` plus the hint while the streak
lasts. Cheap: no new sampling, a few integers under the existing lock.
**Round-2 Fold run (20:23, run 34911468350):** the first no-headroom streak was a **screen recording**,
not the brightness: the user's recording (210.8 s, BT.709 SDR H.264, finalised 20:27:30, so started
~20:23:59) lines up with the ratio falling 2.98 → 1.00 at 20:24:00.9 and staying there to the end. So
Android (at least Samsung's) turns HDR headroom off while the screen is recorded. Every hint now names
both causes; whether maximum brightness does it too is still unmeasured (tester note step H).
### 11.5 The HUD's HDR line (round 2b, the user's layout request)
`40.3ms · Wayland · HDR (no headroom)` ran off the pill's right edge (the capsule's ends curve in, and the
latency line is the bottom of the stack). The display-server label is plain `Wayland` again, and the HDR
state is a line of its own directly under it, fed as a `FusionHdr` code (`FusionHudView.setHdrState`):
`HDR` / `HDR (no headroom)` / `HDR off` / `HDR tone-mapped` (switch on, frame generation without an HDR10
swapchain) / `HDR ready` (gate open, no HDR frames right now). `NONE` - every session whose gate is
closed - draws nothing, so those HUDs are unchanged to the pixel. Pill: the extra line makes the capsule
taller and its ends rounder, so in HDR sessions `fitCapsule()` measures every glyph's ink against the
two rounded ends (plus the outline and 2 sp) and shifts/widens the pill until nothing crosses. Full: an
`HDR` row under `DISP`; Tiles: the *Display* tile's sub-line; Minimal: a line under the footer; Mega: a
fragment after `DISP` in the bottom band. Only the Fusion HUD shows the display server at all; the
classic, GameHub and GameNative HUDs never received it and are untouched.
### 11.6 Round 2d (Fold evidence from 2c)
- **The format scan was a false negative.** FG with the AIO HDR card logged `lists no HDR10 swapchain
format (37/0 37/1000104001 … 37/1000104012)`: `swap_init` read the surface's list into
`VkSurfaceFormatKHR fmts[32]`, and with `VK_EXT_swapchain_colorspace` the Android WSI lists every
format once per colour space (11 on the Fold) - the 10-bit/FP16 rows were past entry 32, and the log's
160-char buffer cut the list too. Now: all pairs read (heap), one line with the total, colour spaces
per format and every HDR-capable pair (10-bit/FP16 first); HDR10 pick A2B10G10R10 → A2R10G10B10 →
FP16 → 8-bit; a driver that refuses the HDR10 swapchain gets the SDR one at once. The only other
fixed array near it (queue families, 16) does not hold surface formats or present modes; FIFO is the
only present mode used, so no list is read.
- **VK_EXT_hdr_metadata** enabled in HDR sessions where the device lists it; the HDR10 swapchain gets the
game's SMPTE 2086 / CTA-861.3 once per swapchain and image description (none if the game sent none).
- **FP16 frame generation for HDR.** The bridge and both engines already take a format (the chain,
the ring); lsfg-vk itself runs HDR in `R16G16B16A16_SFLOAT`. With HDR frames through an HDR10
swapchain the scene (`g_fgscene`), the encode pass (new FP16 output), the effects and the engine run
in FP16 where `fge_format_ok` (storage + linear sampling) passes; the PQ signal is fed as-is (the
shaders' own HDR flags stay off - PQ is already perceptual). A chain that fails to build in FP16 is
sticky inside both engines, so the bridge refuses the format for the session and restarts the engine
in 8 bits instead of marking frame generation failed.
- **Evidence beside the headroom** (app, non-root): PowerManager thermal status (+ listener) and
`getThermalHeadroom(10)` every 10 s, `Settings.System` brightness + mode (+ observer), screenshots
(`registerScreenCaptureCallback`, API 34) and screen recording (`addScreenRecordingCallback`, API 35,
reflection) with the normal `DETECT_SCREEN_CAPTURE` / `DETECT_SCREEN_RECORDING` permissions. Changes
are logged; the no-headroom lines, the 10 s line and the verdict carry the values and name the likely
cause (recording / hot / brightness at maximum) or say none is visible. The verdict lists only the
paths that carried frames, to leave room for it.
### 11.7 Round 2e (headroom request, display ceiling, coverage)
2d on the Fold: all pass (68 surface pairs, HDR10 swapchain A2B10G10R10, FP16 FG at 120 fps, 0 tone-mapped,
VK_EXT_hdr_metadata, headroom 3.00 at brightness 255 **auto**). A tester's ROG Phone 9 Pro (Adreno 830,
API 36): correct HDR frames, composed path, HDR layer ~80% of the 2400x1080 screen (windowed card), not
hot, not recording, brightness 8-223 manual - and the HDR/SDR ratio at **1.00 the whole session**.
- `ASurfaceTransaction_setDesiredHdrHeadroom` (API 35) on the game layer, per HDR frame, only on change:
content peak (max CLL, else mastering max, else the display's peak) / SDR white, capped at the display's
highest ratio; 0 (no preference) when the layer stops carrying HDR; re-sent on a fresh SurfaceControl.
The screen surface (HDR10 swapchain for frame generation) gets the same through
`SurfaceView.setDesiredHdrHeadroom` (API 35, reflection; `SurfaceControl.Transaction` fallback), applied
by the app from `banner_color_screen_headroom()`.
- `Display.getHighestHdrSdrRatio()` (Android 16+, reflection) in the display line and the verdict; 1.00
means no boost at all, and the request is then not made.
- HDR layer coverage in the dataspace line, the composed-picture line and on geometry changes.
- "brightness at maximum" only for MANUAL >= 250/255; no visible cause -> "this phone may not boost HDR
from apps (the requested headroom was …, the display's highest ratio is …)".
### 11.8 What is still NOT done / not proven
- Round 2's composition, switch and HUD line were device-tested in parts on the Fold (setting-driven
gate, auto environment, composed picture, headroom lines, HUD); 2d is CI only.
- The engines' own HDR flags (lsfg `hdr_support` / FSR3 HDR input) stay off: the PQ signal is treated as
perceptual SDR-like data. If interpolation artefacts show only in HDR, that is the next thing to try.
- Colour effects (brightness/contrast/saturation/gamma, the "HDR" bloom) operate on the PQ signal in
route 1 and look stronger than in SDR; sharpening/AA/CRT/upscalers are perceptually fine.
- scRGB (FP16) swapchains (Phase C) — unchanged: needs FP16 gralloc and a layer change.
@@ -1,472 +0,0 @@
# Bannerlator Wayland runtime (feat/wayland-runtime)
Experimental **parallel** display runtime: run/launch games through Wine's
`winewayland.drv` talking to our own embedded Wayland compositor, instead of the
X11 path (pure-Java X11 server + `libwinlator.so`). The X11 runtime stays the
default and is untouched — this is a separate flavor/branch.
## What's already proven (spike repo `bannerlator-wayland`, device-tested on Adreno 750)
- Minimal libwayland-server compositor: globals + xdg-shell handshake + buffer commit.
- **Turnip's Vulkan WSI exports real zero-copy dmabufs to our external compositor**
(same Mesa path winewayland.drv uses for DXVK/VKD3D) — risk #1 retired.
- Compositor imports that dmabuf into its own Turnip VkImage (`vk_import.c`).
The staged `src/` here is that proven code, to grow into the app-embedded compositor.
## Dependencies
1. **A Proton 11 arm64ec wcp that ships `winewayland.drv`** — built on branch
`The412Banner/proton-wine:feat/winewayland` (task #1). Nothing runs end-to-end
without it.
2. **Wayland runtime libs in the imagefs** so `winewayland.so` (unixlib) loads:
`libwayland-client.so`, `libwayland-egl.so`, `libxkbcommon.so`, `libxkbregistry.so`
(bionic aarch64). The wcp bundles them in its `lib/` as a fallback; the clean home
is the imagefs — add via `ImageFsInstaller` (new `installWaylandLibs()`), same
pattern as `installFFmpeg8()`.
## Integration plan (M4)
- **CMake**: add `waylandcomp` as a native lib (`libbannerwayland.so`) built with the
NDK, linking the bionic `libwayland-server`/`libvulkan`. Generate protocol glue from
`protocols/*.xml` at build time (host `wayland-scanner`).
- **Surface**: a `WaylandDisplayActivity` (parallel to `XServerDisplayActivity`) hosts a
`SurfaceView`; JNI hands the `ANativeWindow` to the compositor, which creates a Vulkan
swapchain on it and blits the imported game VkImage each frame (the last un-proven
render step; standard Vulkan once the window exists).
- **Input**: Android `MotionEvent`/`KeyEvent` → `wl_seat`/`wl_pointer`/`wl_keyboard`.
- **Launch wiring**: start the compositor, export `WAYLAND_DISPLAY`, select the wayland
driver per-prefix (registry `Drivers\Graphics = winewayland`) instead of `winex11`,
and point the container at the winewayland wcp.
## Status
- ✅ **Native lib foundation done + compile-verified.** Compositor + vk_import + pre-generated
protocol glue build as `libbannerwayland.so` (CMake target added), linking the vendored
bionic `libwayland-server`. Verified: compiles/links clean as an aarch64 bionic `.so`,
exports `banner_wayland_run`, NEEDED = libwayland-server + libvulkan. JNI entry
(`waylandcomp_jni.c`) + `WaylandCompositor.java` bring it up on a thread. Compositor-process
runtime deps (libwayland-server/libffi/libandroid-support) staged in `jniLibs/arm64-v8a`.
- ⏭️ **Next phase (gated on the winewayland wcp landing green):** `WaylandDisplayActivity`
(SurfaceView) + JNI `ANativeWindow`→Vulkan swapchain + blit the imported game VkImage to the
window (last un-proven render step) + input + launch wiring (start compositor, `WAYLAND_DISPLAY`,
per-prefix `Drivers\Graphics=winewayland`) + `ImageFsInstaller.installWaylandLibs()` (client/egl/xkb
into the imagefs for winewayland.so).
## Pointer lock / relative mouse (zwp_pointer_constraints_v1 + zwp_relative_pointer_manager_v1)
- Both globals are advertised (version 1); glue is pre-generated in `generated/` with
`wayland-scanner server-header` / `private-code` from `protocols/*-unstable-v1.xml` (same
scanner version as the rest, 1.24.0), no build-time scanner needed.
- winewayland's use: `ClipCursor`/fullscreen + hidden cursor → `lock_pointer` (persistent);
`SetCursorPos` → lock + `set_cursor_position_hint` + commit + unlock; visible cursor +
`ClipCursor(rect)` → `confine_pointer` with a one-rectangle region. It only turns relative
motion on for a window its `wl_pointer` has entered, so **pointer focus follows an active
constraint** (the desktop surface gets `leave`, the constrained surface `enter`) and goes
back to the desktop on the next motion after the constraint ends.
- Lock: the pointer is frozen; every input delta becomes `relative_motion` (no `motion`).
Confine: absolute motion is clamped to surface ∩ region. The position hint is applied on
the surface's commit (pointer moves there) and is where the pointer stays when the lock ends.
One constraint holds at a time (a newer request ends the older one); oneshot constraints
are defunct after ending, persistent ones re-take hold on focus re-entry.
- App side: input type 6 = relative delta (1/256 px); `banner_on_pointer_lock(locked, x, y)`
tells Java to switch its touch/mouse path to deltas (`XServer.setExternalRelativeMode`) and,
on unlock, to re-sync the X pointer (the absolute input's source) to x,y. With Wayland mode
`WinHandler.mouseEvent` routes relative-mode input to the compositor instead of the guest.
- Session log tag `pointer`: `lock requested by …`, `locked: … frozen at x,y`, `unlocked: … (why)`,
`confined: …`, `unconfined: …`, `relative pointer created for …`, `position hint: …`.
Test FIFO gained `rel DX DY`.
## Screen surface, swapchain recovery (vk_present.c)
- The app's UI thread never waits on the renderer: `vk_present_set_window()` only leaves the new
`ANativeWindow` (or NULL) in a request slot; the compositor thread applies it before its next
frame and on every vsync tick (`vkp_apply_window_request`), tearing the old swapchain down and
releasing the old window's reference. Acquire waits at most 1 s, never forever.
- OUT_OF_DATE / SURFACE_LOST on acquire or present rebuild the swapchain (once inline on acquire, so
the frame isn't lost). SUBOPTIMAL is presented as is: the swapchain uses IDENTITY preTransform on
purpose, so a rotated panel reports it on every frame. DEVICE_LOST logs
`GPU device lost … the compositor has stopped presenting` once and stops touching the device;
clients keep being paced (`pace_without_output`) so they don't wedge.
- A swapchain that can't be created is retried every 0.5 s and logged once per streak.
## Buffer lifetime, FPS limiter (compositor.c)
- `struct dmabuf_buffer` is reference counted: one ref for the wl_buffer resource, one per surface
showing it. Mesa destroys a swapchain's wl_buffers when the game rebuilds its swapchain, while the
last committed one is still on screen; the surface keeps the import (`s->dmabuf_buf`) until its
next commit, so the window neither blinks to black nor is unmapped/remapped ("closed"/"opened").
- Surface destroy releases its buffer immediately (`drop_dmabuf(s, 0)`), replaced buffers go back on
the limiter's cadence. The per-surface release schedule is bounded: a slot in the past is brought
to now, and the schedule never runs further ahead than `(releases still pending + 1)` intervals.
## Fullscreen mode + screen alignment
- `WaylandCompositor.nativeSetScaleMode(fullscreenMode, screenAlignment)` takes the app's
`Container.FULLSCREEN_OFF/FIT/STRETCH/FILL/INTEGER` (0..4) and `ALIGN_CENTER/TOP/BOTTOM` (0..2),
any thread, any time (the drawer changes it live). `update_map()` in vk_present.c is a line-for-line
mirror of `ViewTransformation.update()`: OFF and FIT both letterbox (OFF only differs in the app's
fullscreen gates), TOP/BOTTOM confine the picture to the top/bottom half of the output (the
handheld split), FILL overflow is clipped to the region. The app maps touch through the same class
(TouchpadView → X server → input sink → scene input), the overlay arrow through
`waylandSceneToView`, and the SurfaceView's own INPUT_SPACE touches through `vkp_output_to_scene`.
- Log tag `screen`: `<mode>, <alignment>: WxH scene shown WxH at x,y on the WxH output` whenever the
mapping changes (mode, alignment, scene or output size).
## Clipboard, text input, window icons (feat/wayland-clipboard-ime)
Three globals winewayland used to complain about at startup, each in its own file behind the
small interface in `src/banner_ext.h` (compositor.c only gained hook calls + accessors):
- **Clipboard** (`src/wl_clipboard.c`): `wl_data_device_manager` **v3** and
`zwlr_data_control_manager_v1` **v1**, selection only (start_drag is refused with `cancelled`).
Wine prefers data-control: its desktop process then owns the clipboard without needing keyboard
focus and the "clipboard functionality will be limited" ERR goes away too. One shared selection:
a program's source, or text from Android (`nativeSetClipboardText`), or nothing. Data-control
devices hear every change; `wl_data_device`s only while their client holds keyboard focus (hook in
`keyboard_focus()`), and on focus arrival. Guest → Android: a selection with a text mime is read
once over a pipe on the event loop (1 MiB cap, 3 s timeout) → `banner_on_clipboard_text` →
`WaylandClipboardSync` → `ClipboardManager`. Android → guest: `OnPrimaryClipChangedListener` plus a
re-read on resume / window focus (Android hides clipboard changes from background apps); `receive`
is served by a non-blocking writer. Echo guard on both sides. Log tag `clipboard`.
- **Text input** (`src/wl_text_input.c`): `zwp_text_input_manager_v3` **v1**, double-buffered state,
`done(serial)` only when we sent events. winewayland enables text input for whichever surface gets
`enter` and posts IME updates inside *that window's process*, so text input can't follow keyboard
focus (the desktop surface): it follows the last clicked program window (`g_ime_click`, set in
`pointer_input`), else the topmost non-shell window. `set_cursor_rectangle` arrives when an edit
control calls `ImmSetCompositionWindow`; the app (`WaylandTextInput`) shows the soft keyboard only
then (and with no hardware keyboard), hides it on disable unless the user's own toggle opened it.
Typed text: `SurfaceInputView`'s `InputConnection` → `commit_string` / `preedit_string` + `done`.
Deletions become Backspace/Delete key presses — winewayland's `delete_surrounding_text` handler is
empty. Log tag `text-input`.
- **Window icons** (`src/wl_toplevel_icon.c`): `xdg_toplevel_icon_manager_v1` **v1**, accepts and
drops icons (`done` at bind, no sizes).
- Host → compositor text crosses as UTF-8 `byte[]` through `banner_ext.c`'s queue (mutex + wake
pipe), drained on the compositor thread. Glue for the three protocols is pre-generated with
wayland-scanner 1.24.0 from `protocols/`.
## Compressed (UBWC) game buffers (feat/wayland-ubwc)
- `zwp_linux_dmabuf_v1` used to advertise `LINEAR` (+`INVALID`) only, so Turnip's Wayland WSI in the
game allocated linear swapchain images and DXVK/VKD3D resolved every frame from their tiled/UBWC
render targets into that linear copy, before the compositor's own blit. Now `bind_dmabuf` builds
its table at the first bind from the renderer's driver (`vkp_dmabuf_modifiers`):
`vkGetPhysicalDeviceFormatProperties2` + `VkDrmFormatModifierPropertiesListEXT` per fourcc
(AR24/XR24/AB24/XB24), each modifier confirmed with `vkGetPhysicalDeviceImageFormatProperties2`
for a dma-buf-backed `TRANSFER_SRC` image. Only `DRM_FORMAT_MOD_LINEAR` (0) and
`DRM_FORMAT_MOD_QCOM_COMPRESSED` (`0x0500000000000001`, UBWC, single memory plane on Adreno) are
ever advertised; anything else the driver reports goes to logcat as "not advertised".
- Mesa's WSI hands every advertised+supported modifier to `vkCreateImage` as a modifier *list* and
Turnip picks `QCOM_COMPRESSED` whenever it is in the list (`tu_image.cc`), so with the
advertisement the game's swapchain is UBWC. The `wl_buffer` then arrives with that modifier and
the pixel-plane pitch/offset from the game's `vkGetImageSubresourceLayout(MEMORY_PLANE_0)`;
`vkp_image_import_dmabuf` creates the compositor's image with the explicit modifier and that one
plane layout (`VkImageDrmFormatModifierExplicitCreateInfoEXT`), which Turnip validates
(`INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT` if the pitch does not fit its `fdl6` alignment). The
swapchain blit (`vkp_render`) and the layer-mode blit (`vkp_blit_image`) read it like any other
source; nothing CPU-side ever touches a dma-buf. Layer mode still blits into its own gralloc pool.
- **A/B switch:** `BANNER_WAYLAND_UBWC=0` (or `false`/`off`) in the container's/shortcut's
environment variables → `nativeSetUbwc(false)` → linear-only advertisement, exactly the old
behaviour. Default on. The driver is still queried, so the log shows what it could have done.
- Log tag `dmabuf`: `formats: AR24 linear+qcom_compressed, XR24 …, AB24 …, XB24 …` at the first
bind (with ` (BANNER_WAYLAND_UBWC=0: qcom_compressed not advertised)` when off, or a second line
`the compositor's driver (…) reports no importable qcom_compressed layout: game swapchains stay
linear` when the adrenotools Turnip lacks it). Per window, the existing `vulkan` line now names
the modifier: `… is presenting GPU frames through Wayland: 1920x1080, format XB24, qcom_compressed
(zero-copy)` vs `… linear (zero-copy)`. Import failures are `error` lines naming the stage,
modifier, size, pitch and VkResult (`dmabuf: vkCreateImage(qcom_compressed, …) -> …`).
- Caveat: both sides must agree on the UBWC encoding for the GPU. The game runs the wcp's Wayland
Turnip, the compositor the user's adrenotools Turnip; the layout code (`fdl6`) is the same, but a
very different Mesa version on one side is the first suspect if a compressed frame imports fine
yet looks scrambled — `=0` is the workaround, and the `gpu` line names both the GPU and the
compositor's driver.
## Phones with no DRM node (OpenGL) — `BANNER_WAYLAND_NO_RENDER_NODE=1`
- The feedback's `main_device` is the first of `/dev/dri/renderD128`, `renderD129`, `card0` the app
can `stat`; retail phones (Adreno 830/840 reports, 2026-09-14) expose none, so it is `0:0`.
Vulkan games do not care. Mesa's EGL did: its Wayland DRM initialiser failed the display in
`dri2_setup_device()` without a render node and fell back to a software path that has no
rasteriser in these layers — native OpenGL black, sound playing. Wayland layer versionCode 9
(Banners-Turnip `patches/wayland/egl_wayland_no_drm_node.py`) runs zink + kopper on the Vulkan
device without a node instead; nothing app-side is needed for that.
- `BANNER_WAYLAND_NO_RENDER_NODE=1` (container or shortcut env) makes the compositor advertise
`main device 0:0` on a device that does have a node, to reproduce those phones here.
- Log lines: `dmabuf` `… no display (DRM) device …: OpenGL games need Wayland layer versionCode 9
or newer …` when the main device is 0:0; `opengl` `<program> asked for GPU buffers but has drawn
only software (shared-memory) frames …` once per program that asked for dma-buf feedback (EGL's
GPU path always does) and then committed 150 wl_shm frames without making a dma-buf buffer.
## Zero-copy window layers (spike, `BANNER_WAYLAND_ZERO_COPY=1`)
Research + host-side prototype in `ZERO_COPY_SPIKE.md`: why a dma-buf can't become an
`AHardwareBuffer` (the game's buffers are DMA-heap allocations, not gralloc's), why the interop
must start from a gralloc buffer whose native-handle fd the drivers import (Turnip's own AHB path),
and the WSI patch this needs in our Wayland Turnip. `src/sc_layer.c` implements the receive side:
with the variable in the container's environment, one fullscreen game window is shown on its own
`ASurfaceControl` child of the SurfaceView (pool of three compositor-allocated AHBs, one blit,
release fences from `setOnComplete`, geometry from the fullscreen mode); any other scene falls back
to the swapchain blit. Log tag `layer`. Off by default.
## Zero-copy game frames (`BANNER_WAYLAND_ZERO_COPY=1`, drawer switch live)
Option (a') of `ZERO_COPY_SPIKE.md`, both halves:
- **Guest (our Wayland Turnip, banners-turnip-wayland `patches/wayland/banner_ahb_wsi.py`, all
variants):** while the compositor's mode is on (see the live switch below), the Wayland
WSI allocates every swapchain image as a gralloc `AHardwareBuffer` (usage GPU_SAMPLED_IMAGE |
GPU_FRAMEBUFFER | COMPOSER_OVERLAY; UBWC when gralloc picks it and a test `vkCreateImage` with the
explicit `QCOM_COMPRESSED` layout succeeds, else linear via a CPU usage bit; `BANNER_WSI_AHB_LINEAR=1`
forces linear), imports the handle's dma-buf with an explicit modifier + pitch (the compositor's own
pool-import path; `vk_android.c` is not in a platforms=wayland build), shares it through
`zwp_linux_dmabuf_v1` as before (blit fallback), and sends the `AHardwareBuffer` once per image:
`socketpair` + `AHardwareBuffer_sendHandleToUnixSocket` + `banner_ahb_v1.attach(wl_buffer, fd, w, h,
stride, modifier hi/lo, image_count)`. Surface formats are limited to `R8G8B8A8_UNORM/SRGB` (and
`A2B10G10R10`) while the mode is on: gralloc has no BGRA (DXVK/Zink blit into the swapchain image
anyway). Marker: `strings libvulkan_freedreno_wayland*.so | grep banner_ahb_v1`.
- **Compositor (`src/ahb_swapchain.c`, protocol `protocols/banner-ahb-v1.xml`):** the global is
advertised on every session where a display layer is possible at all (`sc_layer_available()`);
whether gralloc buffers are WANTED right now is the `mode` event, not the global's presence.
`attach` → `AHardwareBuffer_recvHandleFromUnixSocket`, record on the `dmabuf_buffer`.
When that buffer is the one fullscreen frame (`layer_candidate`, or the layer-only candidate when
this renderer could not import it), `sc_layer_present_ahb` sets it on the `banner_wayland_game`
SurfaceControl as is. **Acquire fence** = `DMA_BUF_IOCTL_EXPORT_SYNC_FILE(READ)` of the dma-buf: Mesa
imports the render fence into the dma-buf (`wsi_signal_dma_buf_from_semaphore`) *before* the
`wl_surface.commit`, so the export always carries the frame; if the ioctl fails the compositor
polls the dma-buf instead (logged once). **Release**: `setOnComplete` →
`getPreviousReleaseFenceFd` → `DMA_BUF_IOCTL_IMPORT_SYNC_FILE(READ)` into the dma-buf → then
`wl_buffer.release` (still on the FPS limiter's cadence); Mesa's acquire
(`wsi_create_sync_for_dma_buf_wait`) exports every fence of the dma-buf, so the game waits for the
display. If the import fails the release waits on the fence fd in the event loop instead. A hidden
or retired layer gets a 16x16 blank buffer so SurfaceFlinger actually lets go of the game's buffer.
- **App:** `BANNER_WAYLAND_ZERO_COPY=1` (container/shortcut env) is the session's STARTING state and
also exports `BANNER_WSI_AHB=1` into the guest (`XServerDisplayActivity.isWaylandZeroCopyRequested`),
which is what an older driver (bound at `banner_ahb_v1` version 1) needs.
### The live switch ("Zero-copy presentation", Graphics tab)
`banner_ahb_v1` version 2 adds one event, `mode(enabled)`, sent to every bound client on bind and
again on every flip. The compositor owns the state:
- `WaylandCompositor.nativeSetZeroCopy(on)` is live and callable from any thread. Before the
compositor starts it just seeds `g_zero_copy`; afterwards it posts through the `banner_ext.c` host
queue (like the clipboard/IME setters) so `ahb_swapchain_set_mode()` runs on the compositor thread,
which flips `g_zero_copy`, broadcasts `mode`, flushes the clients and redraws.
- **Guest:** the WSI follows `display->banner_ahb_mode`, decided per swapchain at creation. When a
live swapchain was built for the other mode, the next `vkAcquireNextImageKHR` /
`vkQueuePresentKHR` returns `VK_ERROR_OUT_OF_DATE_KHR` (it reuses Mesa's own `chain->retired`
early-outs), which DXVK, vkd3d-proton and Zink all answer by rebuilding the swapchain. The mode
event is read with a non-blocking dispatch of the display queue on each acquire/present, because
in MAILBOX that queue is otherwise only dispatched when the acquire loop runs out of images.
- **Compatibility, both directions.** The registry bind clamps to `MIN(advertised, 2)` — binding
above the advertised version is a fatal `wl_display` error, and a Bannerlator from before the live
switch advertises version 1. On version 1, and on version 2 before the first `mode` event arrives,
the driver keeps the original contract exactly: gralloc images iff `BANNER_WSI_AHB=1`, decided per
swapchain, and no swapchain is ever retired. Once the mode is in play `BANNER_WSI_AHB=1` no longer
forces it on (the app exports that on every zero-copy launch, which would freeze the switch);
`BANNER_WSI_AHB=0` still forces the whole feature off.
- **No black frame, either way.** Switching OFF does not tear down anything: the old gralloc chain's
frames keep being shown — on the layer when this renderer could not import them, through the copy
path when it could — until the game has rebuilt, and only then does `sc_layer_hide()` run. Buffers
already on the layer keep their deferred release (`ahb_swapchain_defer_release` is no longer gated
on `g_zero_copy`, or the display would be handed a buffer it is still scanning out). Switching ON,
the layer path resumes as soon as the first `attach` of the new chain lands.
- **Drawer:** the toggle applies live *and* writes the env as the next launch's default. Its status
line follows the compositor, not the switch: `nativeZeroCopyLastFrameAgeMs()` (updated on every
zero-copy present, unlike the 10 s counter) is what makes it say "Switching on…" / "Switching off…"
only for as long as it really is.
- **Log lines (tag `layer`):** `zero-copy: banner_ahb_v1 version 2 advertised …`, `zero-copy: <exe>
bound banner_ahb_v1 version N …`, `zero-copy: on|off at launch …`, `zero-copy switched on|off from
the drawer: N bound programs told to rebuild their swapchains …`, `zero-copy: AHB swapchain from <exe> (N images, WxH, UBWC (QCOM_COMPRESSED)|linear,
stride S px)`, `zero-copy: presenting "<title>" (<exe>) without a copy`; the 10 s `stats` line ends
with `| N zero-copy frames`. Guest side (Mesa log, stderr of the game): `banner-ahb: WxH swapchain
(N images) on gralloc buffers: UBWC|linear, stride S px` or the reason it stayed on standard
buffers, plus `banner-ahb: compositor wants gralloc|standard swapchain images` and `banner-ahb:
zero-copy switched on|off: retiring the WxH swapchain …` on a live flip.
## Multi-layer presentation (feat/wayland-multilayer, `src/sc_layer.c`)
Layer mode hands the scene to SurfaceFlinger as a small, deliberately ordered **set** of Android
display layers instead of one. The whole model, top to bottom:
```
app window ....... Compose UI, the in-game drawer, the perf HUD, the on-screen controls, the
pointer arrow - ordinary Android views in the activity's window, ALWAYS on top
+-- SurfaceView .. the compositor's Vulkan swapchain (black while layer mode is up)
+-- z=1 "banner_wayland_game" ...... the one fullscreen game window
+-- z=2 "banner_wayland_overlay" ... at most ONE window drawn above the game
```
- **Two layers is the hard cap** (`SC_LAYER_COUNT`). HWC composes only a few layers before
SurfaceFlinger falls back to GPU client composition, which would throw the whole benefit away, so
the compositor never asks for a third: a scene with two or more windows above the game goes back
to the copy path. The count is said out loud the first time the overlay goes up (`layer`: `2
display layers in use: "banner_wayland_game" (z=1) and "banner_wayland_overlay" (z=2) above it …`).
- **Who owns what.** Each layer has its own `ASurfaceControl`, its own gralloc buffer pool (3
buffers each), its own geometry and its own lifetime; `struct layer`
in `sc_layer.c` holds all of it and the z-order is fixed by the id (nothing is re-ordered at
runtime). The SurfaceControls are children of the SurfaceView's surface, created on first use and
retired together when the output window changes or goes away.
- **What can be on the game layer**, cheapest first: the game's own gralloc buffer (zero-copy, no
copy anywhere — `ahb_swapchain.c`); one blit of the game's frame into a compositor buffer
(`sc_layer_present`); or, with screen effects on, the compositor pass's **result** blitted into
such a buffer (`sc_layer_present_pass` → `vkp_pass_begin` / `vkp_pass_copy_to`). The last one is
what keeps a Look from dropping the session back to the app's swapchain: the scene → output
mapping (Fullscreen Mode / Alignment, and the scaling a mode like FSR asks for) is then done by
the display through `setGeometry` instead of by a second full-screen GPU blit.
- **Present order matters, and it was measured.** The layers go up first; the base surface's black
frame is presented *after* the layer transaction, and the effects chain runs in its own submit
with **no swapchain image acquired**. A present holds an acquired image and the acquire semaphore
is a vblank gate: folding the chain into that submit (one command buffer, one present — the
shape that looks cheaper) put the whole 13-pass chain behind a vblank and measured **72 fps**
against **111 fps** for the split shape, on HL2 + Retro CRT at 1920x1080 on the Pocket FIT (the
copy path is 129 fps on the same scene). Anyone tempted to "optimise" this into one submit should
read this paragraph first.
- **What goes on the overlay layer**: exactly one draw above the game — a second Wayland toplevel
(a launcher or settings window, a message box), copied 1:1 into the overlay pool and then
*cropped and placed* by the display through the same `vkp_map_draw` mapping the copy path uses.
The point is what does **not** happen: the game's frames are no longer copied through the
compositor's swapchain just because something small sits on top of them.
- **Transparency.** The compositor composes with blits, which overwrite — it never alpha-blends,
on either path — so every layer is marked `ASC_TRANSPARENCY_OPAQUE` and the overlay layer is
cropped to the window it carries. There is no blending to get wrong: no black box over the game,
no double-darkening, and the picture is the copy path's, pixel for pixel.
- **Input is untouched.** An `ASurfaceControl` has no input channel, so neither layer can take a
touch: everything still reaches the app's `SurfaceView` (and the views above it) exactly as
before, and the scene mapping touch goes through (`vkp_output_to_scene` / `ViewTransformation`)
is the same one the layers are placed with, so the cursor still lands where it is pointing. The
SurfaceView is not Z-on-top, so its whole subtree — both layers included — stays under the
activity's own window: the drawer, the HUD and every Compose dialog draw above them.
- **What cannot be layered, and why.**
- *Frame generation*: each generated frame needs a present of its own on consecutive vblanks, and
a display layer latches one buffer per refresh — pacing that is the swapchain's job. While an
engine is armed the whole scene takes the copy path (unchanged).
- *Effects with a window above the game*: the chain has to see the whole scene to look the way it
does on the copy path (and on X11), and the game layer only carries the game. Copy path.
- *Two or more draws above the game* (several windows, or a window with subsurfaces): a third
layer is not worth the client-composition risk. Copy path.
- **Lifecycle.** Hiding is top-down so nothing is uncovered for a frame; a hidden or retired layer
gets the shared 16x16 blank buffer so SurfaceFlinger really lets go of the game's buffer (and
reports a release fence for it) instead of holding it while invisible. `sc_layer_hide()` takes
every layer down (scene no longer a fullscreen game, zero-copy switched off live, session end),
`sc_layer_hide_overlay()` is used when only the window above the game closed, and
`sc_layer_window_gone()` retires both SurfaceControls and drains both pools on a surface loss /
HOME / resume. **The overlay layer is always RETIRED, never merely hidden** (both paths) — see
the HWC note below: a live second SurfaceControl keeps SurfaceFlinger composing on the GPU even
when it is invisible. The game layer keeps its SurfaceControl through a hide, because it goes up
and down with every effects / frame-generation toggle.
- **The display frame-rate vote belongs to one layer.** The refresh-rate stream's
`sc_layer_set_frame_rate()` (the game's cadence, the same value the app votes on its own surface)
is carried **only** by the layer the game presents on — the game layer. The overlay layer is
explicitly voted `0`, so a window that redraws once a second can never hold the panel at the
game's cadence, nor drag the game's cadence down to its own. Each layer remembers what its live
SurfaceControl carries (`fps_applied`) and re-applies on the next transaction, and a retired
SurfaceControl resets it so a re-created layer is re-voted from scratch. Log: `display frame-rate
vote on banner_wayland_game: 60.00 Hz`.
- **Stats.** The 10 s `stats` line now ends with `| N zero-copy frames` (the game's own buffers) and
`| N layer frames` (frames the compositor put on a layer through one of its own buffers — the
plain layer blit or the effects result). Both are hardware-composed; only the first is copy-free.
- **Measured on the Pocket FIT (Adreno 750, portrait panel, landscape session → every layer is
ROT_90 + scaled), `dumpsys android.hardware.graphics.composer3.IComposer/default`:** one layer is
`composition: DEVICE/DEVICE` (game's own gralloc buffer scanned out by the DPU), with effects on
the layer it stays `DEVICE/DEVICE`, but **two** layers flip the whole frame to `DEVICE/CLIENT` —
SurfaceFlinger composes it on the GPU — **and it does not come back when the overlay goes away**:
retiring the overlay's SurfaceControl (which the compositor now does rather than merely hiding
it) leaves the frame in client composition; only re-creating the *game* layer's SurfaceControl
clears it (HOME + resume does, reproduced twice). Retiring is still right — one fewer live layer,
and it leaves the HWC list — it is just not the whole cure; the open follow-up is to retire and
immediately re-create the game layer when the overlay goes, which would cost one black frame
unless the new SurfaceControl is shown before the old one is dropped. The likely mechanism is the
DPU's rotator budget (one rotated+scaled layer), not the layer count as such, so a device or
orientation that needs no rotation may well take both on the DPU. Even in client composition the
overlay layer is not a loss (SurfaceFlinger does the one blit the compositor would have done), but
the hardware-composition win is only real for the single-layer cases. Note `VRI[ScreenDecorHwcOverlay]`
is always `DISPLAY_DECORATION/CLIENT` (the system's rounded corners) — that is why SurfaceFlinger's
`clientCompositionFrames` counter reads 100 % on this device in every state and is useless here.
- **Not done yet:** a second toplevel that is itself rendering into gralloc buffers could go on the
overlay layer zero-copy too (the token machinery in `ahb_swapchain.c` is already per-buffer, not
per-layer); today the overlay always costs one small blit.
## Screen effects (feat/wayland-effects, `src/effects_chain.c`)
The X11 renderer's screen-effect chain, run by the compositor between the composited scene and the
output. Same SPIR-V as the X11 Vulkan renderer (`winlator/*_frag.h`, made by `winlator/gen_shaders.sh`
and committed — the NDK build compiles no GLSL, so there is no build-time shader tooling), same
push-constant layouts, uniform ranges and pass order, so a saved preset looks the same on both backends.
**Present-pass hook order (`vk_present.c`, `vkp_render`):**
```
scene (every draw blitted 1:1 into a scene-sized R8G8B8A8 image; compositor pass only)
-> vkp_effects_run() effects_chain.c: scaling -> effects (below)
-> vkp_framegen_run() framegen_bridge.c: frame generation on the chain's result (next section)
-> update_map / blit Fullscreen Mode + Screen Alignment map the result (and each generated
frame) onto its own swapchain image
-> present generated frames first, the real frame last, FIFO
```
The compositor pass runs when either stage needs it (`vkp_effects_active() || vkp_framegen_active()`).
Effects operate on the scene; the mapping operates on the output — the chain's result stands for
the whole scene and goes through the same `draw_to_blit` as a plain frame. With everything off the
frame takes the old path unchanged (draws blitted straight through the mapping); only the blit filter
follows the scaling mode (Nearest = `VK_FILTER_NEAREST`).
**Pass order (locked to the X11 Vulkan renderer, `VulkanRendererContext::recordUpscalePasses`):**
scaling → FXAA → Toon → Colour (brightness/contrast/gamma/saturation) → CAS → HDR → NTSC → CRT →
Debanding (terminal dither). Scaling modes: 0 None / 1 Linear / 2 Nearest = a filtered resize;
3 SGSR, 8 SGSR HQ, 7 NIS = one pass; 4 FSR / 5 FSR Fit = EASU → RCAS (fill vs fit is the Fullscreen
Mode's business here, both run the same pair); 6 Sharpen = RCAS 1:1 (nearest scale + sharpen, as X11).
Render scale is "Not used on Wayland": the scaling modes resize the scene to its **mapped output size**
(3/4/5/7/8 only when that is larger than the scene, the X11 "render below display" gate; 6 always).
Every later pass runs at that resolution, ping-ponging between two targets (+ one EASU mid target).
**Settings** (`WaylandCompositor.nativeSetUpscaler / nativeSetUpscaleSharpness / nativeSetCas /
nativeSetHdr / nativeSetDeband / nativeSetScreenEffects / nativeSetLookName`, 1:1 with
`VulkanRenderer`'s): any thread; the compositor thread snapshots them per frame, redraws even a
static scene, and writes one `effects` line per change:
`effects scaling=SGSR HQ 75%, CAS on 55%, Look="Game Clarity", colour b=+2 c=+12 g=1.00 s=108%`
(`effects all off: scaling=Linear (plain blit)` when nothing is on). The chain's Vulkan objects are
built on the first active frame (`effects chain ready: 13 passes … on <GPU>`; a driver that refuses
them logs an `error` and effects stay off for the session). The app wires the drawer's Vulkan
post-chain block (`XServerDialogState.on*Apply`) to these in `XServerDisplayActivity.initWaylandEffects`
and remembers the same per-game keys as the Vulkan path (#382).
**Zero-copy interplay:** the chain's result no longer has to land in the app's swapchain — with a
fullscreen game alone on screen it is copied into the game layer's own gralloc buffer and the game
**stays on its display layer** while a Look is applied (see "Multi-layer presentation" below). It
still takes the copy path when a window is drawn above the game (the chain has to see the whole
scene to look as it does on X11) — `effects zero-copy paused: a window above the game needs the
compositor pass for the whole scene` — and always while frame generation is armed
(`framegen zero-copy paused: frame generation needs the compositor pass`); the layer comes back with
`effects zero-copy resumed: the game is back on its own display layer`. A frame this renderer could
not import (layer-only candidate) still goes on the layer, effects/frame generation skipped, said once.
## Frame generation (feat/wayland-framegen, `src/framegen_bridge.c`)
The two **native** engines the X11 renderer hosts inside `libwinlator`'s compositor run inside the
Wayland compositor's own Turnip device: **LSFG Native** (the user's Lossless.dll chain,
`winlator/lsfg`) and **Win-FG Native** (our FSR3-derived optical-flow chain, `winlator/winfg`).
The engine sources are compiled into `libbannerwayland.so` unmodified (CMake, app build only; the
standalone build gets `framegen_engine_stub.c`) and driven by `src/framegen_engine.cpp`, which fills
their `VkTable` by name from the compositor's `vkGetInstanceProcAddr`/`vkGetDeviceProcAddr`.
**bionic-fg** (the guest-side win-fg Vulkan layer) is X11-only: its frames are born inside the guest
and have nothing to attach to here, so on Wayland the "bionic" engine is always Win-FG Native.
- **Where it runs** (the hook order above): frame generation takes the effects chain's RESULT
(`vkp_effects_run`'s output, at its size - the scene, or the mapped output size under a scaling
mode - moved to GENERAL), so generated frames carry the same look as real ones. The ONE hook
`vkp_framegen_run(cmd, image, view, w, h, fmt, gens[])` records the engine's chain into the same
command buffer and returns 0..3 generated images. Each image is then blitted through the scene ->
output mapping into its own acquired swapchain image and presented: **generated frames first, the
real frame last** (interpolation produces frames between N-1 and N; that one output interval of
latency is inherent). One acquire/render-done semaphore pair and command buffer per present
(`MAX_PRESENTS` = 4), the frame fence on the last submit. With the FIFO swapchain the presentation
engine shows them on consecutive vblanks: that is the pacing, there are no sleeps and no host pacer.
The swapchain is rebuilt with `multiplier - 1` extra images while armed so every present of a frame
queues without a vblank wait. Frame generation off and effects off = the direct path, byte-for-byte
as before.
- **Device**: `dev_init` asks the bridge for the LSFG feature chain (`VkPhysicalDeviceVulkan12Features`
vulkanMemoryModel + `shaderStorageImageWriteWithoutFormat/ExtendedFormats`, the same chain
`VulkanRendererContext::createLogicalDevice` uses; instance apiVersion is 1.3 for the probe) and
retries device creation without it if the driver refuses. Win-FG needs only a storage-capable ring
format (RGBA8). The generation ring (`STORAGE|SAMPLED|TRANSFER_SRC|DST`, GENERAL) and the engines'
own history live for the session; arming/disarming is a flag, a multiplier change resizes the ring.
- **FPS limiter / vsync**: untouched. The limiter still paces the game's buffer returns (real frames);
`applyNativeFgLocks` forces it on while multiplying, exactly as on X11 (`pacedLimitWithSlack` still
applies through `nativeSetFpsLimit`). Generated frames never touch the limiter, the frame callbacks
or the `wp_presentation` feedback - the guest sees one present per real frame.
- **Zero-copy**: the layer bypasses the compositor pass, so while frame generation is armed the
fullscreen window takes the copy path (`render_scene`: `framegen zero-copy paused: frame generation
needs the compositor pass`) and the layer resumes when both it and the effects are off (see the
effects section's zero-copy interplay).
- **App side**: `WaylandCompositor.nativeSetFrameGenEngine/Armed/Tuning`, `nativeSetLsfgCachePath`,
`nativeSetWinFgTuning`, `nativeFrameGenProblem/CapsReason/Stats` (same codes and six-float shape
as `VulkanRenderer`). `XServerDisplayActivity.applyWaylandFrameGen` is the single apply:
`applyLsfgNative`/`applyWinFgNative` route to it in Wayland mode (launch and drawer), the drawer's
`onBionicFgConfigChange` has a Wayland branch, `nativeFgProblemReason`/`startLsfgStatsReadout` read
the bridge. Every native setter is a value store, so the launch code may arm before the compositor
thread exists.
- **Log tag `framegen`**: `engines ready on the compositor's device: LSFG Native available (supported),
Win-FG Native available`, `<engine> engine ready (...)`, `<engine> x2 armed (flow scale 0.80, panel
120 Hz): ...`, `generating: <engine> x2 at 1920x1080 (1 interpolated frame per game frame)`, the
fallbacks (`... can't start: no shader cache`, `... failed to start on the compositor's driver`,
`... could not build its chain at WxH`, `the driver refused the LSFG feature set`), `frame generation
off (N frames generated this session)`, and the zero-copy pause/resume above. The 10 s `stats` line
counts generated frames in `frames on screen` and ends with `| N generated frames`; `GPU frames from
games` never includes them.
+30 -30
View File
@@ -1,4 +1,4 @@
# Zero-copy window layers on Wayland — research spike + host-side prototype
# Zero-copy window layers on Wayland - research spike + host-side prototype
Branch `spike/wayland-zero-copy-layers` (on `feat/wayland-phase2`). Written 2026-09-13.
@@ -14,21 +14,21 @@ adrenotools Turnip.
**Answer in one paragraph.** A raw dma-buf can never become an `AHardwareBuffer` (option b is
closed: no public API, and the game's buffers are `/dev/dma_heap/system` allocations, not
gralloc buffers). The interop must go the other way: the buffer is a **gralloc `AHardwareBuffer`
first**, and its dma-buf fd (`native_handle->data[0]`) is what the Vulkan drivers import — which is
first**, and its dma-buf fd (`native_handle->data[0]`) is what the Vulkan drivers import - which is
exactly how Turnip already imports every gralloc buffer on Android (`vk_android.c`). Both Turnips
in play already have every extension needed; nothing is missing in the drivers. What is missing is
a **WSI hook**: Mesa's Wayland WSI always allocates its own dma-bufs and has no way to receive or
allocate compositor-visible buffers, so **option (a) or (c) each need a ~300-line patch to
`wsi_common_wayland.c` in our own Wayland Turnip build** (banners-turnip-wayland
`build_turnip_wayland_wsi.sh`, Mesa `7cda7850`) plus a small private Wayland protocol. Nothing in
the app can remove the copy on its own. Recommended: **option (a′) — the game's WSI allocates its
the app can remove the copy on its own. Recommended: **option (a′) - the game's WSI allocates its
swapchain images as `AHardwareBuffer`s (gralloc-native, UBWC), imports them through the AHB path
Turnip already has, and hands the compositor the AHB over a Unix socket (`AHardwareBuffer_send/
recvHandleFromUnixSocket`, the same mechanism the X11 renderer already uses for `GPUImage`) plus
the dma-buf fd through `zwp_linux_dmabuf_v1` as today (so the blit path stays as the fallback).**
The host half — one `ASurfaceControl` layer per fullscreen window, geometry from the fullscreen
The host half - one `ASurfaceControl` layer per fullscreen window, geometry from the fullscreen
mode, release fences from `ASurfaceTransaction_setOnComplete`, black base surface, HUD/pointer as
Android views above — is implemented in this branch as a gated prototype (`sc_layer.c`,
Android views above - is implemented in this branch as a gated prototype (`sc_layer.c`,
`BANNER_WAYLAND_ZERO_COPY=1`) with a compositor-owned AHB pool and one blit, so the
SurfaceFlinger side can be measured on the device before the driver work starts.
@@ -47,7 +47,7 @@ Facts the options rest on (Mesa `7cda7850`, the commit both Turnips are built fr
- **Dma-buf import works on both Turnips**: `VK_KHR_external_memory_fd`,
`VK_EXT_external_memory_dma_buf`, `VK_EXT_image_drm_format_modifier` are unconditional
(`tu_device.cc:253, 354, 369`); the compositor proves it every frame. Only
`VK_EXT_physical_device_drm` is off on KGSL (`:385`) — relevant to dmabuf-v4 feedback (§2).
`VK_EXT_physical_device_drm` is off on KGSL (`:385`) - relevant to dmabuf-v4 feedback (§2).
- **AHB import in Turnip is dma-buf import.** `VK_ANDROID_external_memory_android_hardware_buffer`
is exposed whenever a u_gralloc backend exists (`tu_device.cc:219, 428`), and the fallback backend
always exists. `vkGetAndroidHardwareBufferPropertiesANDROID` does
@@ -59,7 +59,7 @@ Facts the options rest on (Mesa `7cda7850`, the commit both Turnips are built fr
int) the UBWC flag `0x08000000` in the next int selects `DRM_FORMAT_MOD_QCOM_COMPRESSED`, else
`DRM_FORMAT_MOD_LINEAR`; pitch = `pixel_stride × bpp`, offset 0. The qcom backend
(`u_gralloc_qcom.c`, `hw_get_module`) cannot load the vendor gralloc from an app process, so the
fallback is the one in use — the local KGSL build imports exactly `AHardwareBuffer_allocate /
fallback is the one in use - the local KGSL build imports exactly `AHardwareBuffer_allocate /
describe / getNativeHandle / acquire / release / isSupported` and `hw_get_module`, NEEDED
`libnativewindow.so` + `libhardware.so` (readelf on `turnip608/stg/libvulkan_freedreno.so`).
So **the Wine process already loads `libnativewindow.so`** (as a dependency of the ICD), and
@@ -69,7 +69,7 @@ Facts the options rest on (Mesa `7cda7850`, the commit both Turnips are built fr
### (a) Game side: `AHardwareBuffer_allocate` in the guest, exported as a dma-buf
Feasible with the current drivers, **but only through a WSI patch** — the app cannot do it.
Feasible with the current drivers, **but only through a WSI patch** - the app cannot do it.
Mesa's Wayland WSI creates every native swapchain image with `wsi_create_native_image_mem`
(`wsi_common_drm.c:760-850`): `vkAllocateMemory` with `VkExportMemoryAllocateInfo{DMA_BUF}` →
`vkGetMemoryFdKHR` → `zwp_linux_buffer_params_v1.add(fd, …)` + `create_immed`
@@ -80,7 +80,7 @@ The patch (in our Wayland Turnip build): a new `create_mem` for the Wayland chai
compositor advertises a private global `banner_layer_buffer_v1`:
1. `AHardwareBuffer_allocate(w, h, R8G8B8A8, GPU_COLOR_OUTPUT | GPU_SAMPLED_IMAGE)`;
`vkGetAndroidHardwareBufferPropertiesANDROID` → `vkAllocateMemory` with
`VkImportAndroidHardwareBufferInfoANDROID` (Turnip: `tu_device.cc:3828`), bind — the image was
`VkImportAndroidHardwareBufferInfoANDROID` (Turnip: `tu_device.cc:3828`), bind - the image was
created with the modifier/pitch gralloc reports, i.e. UBWC on Adreno gralloc (§2).
2. `wsi_image.dma_buf_fd = dup(native_handle->data[0])`, `drm_modifier` / `row_pitches[0]` from
the same layout → the existing `zwp_linux_dmabuf_v1` path stays untouched: the compositor keeps
@@ -88,17 +88,17 @@ compositor advertises a private global `banner_layer_buffer_v1`:
3. `banner_layer_buffer_v1.attach(wl_buffer, socket_fd)`: the WSI creates a `socketpair`, sends one
end in the request, and calls `AHardwareBuffer_sendHandleToUnixSocket(ahb, other_end)`; the
compositor's request handler does `AHardwareBuffer_recvHandleFromUnixSocket` and stores the AHB
on `struct dmabuf_buffer`. Per swapchain image, once — not per frame.
on `struct dmabuf_buffer`. Per swapchain image, once - not per frame.
Extensions used by the game: `VK_ANDROID_external_memory_android_hardware_buffer` only (the
dma-buf/modifier extensions are used internally by the same driver). Note winevulkan does not
expose the AHB extension to PE code — irrelevant here, the WSI is unix-side inside the ICD.
expose the AHB extension to PE code - irrelevant here, the WSI is unix-side inside the ICD.
Effort: 2–3 days WSI + protocol + wcp rebuild (wine-compat-engineer for the packaging).
### (b) Import the game's existing dma-buf as an `AHardwareBuffer`
**No.** There is no public API: `AHardwareBuffer_createFromHandle` exists only in
`vndk/hardware_buffer.h` (exported by `libnativewindow.so`, so `dlsym` finds it), but it takes a
gralloc `native_handle_t` that the vendor mapper (`IMapper::importBuffer`) validates — on QTI a
gralloc `native_handle_t` that the vendor mapper (`IMapper::importBuffer`) validates - on QTI a
`private_handle_t` with the `'gmsm'` magic, a metadata dma-buf, and vendor-specific ints. The game's
buffers are bare DMA-heap fds. Fabricating a vendor handle around a foreign fd is unversioned
private ABI (differs across gralloc4 releases/OEMs) and is exactly the kind of thing that
@@ -115,8 +115,8 @@ before the swapchain image is created, plus reallocation on `vkCreateSwapchainKH
plus fence plumbing in both directions. `zwp_linux_dmabuf_v1` v4 feedback cannot express this (it
only steers format/modifier/device; the client still allocates), and on KGSL v4 is a trap anyway:
the driver has no DRM `dev_t`, so `same_gpu` stays false (`wsi_common_wayland.c:1660-1686`) and
Mesa takes the prime-blit path (one extra copy per frame). The one advantage over (a) — the
compositor decides linear vs UBWC — is moot because gralloc's decision is the right one for the DPU.
Mesa takes the prime-blit path (one extra copy per frame). The one advantage over (a) - the
compositor decides linear vs UBWC - is moot because gralloc's decision is the right one for the DPU.
Effort: 3–4 days; strictly more moving parts than (a). Not recommended.
**Recommendation: (a).** It reuses two proven mechanisms (Turnip's AHB import, the X11 path's
@@ -135,7 +135,7 @@ to our own Mesa build plus one small protocol.
- The compositor advertises `LINEAR` and `INVALID` only (`compositor.c: bind_dmabuf`,
`uint64_t mods[] = {MOD_LINEAR, MOD_INVALID}`); `INVALID` is dropped by the WSI
(`wsi_common_wayland.c:449-451`), so every game swapchain is linear today. DXVK/VKD3D render to
their own tiled/UBWC targets and copy into the linear swapchain image at present — a full-frame
their own tiled/UBWC targets and copy into the linear swapchain image at present - a full-frame
linear write per frame on the game side, on top of the compositor's blit. This is a real part of
the 20 %.
- Advertising `QCOM_COMPRESSED` as well: one line in `bind_dmabuf`. The WSI intersects the
@@ -149,23 +149,23 @@ to our own Mesa build plus one small protocol.
`GPU_COLOR_OUTPUT` RGBA8 unless a CPU usage bit is set (the prototype exploits exactly that:
`sc_layer.c: alloc_slot` retries with `CPU_READ_RARELY` when a UBWC import fails). The DPU
scans out UBWC RGBA natively; a prior device experiment (GL native rendering, 2026-06-29)
found the SDE pipes accepted both UBWC and linear layers — rotation+scale was the blocker, not
found the SDE pipes accepted both UBWC and linear layers - rotation+scale was the blocker, not
UBWC.
## 3. Per-layer composition (design; the prototype implements the single-layer subset)
- **wl_surface → SurfaceControl.** One `ASurfaceControl_createFromWindow(surfaceView_window,
name)` child per mapped toplevel (and per subsurface tree flattened per toplevel); the desktop
surface is the base layer at z 0 (or, with `g_hide_shell`, a black base — the SurfaceView's own
surface is the base layer at z 0 (or, with `g_hide_shell`, a black base - the SurfaceView's own
buffer); toplevels get `setZOrder` from the `g_toplevels` order (`apply_zorder`), with
`INT_MAX` reserved for the app's cursor/HUD if they ever move native. Position/scale:
`ASurfaceTransaction_setGeometry(src = buffer rect, dst = output rect, transform 0)`, with the
output rect produced by the same `update_map()` / `draw_to_blit()` arithmetic the blit path uses
(`vk_present.c: vkp_map_draw`) — letterbox/FILL crop/TOP-BOTTOM half all come out identical, so
(`vk_present.c: vkp_map_draw`) - letterbox/FILL crop/TOP-BOTTOM half all come out identical, so
**input stays consistent by construction**: the app maps touch through `ViewTransformation`
with the same inputs (see `WAYLAND_RUNTIME.md` "Fullscreen mode"), and the compositor's own
`vkp_output_to_scene` keeps working because the map is still updated every frame.
`setBufferTransparency(OPAQUE)` for XR24/XB24; `setBufferTransform` stays 0 — the DPU refuses
`setBufferTransparency(OPAQUE)` for XR24/XB24; `setBufferTransform` stays 0 - the DPU refuses
rotation+scale on one pipe (prior experiment), so a rotated panel must be handled by the
activity's orientation, not the layer.
- **HUD.** Already a separate layer: `PerfHudView`/`FrameRating`/cursor are Android views in
@@ -177,7 +177,7 @@ to our own Mesa build plus one small protocol.
fires `wl_callback.done` for those surfaces (as today, `fire_all_frames`). `wp_presentation`
feedback moves from "after our present" to the transaction's `ASurfaceTransaction_setOnComplete`
→ `ASurfaceTransactionStats_getLatchTime` / `getPresentFenceFd` (API 29) give the real on-glass
time — better than today's `now_ns()`; `setFrameTimeline` (API 33) can be added later for
time - better than today's `now_ns()`; `setFrameTimeline` (API 33) can be added later for
`VK_GOOGLE_display_timing` work. Without a window, `pace_without_output` stays as is.
- **`wl_buffer.release` timing.** Today a replaced buffer is released on the limiter's cadence,
which is safe because the blit was waited for. With layers the buffer is on the display until
@@ -185,14 +185,14 @@ to our own Mesa build plus one small protocol.
transaction that **replaced** it delivers `ASurfaceTransactionStats_getPreviousReleaseFenceFd`
(this is what `ASurfaceRendererContext::transactionCompleteCallback` does), wait that fence on
the compositor thread (or, better, hand it to the game: with option (a) the WSI can import it as
the `WSI_ES_RELEASE` point — Turnip/KGSL implements `vk_sync.import_sync_file`,
the `WSI_ES_RELEASE` point - Turnip/KGSL implements `vk_sync.import_sync_file`,
`tu_knl_kgsl.cc:1261-1303`), then `wl_buffer_send_release`. The FPS limiter keeps working: it only
delays the release further. Android 16's `setBufferWithRelease` is not available on 14.
- **Acquire fence.** Two sources: (1) if the kernel supports `DMA_BUF_IOCTL_EXPORT_SYNC_FILE`
(6.x GKI), the WSI already attaches its present semaphore to the dma-buf as a sync_file
(`wsi_drm_init_swapchain_implicit_sync`, `wsi_common_drm.c:368-395`, gated on the driver
exporting `SYNC_FD` semaphores — KGSL does, `:1278-1303`), and the compositor can
`EXPORT_SYNC_FILE(READ)` it and pass the fd to `setBuffer` — the prototype probes this on the
exporting `SYNC_FD` semaphores - KGSL does, `:1278-1303`), and the compositor can
`EXPORT_SYNC_FILE(READ)` it and pass the fd to `setBuffer` - the prototype probes this on the
first frame and logs the answer; (2) otherwise the option-(a) protocol carries a sync_fd per
present (`vkGetSemaphoreFdKHR`), which is what the X11 wrapper does. Never `-1` for a real
zero-copy frame.
@@ -210,11 +210,11 @@ to our own Mesa build plus one small protocol.
src == dst was DEVICE on every layer, GPU_TARGET empty. So: no SurfaceControl rotation ever; on
a landscape-native panel (handheld) the identity-transform game layer with a DPU scale should
promote; on a portrait phone in landscape it will not, and the base swapchain path is the better
one there — decide per device by measurement, not by assumption. Measure with
one there - decide per device by measurement, not by assumption. Measure with
`dumpsys SurfaceFlinger` (layer `composition: DEVICE/DEVICE` vs `DEVICE/CLIENT`, and the SDE pipe
table `GPU_TARGET` line) with the app foreground and the drawer closed.
- **Layer count.** The DPU has a handful of pipes (4–8 usable RGB pipes on SDM/SM8xxx); 2–3 game
layers + base + HUD + cursor is fine, a desktop with 10 windows is not — SurfaceFlinger then
layers + base + HUD + cursor is fine, a desktop with 10 windows is not - SurfaceFlinger then
composites the overflow on the GPU (still correct, just no gain). Policy: layers only for
fullscreen/topmost windows, everything else stays in the base swapchain (the prototype's rule).
- **Buffer ownership across processes.** The game's `AHardwareBuffer` is alive in the guest; the
@@ -226,7 +226,7 @@ to our own Mesa build plus one small protocol.
real thing (the prototype waits on the CPU instead, which is safe).
- **Background / rotation / device lost.** Covered by the window-request serialisation; a device
loss in the compositor's Turnip does not affect layers already on screen (SurfaceFlinger owns
them) — the old path's `device_lost` message stays.
them) - the old path's `device_lost` message stays.
- **Non-Adreno / old SoCs.** Everything here is NDK API 29 + gralloc-allocated buffers; the only
vendor-specific piece is the `'gmsm'` handle sniff, which degrades to "linear with a CPU bit".
Same risk class as the X11 ASR renderer; keep it opt-in until proven on a second SoC.
@@ -248,7 +248,7 @@ D3D12 tabs) as the workload, session log in `Download/Wayland-logs/`.
variables, launch the AIO fullscreen. Expected log lines (tag `layer`):
`SurfaceControl "banner_wayland_game" created as a child of the screen surface`,
`pool buffer 1920x1080 UBWC (QCOM_COMPRESSED), stride 1920 px (gralloc handle 2 fds / N ints)`
(or `linear` after `import of a UBWC … failed` — also a result: it tells whether the compositor
(or `linear` after `import of a UBWC … failed` - also a result: it tells whether the compositor
Turnip accepts gralloc's UBWC pitch), `geometry: buffer 0,0-1920,1080 -> screen …`,
`presenting 1920x1080 game frames on their own SurfaceControl layer (UBWC pool, 3 buffers)`,
and the probe line `kernel exports sync_file fences from the game's dma-buf …` or
@@ -293,7 +293,7 @@ hides the layer and draws the old way; the layer is retired when the SurfaceView
Everything else (frame callbacks, presentation feedback, FPS limiter, HUD counting, input) is
unchanged. Off by default: with the variable unset no new code runs.
Not zero-copy yet — it is the receive side. The copy disappears when step 5 lands and
Not zero-copy yet - it is the receive side. The copy disappears when step 5 lands and
`sc_layer_present` is handed the game's own `AHardwareBuffer` instead of a pool slot.
## 7. Step 5 implemented (feat/wayland-zero-copy-wsi + banners-turnip-wayland `banner_ahb_wsi.py`)
@@ -302,8 +302,8 @@ Option (a′) as recommended, with one deviation forced by the build: the Waylan
`platforms=wayland` build with Android detection off, so `VK_ANDROID_external_memory_android_hardware_buffer`
/ `vk_android.c` do not exist in it. The WSI therefore allocates the `AHardwareBuffer` itself
(`libnativewindow.so` dlopen'd), sniffs the gralloc handle for UBWC exactly like `u_gralloc_fallback.c`,
and imports `native_handle->data[0]` as a dma-buf with an explicit modifier + pitch — the same path
this compositor's pool uses (`vkp_image_import_dmabuf`) — after a test `vkCreateImage` proves the
and imports `native_handle->data[0]` as a dma-buf with an explicit modifier + pitch - the same path
this compositor's pool uses (`vkp_image_import_dmabuf`) - after a test `vkCreateImage` proves the
driver accepts that layout (linear retry otherwise). Fences: implicit via the dma-buf in both
directions (§3's option 1, proven by the probe; the compositor imports SurfaceFlinger's release
fence back into the dma-buf before `wl_buffer.release`). Details in `WAYLAND_RUNTIME.md`.
@@ -1,7 +1,7 @@
#ifndef BANNER_COLOR_H
#define BANNER_COLOR_H
/*
* HDR10 output on the Wayland backend (opt-in) — the compositor half of HDR_RECON.md Phase A (round 1)
* HDR10 output on the Wayland backend (opt-in) - the compositor half of HDR_RECON.md Phase A (round 1)
* plus HDR-aware composition (round 2, hdr_compose.h), implemented in wl_color_mgmt.c. The game half
* already ships: Mesa's Wayland WSI in our Turnip is a wp_color_manager_v1 client and exposes
* VK_COLOR_SPACE_HDR10_ST2084_EXT as soon as a compositor advertises BT.2020 + ST 2084; DXVK takes it
@@ -11,13 +11,13 @@
* - the container's / game's "HDR output" setting is on, or BANNER_WAYLAND_HDR=1 in their
* environment (which overrides the setting either way);
* - the display the game is on lists HDR10 among its supported HDR types (read by the app from
* android.view.Display — a property of the connector, not of the device);
* android.view.Display - a property of the connector, not of the device);
* - display layers with dataspace control (ASurfaceTransaction_setBufferDataSpace, Android 10+)
* and the zero-copy global (banner_ahb_v1): the HDR frame (or the composed HDR picture) is shown
* on the game's own display layer, tagged BT2020_PQ.
* In game, the drawer's HDR output switch (banner_color_set_output) flips between that and the same
* frames tone-mapped to SDR, live; the gate itself never changes during a session.
* A closed gate advertises nothing — no colour-management global, no 10-bit dma-buf formats — so the
* A closed gate advertises nothing - no colour-management global, no 10-bit dma-buf formats - so the
* session is byte-for-byte what it was before this file existed, and the session log says why.
* BANNER_WAYLAND_HDR=force skips the display check only (testing the negotiation on an SDR panel;
* SurfaceFlinger then tone-maps the layer, and says so in its composition type).
@@ -83,7 +83,7 @@ int banner_color_hdr_state(void);
/* Nits SDR content is placed at inside an HDR picture (default 203, BT.2408); any thread. */
void banner_color_set_sdr_white(float nits);
float banner_color_sdr_white(void);
/* The HDR session was asked for (BANNER_WAYLAND_HDR / the setting resolved on or force) — known before
/* The HDR session was asked for (BANNER_WAYLAND_HDR / the setting resolved on or force) - known before
* the compositor starts, so the Vulkan instance can enable VK_EXT_swapchain_colorspace for it. */
int banner_color_requested(void);
/* The drawer's live "HDR output" switch, per session, starting ON. On = HDR frames go to the display as
@@ -3,7 +3,7 @@
*
* The app's threads (UI thread, clipboard listener) hand text to the compositor through
* banner_host_* ; the messages are queued under a mutex and a pipe byte wakes the wl event
* loop, which drains them on the compositor thread — the only thread that may touch
* loop, which drains them on the compositor thread - the only thread that may touch
* wl_resources.
*/
#define _GNU_SOURCE 1
+416 -29
View File
@@ -1,5 +1,5 @@
/*
* bannerlator-wayland — the embedded compositor of the Wayland display path.
* bannerlator-wayland - the embedded compositor of the Wayland display path.
*
* Clients are the Wine processes of a container (winewayland.drv) and, through them,
* Mesa's Vulkan WSI. Each process is its own client, so a Windows virtual desktop is
@@ -65,7 +65,7 @@
static FILE *g_log;
/* Lines logged before the session file exists are kept here and written into it, in order, the
* moment it opens — the compositor runs on its own thread, so anything the app reports as that
* moment it opens - the compositor runs on its own thread, so anything the app reports as that
* thread starts (the display's HDR capability, for one) would otherwise only reach logcat. The
* lock also serialises the file writes, which now come from the app's threads too. */
#define PRELOG_MAX 32
@@ -226,7 +226,7 @@ static void on_client_created(struct wl_listener *l, void *data) {
/* ------------------------------------------------------------------ surfaces */
enum surface_role { ROLE_NONE, ROLE_TOPLEVEL, ROLE_SUBSURFACE, ROLE_DESKTOP };
enum surface_role { ROLE_NONE, ROLE_TOPLEVEL, ROLE_SUBSURFACE, ROLE_DESKTOP, ROLE_CURSOR };
struct surface {
struct wl_resource *resource;
@@ -275,6 +275,7 @@ struct surface {
int sub_x, sub_y, sub_pending_x, sub_pending_y, sub_pending;
int below_parent;
int fullscreen; /* xdg_toplevel.set_fullscreen: no client decorations */
int toplevel_committed; /* the toplevel's initial commit was answered */
char *title; /* xdg_toplevel title, for the session log */
int announced_vulkan; /* logged its first dmabuf frame */
@@ -432,7 +433,7 @@ static int on_release_timer(int fd, uint32_t mask, void *data) {
* Each release takes the next slot of a per-surface cadence (one slot per interval), so a game
* gets one buffer back per interval however fast it commits. The schedule is kept honest at both
* ends: a slot in the past is brought to now (the game was slower than the cap, no catch-up burst
* is owed), and it never runs further ahead than the releases actually queued — with p releases
* is owed), and it never runs further ahead than the releases actually queued - with p releases
* still pending the new one lands at most (p + 1) intervals out, nothing pending means at most
* one interval. Without that bound the schedule kept slots that were consumed but never
* delivered (a swapchain rebuild destroys buffers with queued releases) or that a coarser earlier
@@ -464,17 +465,86 @@ static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_
/* Each Wine process is a separate client with its own wl_pointer / wl_keyboard. */
struct seat_pointer { struct wl_resource *ptr; struct wl_resource *focus; };
struct seat_keyboard { struct wl_resource *kb; struct wl_resource *focus; };
struct seat_touch { struct wl_resource *touch; struct wl_resource *focus; };
#define MAX_PTRS 32
#define MAX_TOUCH_POINTS 32
static struct seat_pointer g_ptrs[MAX_PTRS];
static int g_nptrs;
static struct seat_keyboard g_kbs[MAX_PTRS];
static int g_nkbs;
static struct seat_touch g_touches[MAX_PTRS];
static int g_ntouches;
/* ---- the client's cursor (wl_pointer.set_cursor) -------------------------------------------
* Not composited: under zero-copy the program's buffer goes straight to an Android layer and there
* is nothing to composite into. The cursor surface is snapshotted here instead and the app draws it
* as its overlay, above every present path (Bannerlator's design). What it buys: the real SHAPE -
* labwc's resize arrows at a window's edges and corners, a text field's I-beam - and an
* authoritative HIDE (set_cursor with a NULL surface, what a mouse-look game does).
* wl_shm cursors (Wine, pixman) are copied directly; a GPU cursor (labwc on Vulkan/GLES, which
* wlroots hands over as a dma-buf, usually UBWC) is read back through vkp_image_readback. */
#define CURSOR_MAX_PX (256 * 256)
static pthread_mutex_t g_cursor_lock = PTHREAD_MUTEX_INITIALIZER;
static struct surface *g_cursor_surface; /* compositor thread only */
static int g_cursor_hx, g_cursor_hy; /* hotspot, surface-local */
static uint32_t g_cursor_px[CURSOR_MAX_PX]; /* guarded by g_cursor_lock: ARGB8888 snapshot */
static uint32_t g_cursor_rb[CURSOR_MAX_PX]; /* compositor thread: GPU readback staging */
static int g_cursor_w, g_cursor_h; /* 0 = nothing to draw */
static int g_cursor_hidden = 1; /* the client asked for no pointer */
static int g_cursor_serial; /* bumped on every change; the app polls it */
static void cursor_publish_hidden(void) {
pthread_mutex_lock(&g_cursor_lock);
if (!g_cursor_hidden) { g_cursor_hidden = 1; g_cursor_serial++; }
pthread_mutex_unlock(&g_cursor_lock);
}
static void cursor_publish_pixels(const uint8_t *src, int w, int h, size_t stride, int hx, int hy) {
if (w <= 0 || h <= 0 || w * h > CURSOR_MAX_PX) return;
pthread_mutex_lock(&g_cursor_lock);
for (int y = 0; y < h; y++)
memcpy(&g_cursor_px[y * w], src + (size_t)y * stride, (size_t)w * 4);
g_cursor_w = w; g_cursor_h = h;
g_cursor_hx = hx; g_cursor_hy = hy;
g_cursor_hidden = 0;
g_cursor_serial++;
pthread_mutex_unlock(&g_cursor_lock);
}
static void cursor_publish_shm(struct wl_shm_buffer *shm, int hx, int hy) {
int32_t w = wl_shm_buffer_get_width(shm), h = wl_shm_buffer_get_height(shm);
if (w <= 0 || h <= 0 || w * h > CURSOR_MAX_PX) return;
wl_shm_buffer_begin_access(shm);
cursor_publish_pixels((const uint8_t *)wl_shm_buffer_get_data(shm), w, h,
(size_t)wl_shm_buffer_get_stride(shm), hx, hy);
wl_shm_buffer_end_access(shm);
}
/* Read by the app (UI thread). out = [serial, hidden, w, h, hotspotX, hotspotY, pixels...].
* Returns the number of ints written, or 0 if out is too small. */
int banner_cursor_snapshot(int *out, int cap) {
int n = 0;
pthread_mutex_lock(&g_cursor_lock);
int need = 6 + (g_cursor_hidden ? 0 : g_cursor_w * g_cursor_h);
if (cap >= need) {
out[0] = g_cursor_serial; out[1] = g_cursor_hidden;
out[2] = g_cursor_w; out[3] = g_cursor_h;
out[4] = g_cursor_hx; out[5] = g_cursor_hy;
if (!g_cursor_hidden)
memcpy(out + 6, g_cursor_px, (size_t)g_cursor_w * g_cursor_h * 4);
n = need;
}
pthread_mutex_unlock(&g_cursor_lock);
return n;
}
struct active_touch { struct surface *surface; int pointer_fallback; };
static struct active_touch g_touch_points[MAX_TOUCH_POINTS];
static void touch_cancel_surface(struct surface *surface);
static struct surface *g_grab; /* no-desktop fallback: surface holding the button */
static struct surface *g_key_target; /* no-desktop fallback: last clicked surface */
static struct surface *g_ime_click; /* last clicked program window: where text input goes */
static int g_input_pipe[2] = {-1, -1};
/* type 0 = pointer (p1=action 0down/1move/2up, p2=x, p3=y); type 1 = key (p1=evdev, p2=state 1down/0up) */
struct input_msg { int type; int p1; int p2; int p3; };
struct input_msg { int type; int p1; int p2; int p3; int p4; };
/* Pointer constraints (zwp_pointer_constraints_v1) and relative pointers
* (zwp_relative_pointer_manager_v1); see the "pointer constraints" section. */
@@ -589,6 +659,10 @@ static void map_toplevel(struct surface *s) {
s->placed ? "" : " (no desktop position yet)");
}
wl_list_insert(g_toplevels.prev, &s->toplevel_link); /* new windows start on top */
/* Without a Wine desktop the clients are whole programs (the Linux desktop, a gamescope a
* program on it started over it): a window that opens on top takes the keys too, as it would
* on any desktop, rather than the one clicked last that is now underneath it. */
if (!g_desktop) g_key_target = s;
apply_zorder();
}
@@ -675,9 +749,11 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res
if (ci && ci->asked_feedback) ci->shm_frames++; /* since it asked for GPU buffers */
}
/* The window the app's performance HUD follows: the latest one to start presenting GPU frames
* (X11 binds the HUD to the _MESA_DRV window and counts X presents instead). JNI upcalls. */
/* The window the app's performance HUD follows: the latest one at least as big as the last to
* start presenting GPU frames (take_dmabuf) (X11 binds the HUD to the _MESA_DRV window and counts X presents instead). JNI upcalls. */
static struct surface *g_hud_surface;
/* When the HUD's window last presented a frame (now_ns), to let a replacement take over. */
static int64_t g_hud_last_ns;
extern void banner_on_game_surface(const char *window, const char *gpu); /* window NULL = gone */
extern void banner_on_game_frame(void);
/* The program behind that window: its Linux pid (the Wayland client's credentials) and executable name
@@ -720,6 +796,7 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
s->buf_h = b->height;
s->has_content = b->img != NULL;
g_stat_dmabuf++;
const int announced_now = !s->announced_vulkan;
if (!s->announced_vulkan) {
char name[160];
s->announced_vulkan = 1;
@@ -740,14 +817,23 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
else
banner_log("error", "could not import GPU frames from %s (%dx%d, modifier %#llx)",
name, b->width, b->height, (unsigned long long)b->modifier);
/* The HUD follows the window whether its frames are copied or go straight to the layer:
* a zero-copy frame the compositor never imported is still a presented game frame. */
if (b->img || ahb_swapchain_has_ahb(b)) {
g_hud_surface = s;
banner_on_game_surface(name, vkp_gpu_name());
struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
if (ci) banner_on_game_program((int)ci->pid, strncmp(ci->name, "pid ", 4) ? ci->name : "");
}
}
/* The HUD follows the window whether its frames are copied or go straight to the layer: a
* zero-copy frame the compositor never imported is still a presented game frame. A
* program's small surfaces - gamescope's 1x1 cursor, Qt's menus and tooltips (melonDS) -
* came after the game's and drew too rarely to count, which left the HUD on 0 fps while the
* game ran at 60. So a new window takes the HUD only when it is at least as big (a
* full-screen gamescope over the desktop still does), and a window gone quiet for a second
* gives way to whatever draws next. */
if (s != g_hud_surface && (b->img || ahb_swapchain_has_ahb(b)) &&
(!g_hud_surface || now_ns() - g_hud_last_ns > 1000000000LL ||
(announced_now && (int64_t)b->width * b->height >= (int64_t)g_hud_surface->buf_w * g_hud_surface->buf_h))) {
char name[160];
describe(s, name, sizeof(name));
g_hud_surface = s;
banner_on_game_surface(name, vkp_gpu_name());
struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
if (ci) banner_on_game_program((int)ci->pid, strncmp(ci->name, "pid ", 4) ? ci->name : "");
}
/* HDR session (gate open): a DXVK game switches to HDR by REBUILDING its swapchain on the same
* surface, so the one-shot line above never sees the 10-bit buffers - name every format change. */
@@ -769,7 +855,7 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
* Asking whether the compositor could import it, as this used to, is a question about the
* copy path and not about whether a frame happened: a game whose buffers go straight to
* the display layer draws on screen while the counter sat at 0.0 fps and 1000.0 ms. */
if (s == g_hud_surface) banner_on_game_frame();
if (s == g_hud_surface) { g_hud_last_ns = now_ns(); banner_on_game_frame(); }
}
/* ---- hooks for ahb_swapchain.c (zero-copy layers) */
@@ -884,10 +970,21 @@ static void surface_set_opaque(struct wl_client *c, struct wl_resource *r,
static void surface_set_input(struct wl_client *c, struct wl_resource *r,
struct wl_resource *region) {}
static void send_toplevel_configure(struct surface *s);
static void surface_commit(struct wl_client *c, struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
struct surface *child;
/* xdg-shell: the initial commit of a toplevel is answered with a configure. One is also sent
* at get_toplevel, which Wine's driver has always had; a client that waits for the reply to its
* commit on a queue of its own (wlroots' Wayland backend - the desktop's labwc) can read that
* early one onto the wrong queue and would wait for ever. */
if (s->xdg_toplevel && !s->toplevel_committed) {
s->toplevel_committed = 1;
send_toplevel_configure(s);
}
if (s->pending_src_set) {
s->src_set = s->pending_src[2] > 0;
memcpy(s->src, s->pending_src, sizeof(s->src));
@@ -914,8 +1011,26 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
s->pending_buffer = NULL;
s->pending_attach = 0;
if (s->role == ROLE_NONE) {
/* Cursor or role-less surface: never drawn (the app draws its own pointer). */
if (s->role == ROLE_CURSOR) {
/* The client's pointer image, copied out for the app's overlay (see cursor_publish_*)
* rather than composited, so it survives the zero-copy and HDR layer paths. */
if (!buffer) {
cursor_publish_hidden(); /* wlroots clears its cursor surface to hide the pointer */
} else if (shm) {
cursor_publish_shm(shm, g_cursor_hx, g_cursor_hy);
} else if (db && db->n_planes >= 1 && db->width * db->height <= CURSOR_MAX_PX) {
if (!db->img && !db->import_failed) {
db->img = vkp_image_from_dmabuf(db->fd[0], db->format, db->modifier, db->width, db->height,
db->stride[0], db->offset[0]);
if (!db->img) db->import_failed = 1;
}
if (db->img && vkp_image_readback(db->img, g_cursor_rb, CURSOR_MAX_PX) == 0)
cursor_publish_pixels((const uint8_t *)g_cursor_rb, db->width, db->height,
(size_t)db->width * 4, g_cursor_hx, g_cursor_hy);
}
if (buffer) wl_buffer_send_release(buffer);
} else if (s->role == ROLE_NONE) {
/* Role-less surface: never drawn. */
if (buffer) wl_buffer_send_release(buffer);
} else if (db) {
take_dmabuf(s, db, buffer);
@@ -986,6 +1101,8 @@ static void surface_resource_destroy(struct wl_resource *r) {
if (!s) return;
for (int i = 0; i < g_nptrs; i++) if (g_ptrs[i].focus == r) g_ptrs[i].focus = NULL;
for (int i = 0; i < g_nkbs; i++) if (g_kbs[i].focus == r) g_kbs[i].focus = NULL;
touch_cancel_surface(s);
if (g_cursor_surface == s) { g_cursor_surface = NULL; cursor_publish_hidden(); }
if (g_grab == s) g_grab = NULL;
if (g_key_target == s) g_key_target = NULL;
if (g_ime_click == s) g_ime_click = NULL;
@@ -1298,6 +1415,7 @@ static void xdg_toplevel_resource_destroy(struct wl_resource *r) {
struct surface *s = wl_resource_get_user_data(r);
if (!s) return;
s->xdg_toplevel = NULL;
s->toplevel_committed = 0;
unmap_toplevel(s);
s->placed = 0;
s->hwnd = 0;
@@ -1315,8 +1433,23 @@ static void xdg_surface_get_toplevel(struct wl_client *c, struct wl_resource *r,
s->xdg_toplevel = tl;
send_toplevel_configure(s);
}
/* A popup is dismissed as soon as it is made (see the positioner below). */
static void popup_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void popup_grab(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat, uint32_t serial) {}
static void popup_reposition(struct wl_client *c, struct wl_resource *r, struct wl_resource *positioner,
uint32_t token) {}
static const struct xdg_popup_interface popup_impl = {
.destroy = popup_destroy,
.grab = popup_grab,
.reposition = popup_reposition,
};
static void xdg_surface_get_popup(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *parent, struct wl_resource *positioner) {}
struct wl_resource *parent, struct wl_resource *positioner) {
struct wl_resource *p = wl_resource_create(c, &xdg_popup_interface, wl_resource_get_version(r), id);
if (!p) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(p, &popup_impl, NULL, NULL);
xdg_popup_send_popup_done(p);
}
static void xdg_surface_set_geometry(struct wl_client *c, struct wl_resource *r,
int32_t x, int32_t y, int32_t w, int32_t h) {}
static void xdg_surface_ack_configure(struct wl_client *c, struct wl_resource *r, uint32_t serial) {}
@@ -1333,9 +1466,40 @@ static void xdg_surface_resource_destroy(struct wl_resource *r) {
}
static void xdg_wm_base_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
/* Popups are not drawn here, but their requests must still land somewhere: a resource without an
* implementation makes libwayland abort on the first request to it - the whole compositor, and the
* app with it. A positioner takes every request and keeps nothing; a popup is dismissed at once
* (popup_done), which is what a client does with a popup it cannot show. Seen with a gamescope
* started from the Linux desktop beside labwc. */
static void positioner_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static void positioner_set_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {}
static void positioner_set_anchor_rect(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y,
int32_t w, int32_t h) {}
static void positioner_set_anchor(struct wl_client *c, struct wl_resource *r, uint32_t a) {}
static void positioner_set_gravity(struct wl_client *c, struct wl_resource *r, uint32_t g) {}
static void positioner_set_constraint_adjustment(struct wl_client *c, struct wl_resource *r, uint32_t a) {}
static void positioner_set_offset(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y) {}
static void positioner_set_reactive(struct wl_client *c, struct wl_resource *r) {}
static void positioner_set_parent_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {}
static void positioner_set_parent_configure(struct wl_client *c, struct wl_resource *r, uint32_t serial) {}
static const struct xdg_positioner_interface positioner_impl = {
.destroy = positioner_destroy,
.set_size = positioner_set_size,
.set_anchor_rect = positioner_set_anchor_rect,
.set_anchor = positioner_set_anchor,
.set_gravity = positioner_set_gravity,
.set_constraint_adjustment = positioner_set_constraint_adjustment,
.set_offset = positioner_set_offset,
.set_reactive = positioner_set_reactive,
.set_parent_size = positioner_set_parent_size,
.set_parent_configure = positioner_set_parent_configure,
};
static void xdg_wm_base_create_positioner(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct wl_resource *p = wl_resource_create(c, &xdg_positioner_interface, wl_resource_get_version(r), id);
if (p) wl_resource_set_implementation(p, NULL, NULL, NULL);
if (!p) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(p, &positioner_impl, NULL, NULL);
}
static void xdg_wm_base_get_xdg_surface(struct wl_client *c, struct wl_resource *r, uint32_t id,
struct wl_resource *surf) {
@@ -1721,9 +1885,15 @@ static void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t
/* ------------------------------------------------------------------ wl_output */
/* wl_output v3+ has a request (release); without an implementation the first client to send it
* aborted the compositor. */
static void output_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_output_interface output_impl = { .release = output_release };
static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &wl_output_interface, ver, id);
wl_resource_set_implementation(r, NULL, NULL, NULL);
if (!r) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(r, &output_impl, NULL, NULL);
wl_output_send_geometry(r, 0, 0, 340, 190, WL_OUTPUT_SUBPIXEL_UNKNOWN,
"Bannerlator", "Wayland", WL_OUTPUT_TRANSFORM_NORMAL);
wl_output_send_mode(r, WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED,
@@ -2309,9 +2479,23 @@ static uint32_t now_ms(void) {
return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}
/* wl_pointer.set_cursor: the client tells us its pointer image, or asks for none. A NULL surface
* means "no cursor" and is authoritative. The surface itself is snapshotted on its next commit. */
static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint32_t serial,
struct wl_resource *surface, int32_t hx, int32_t hy) {
/* The app draws its own pointer; cursor surfaces stay role-less and aren't drawn. */
if (!surface) {
if (g_cursor_surface) g_cursor_surface->role = ROLE_NONE;
g_cursor_surface = NULL;
cursor_publish_hidden();
return;
}
struct surface *s = wl_resource_get_user_data(surface);
if (!s) return;
if (s->role != ROLE_NONE && s->role != ROLE_CURSOR) return; /* already has another role */
s->role = ROLE_CURSOR;
g_cursor_surface = s;
g_cursor_hx = hx;
g_cursor_hy = hy;
}
static void pointer_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_pointer_interface pointer_impl = {
@@ -2332,6 +2516,10 @@ static void keyboard_res_destroy(struct wl_resource *r) {
}
static void touch_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_touch_interface touch_impl = { .release = touch_release };
static void touch_res_destroy(struct wl_resource *r) {
for (int i = 0; i < g_ntouches; i++)
if (g_touches[i].touch == r) { g_touches[i] = g_touches[--g_ntouches]; break; }
}
static void seat_get_pointer(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct wl_resource *p = wl_resource_create(c, &wl_pointer_interface, wl_resource_get_version(r), id);
@@ -2363,7 +2551,13 @@ static void seat_get_keyboard(struct wl_client *c, struct wl_resource *r, uint32
}
static void seat_get_touch(struct wl_client *c, struct wl_resource *r, uint32_t id) {
struct wl_resource *t = wl_resource_create(c, &wl_touch_interface, wl_resource_get_version(r), id);
if (t) wl_resource_set_implementation(t, &touch_impl, NULL, NULL);
if (!t) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(t, &touch_impl, NULL, touch_res_destroy);
if (g_ntouches < MAX_PTRS) {
g_touches[g_ntouches].touch = t;
g_touches[g_ntouches].focus = NULL;
g_ntouches++;
}
}
static void seat_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
static const struct wl_seat_interface seat_impl = {
@@ -2373,7 +2567,7 @@ static const struct wl_seat_interface seat_impl = {
static void bind_seat(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
struct wl_resource *r = wl_resource_create(c, &wl_seat_interface, ver, id);
wl_resource_set_implementation(r, &seat_impl, NULL, NULL);
wl_seat_send_capabilities(r, WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD);
wl_seat_send_capabilities(r, WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD | WL_SEAT_CAPABILITY_TOUCH);
if (ver >= 2) wl_seat_send_name(r, "bannerlator-seat");
}
@@ -2397,6 +2591,9 @@ static struct seat_keyboard *keyboard_for(struct wl_client *client) {
#define for_each_keyboard_of(client, sk) \
for (int _i = 0; _i < g_nkbs; _i++) \
if (((sk) = &g_kbs[_i]), wl_resource_get_client((sk)->kb) == (client))
#define for_each_touch_of(client, st) \
for (int _i = 0; _i < g_ntouches; _i++) \
if (((st) = &g_touches[_i]), wl_resource_get_client((st)->touch) == (client))
/* Topmost window whose scene rectangle contains the point (no-desktop fallback). */
static struct surface *toplevel_at(double x, double y) {
@@ -2931,6 +3128,188 @@ static void deliver_pointer(const struct input_msg *m) {
pointer_event(x, y, m->p1 == 1 ? 0 : BTN_LEFT, m->p1 == 0);
}
static void input_output_to_scene(int px, int py, double *x, double *y) {
int w, h, ow, oh;
scene_size(&w, &h);
vkp_output_size(&ow, &oh);
if (ow <= 0 || oh <= 0 ||
!vkp_output_to_scene((double)px * ow / INPUT_SPACE_W, (double)py * oh / INPUT_SPACE_H, x, y)) {
*x = (double)px * w / INPUT_SPACE_W;
*y = (double)py * h / INPUT_SPACE_H;
}
if (*x < 0) *x = 0;
if (*y < 0) *y = 0;
if (*x > w - 1) *x = w - 1;
if (*y > h - 1) *y = h - 1;
}
static int touch_points_for_client(struct wl_client *client) {
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
struct surface *surface = g_touch_points[i].surface;
if (surface && wl_resource_get_client(surface->resource) == client) return 1;
}
return 0;
}
static void touch_frame(struct wl_client *client) {
struct seat_touch *st;
for_each_touch_of(client, st)
if (wl_resource_get_version(st->touch) >= WL_TOUCH_FRAME_SINCE_VERSION)
wl_touch_send_frame(st->touch);
}
static void touch_cancel_client(struct wl_client *client) {
struct seat_touch *st;
int release_pointer = 0;
for_each_touch_of(client, st) {
if (st->focus) wl_touch_send_cancel(st->touch);
st->focus = NULL;
}
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
struct surface *surface = g_touch_points[i].surface;
if (surface && wl_resource_get_client(surface->resource) == client) {
release_pointer |= g_touch_points[i].pointer_fallback;
memset(&g_touch_points[i], 0, sizeof(g_touch_points[i]));
}
}
if (release_pointer) pointer_button(BTN_LEFT, 0);
wl_display_flush_clients(g_display);
}
static void touch_cancel_all(void) {
int release_pointer = 0;
for (int i = 0; i < g_ntouches; i++) {
if (g_touches[i].focus) wl_touch_send_cancel(g_touches[i].touch);
g_touches[i].focus = NULL;
}
for (int i = 0; i < MAX_TOUCH_POINTS; i++)
release_pointer |= g_touch_points[i].pointer_fallback;
memset(g_touch_points, 0, sizeof(g_touch_points));
if (release_pointer) pointer_button(BTN_LEFT, 0);
wl_display_flush_clients(g_display);
}
static void touch_cancel_surface(struct surface *surface) {
struct wl_client *client = wl_resource_get_client(surface->resource);
for (int i = 0; i < MAX_TOUCH_POINTS; i++)
if (g_touch_points[i].surface == surface) {
touch_cancel_client(client);
return;
}
}
static struct surface *touch_target_at(double x, double y) {
struct surface *target = toplevel_at(x, y);
if (target) return target;
if (g_desktop) return g_desktop;
return g_grab ? g_grab : g_key_target;
}
static void touch_down(int id, double x, double y) {
if (id < 0 || id >= MAX_TOUCH_POINTS) return;
struct surface *target = touch_target_at(x, y);
if (!target) return;
struct wl_client *client = wl_resource_get_client(target->resource);
/* A down on an id that is still held means that sequence's up was lost: end it first. The
* fingers still down are counted only after that - counted before, a lost up left any_active
* set, and on the pointer path every later touch was dropped (a tap or swipe did nothing). */
if (g_touch_points[id].surface) touch_cancel_client(client);
int any_active = 0;
for (int i = 0; i < MAX_TOUCH_POINTS; i++) {
struct surface *active = g_touch_points[i].surface;
if (!active) continue;
if (active != target) {
touch_cancel_all();
any_active = 0;
break;
}
any_active = 1;
}
struct seat_touch *st;
int has_touch = 0;
for_each_touch_of(client, st) has_touch = 1;
/* One line per gesture (first finger), so a test can see where a touch went. */
if (!any_active) {
char name[160];
describe(target, name, sizeof(name));
banner_log("touch", "%s gets %s", name, has_touch ? "touch" : "the pointer (it takes no touch)");
}
if (!has_touch) {
if (any_active) return; /* one finger drives the pointer; a second one is ignored */
g_touch_points[id].surface = target;
g_touch_points[id].pointer_fallback = 1;
pointer_event(x, y, BTN_LEFT, 1);
wl_display_flush_clients(g_display);
return;
}
int tx = 0, ty = 0;
if (target != g_desktop && target->placed) { tx = target->x; ty = target->y; }
wl_fixed_t fx = wl_fixed_from_double(x - tx), fy = wl_fixed_from_double(y - ty);
int sent = 0;
for_each_touch_of(client, st) {
st->focus = target->resource;
wl_touch_send_down(st->touch, wl_display_next_serial(g_display), now_ms(),
target->resource, id, fx, fy);
sent = 1;
}
if (!sent) return;
g_touch_points[id].surface = target;
touch_frame(client);
wl_display_flush_clients(g_display);
}
static void touch_motion(int id, double x, double y) {
if (id < 0 || id >= MAX_TOUCH_POINTS) return;
struct surface *target = g_touch_points[id].surface;
if (!target) return;
if (g_touch_points[id].pointer_fallback) {
pointer_event(x, y, 0, 0);
wl_display_flush_clients(g_display);
return;
}
struct wl_client *client = wl_resource_get_client(target->resource);
int tx = 0, ty = 0;
if (target != g_desktop && target->placed) { tx = target->x; ty = target->y; }
wl_fixed_t fx = wl_fixed_from_double(x - tx), fy = wl_fixed_from_double(y - ty);
struct seat_touch *st;
for_each_touch_of(client, st)
if (st->focus == target->resource)
wl_touch_send_motion(st->touch, now_ms(), id, fx, fy);
touch_frame(client);
wl_display_flush_clients(g_display);
}
static void touch_up(int id, double x, double y) {
if (id < 0 || id >= MAX_TOUCH_POINTS) return;
struct surface *target = g_touch_points[id].surface;
if (!target) return;
if (g_touch_points[id].pointer_fallback) {
memset(&g_touch_points[id], 0, sizeof(g_touch_points[id]));
pointer_event(x, y, BTN_LEFT, 0);
wl_display_flush_clients(g_display);
return;
}
struct wl_client *client = wl_resource_get_client(target->resource);
struct seat_touch *st;
for_each_touch_of(client, st)
if (st->focus == target->resource)
wl_touch_send_up(st->touch, wl_display_next_serial(g_display), now_ms(), id);
memset(&g_touch_points[id], 0, sizeof(g_touch_points[id]));
touch_frame(client);
if (!touch_points_for_client(client))
for_each_touch_of(client, st) st->focus = NULL;
wl_display_flush_clients(g_display);
}
static void deliver_touch(const struct input_msg *m) {
double x, y;
input_output_to_scene(m->p3, m->p4, &x, &y);
if (m->p1 == 0) touch_down(m->p2, x, y);
else if (m->p1 == 1) touch_motion(m->p2, x, y);
else if (m->p1 == 2) touch_up(m->p2, x, y);
else if (m->p1 == 3) touch_cancel_all();
}
static void key_event(uint32_t evdev, int pressed);
struct wl_resource *banner_ime_target(void);
static void deliver_key(const struct input_msg *m) {
@@ -2999,6 +3378,7 @@ static int on_input_readable(int fd, uint32_t mask, void *data) {
case 4: scroll_event(m.p1); break;
case 5: on_vsync(((int64_t)m.p1 << 32) | (uint32_t)m.p2); break;
case 6: pointer_delta(m.p1 / 256.0, m.p2 / 256.0); break; /* relative motion, 1/256 px */
case 7: deliver_touch(&m); break;
default: deliver_pointer(&m); break;
}
}
@@ -3009,7 +3389,14 @@ static int on_input_readable(int fd, uint32_t mask, void *data) {
* dispatches it. x/y are in INPUT_SPACE (0..1919, 0..1079) over the whole output. */
void banner_wayland_send_pointer(int action, int x, int y) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { 0, action, x, y };
struct input_msg m = { 0, action, x, y, 0 };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
void banner_wayland_send_touch(int action, int pointer_id, int x, int y) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { 7, action, pointer_id, x, y };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
@@ -3017,7 +3404,7 @@ void banner_wayland_send_pointer(int action, int x, int y) {
/* Called from JNI. Queues a key event. evdev = Linux input keycode (e.g. KEY_A=30); state 1=down 0=up. */
void banner_wayland_send_key(int evdev, int state) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { 1, evdev, state, 0 };
struct input_msg m = { 1, evdev, state, 0, 0 };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
@@ -3025,7 +3412,7 @@ void banner_wayland_send_key(int evdev, int state) {
/* Called from JNI on every screen refresh (Choreographer). */
void banner_wayland_vsync(int64_t frame_time_ns) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { 5, (int)(frame_time_ns >> 32), (int)(uint32_t)frame_time_ns, 0 };
struct input_msg m = { 5, (int)(frame_time_ns >> 32), (int)(uint32_t)frame_time_ns, 0, 0 };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
@@ -3035,7 +3422,7 @@ void banner_wayland_vsync(int64_t frame_time_ns) {
* 6 = relative motion by a,b in 1/256 pixel (the app's Relative Mouse / captured-mouse path). */
void banner_wayland_send_scene_input(int type, int a, int b) {
if (g_input_pipe[1] < 0) return;
struct input_msg m = { type, a, b, 0 };
struct input_msg m = { type, a, b, 0, 0 };
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
(void)n;
}
@@ -1,4 +1,4 @@
/* Screen-effect chain — see effects_chain.h. Compositor thread unless noted. */
/* Screen-effect chain - see effects_chain.h. Compositor thread unless noted. */
#define _POSIX_C_SOURCE 200809L
#include "effects_chain.h"
#include "vk_loader.h"
@@ -8,7 +8,7 @@
#include <stdlib.h>
#include <string.h>
/* The X11 Vulkan renderer's SPIR-V, pre-compiled by app/src/main/cpp/winlator/gen_shaders.sh
/* The X11 Vulkan renderer's SPIR-V, pre-compiled by app/src/main/cpp/framegen/gen_shaders.sh
* (glslangValidator --vn) and committed next to the GLSL: the NDK build compiles no shaders, so
* there is no build-time tooling for CI to break. Same bytes as the X11 chain runs. */
#include "upscale_vert.h"
@@ -309,7 +309,7 @@ static VkShaderModule shader(const uint32_t *code, size_t size) {
}
/* One full-quad post pipeline: upscale.vert + the effect's fragment shader, opaque, dynamic
* viewport/scissor — the X11 renderer's createPostPipeline. */
* viewport/scissor - the X11 renderer's createPostPipeline. */
static VkPipeline post_pipeline(VkShaderModule vert, const uint32_t *frag_code, size_t frag_size, const char *name) {
VkShaderModule frag = shader(frag_code, frag_size);
if (!frag) { banner_log("error", "effects: %s: vkCreateShaderModule failed", name); return VK_NULL_HANDLE; }
@@ -2,7 +2,7 @@
#define EFFECTS_CHAIN_H
/*
* Screen-effect chain for the embedded Wayland compositor: the X11 Vulkan renderer's post
* chain (the .frag files in app/src/main/cpp/winlator, VulkanRendererContext::recordUpscalePasses) run
* chain (the .frag files in app/src/main/cpp/framegen, VulkanRendererContext::recordUpscalePasses) run
* between the composited scene and the output blit, with the same SPIR-V, push-constant
* layouts, uniform ranges and pass order, so a saved preset looks the same on both backends.
*
@@ -3,7 +3,7 @@
/*
* Frame generation on the Wayland backend (feat/wayland-framegen).
*
* The two NATIVE engines the X11 renderer runs inside libwinlator's compositor - LSFG Native
* The two NATIVE engines the X11 renderer runs inside Bannerlator's X11 compositor - LSFG Native
* (the user's Lossless.dll chain, lsfg/) and Win-FG Native (our own FSR3-derived optical-flow
* chain, winfg/) - are hosted here inside the Wayland compositor's Turnip device. Both consume
* consecutive presented frames and emit interpolated frames between them; the guest never
@@ -1,7 +1,7 @@
// C++ side of the Wayland frame-generation bridge - see framegen_engine.h.
//
// Hosts the SAME engine objects the X11 renderer drives (lsfg::Engine, winfg::Engine), fed from
// the compositor's own Turnip entry points. Nothing in winlator/lsfg or winlator/winfg is
// the compositor's own Turnip entry points. Nothing in framegen/lsfg or framegen/winfg is
// modified: the engines take a VkTable (VulkanRendererContext.h) for their dispatch, and one is
// filled here by name from the compositor's vkGetInstanceProcAddr / vkGetDeviceProcAddr.
#include "framegen_engine.h"
@@ -3,10 +3,10 @@
/*
* C face of the C++ frame-generation engines for framegen_bridge.c.
*
* framegen_engine.cpp wraps lsfg::Engine (winlator/lsfg) and winfg::Engine (winlator/winfg)
* framegen_engine.cpp wraps lsfg::Engine (framegen/lsfg) and winfg::Engine (framegen/winfg)
* unmodified - the same objects the X11 renderer (VulkanRendererContext) drives - and fills
* their dispatch tables from the compositor's own Turnip entry points. The standalone
* compositor build (no winlator tree) compiles framegen_engine_stub.c instead, which reports
* compositor build (no framegen tree) compiles framegen_engine_stub.c instead, which reports
* "engines not built in" and generates nothing.
*
* Every function runs on the compositor thread.
@@ -1,5 +1,5 @@
/* Stand-in for framegen_engine.cpp when the compositor is built on its own (standalone/, no
* winlator tree, no dxbc). Every engine is reported unavailable; nothing is generated. */
* framegen tree, no dxbc). Every engine is reported unavailable; nothing is generated. */
#include "framegen_engine.h"
const void *fge_probe(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysicalDevice pd,
@@ -7,20 +7,20 @@
#include "framegen_bridge.h"
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetFrameGenEngine(JNIEnv *env, jclass clazz, jint kind) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFrameGenEngine(JNIEnv *env, jclass clazz, jint kind) {
(void)env; (void)clazz;
vkp_framegen_set_engine((int)kind);
}
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetFrameGenArmed(JNIEnv *env, jclass clazz,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFrameGenArmed(JNIEnv *env, jclass clazz,
jboolean armed, jint multiplier) {
(void)env; (void)clazz;
vkp_framegen_set_armed(armed ? 1 : 0, (int)multiplier);
}
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetLsfgCachePath(JNIEnv *env, jclass clazz, jstring path) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLsfgCachePath(JNIEnv *env, jclass clazz, jstring path) {
(void)clazz;
if (!path) { vkp_framegen_set_lsfg_cache_path(NULL); return; }
const char *c = (*env)->GetStringUTFChars(env, path, NULL);
@@ -29,34 +29,34 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetLsfgCachePath(JNI
}
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetFrameGenTuning(JNIEnv *env, jclass clazz,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFrameGenTuning(JNIEnv *env, jclass clazz,
jfloat flowScale, jfloat refreshHz) {
(void)env; (void)clazz;
vkp_framegen_set_tuning((float)flowScale, (float)refreshHz);
}
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetWinFgTuning(JNIEnv *env, jclass clazz,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetWinFgTuning(JNIEnv *env, jclass clazz,
jint model, jint perfPreset) {
(void)env; (void)clazz;
vkp_framegen_set_winfg_tuning((int)model, (int)perfPreset);
}
JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeFrameGenProblem(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeFrameGenProblem(JNIEnv *env, jclass clazz) {
(void)env; (void)clazz;
return (jint)vkp_framegen_problem();
}
JNIEXPORT jstring JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeFrameGenCapsReason(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeFrameGenCapsReason(JNIEnv *env, jclass clazz) {
(void)clazz;
const char *r = vkp_framegen_caps_reason();
return (*env)->NewStringUTF(env, r ? r : "");
}
JNIEXPORT jfloatArray JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeFrameGenStats(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeFrameGenStats(JNIEnv *env, jclass clazz) {
(void)clazz;
float st[6] = {0.f, 0.f, 0.f, 0.f, -1.f, -1.f};
vkp_framegen_stats(st);
@@ -8,7 +8,7 @@
/* SPIR-V, pre-compiled (glslangValidator -V hdr_encode.frag --vn hdr_encode_code) and committed next
* to the GLSL, like every other shader of the compositor: the NDK build compiles no shaders. The quad
* is the effect chain's upscale.vert (same bytes, from app/src/main/cpp/winlator). */
* is the effect chain's upscale.vert (same bytes, from app/src/main/cpp/framegen). */
/* The headers define plain (external) arrays and effects_chain.c includes upscale_vert.h too: this
* translation unit gets its own copy under another name, so the link sees no duplicate symbol. */
#define upscale_vert_code hdrc_upscale_vert_code
+15 -15
View File
@@ -1,4 +1,4 @@
/* Layer mode: the scene on a small set of ASurfaceControl layers — see sc_layer.h and
/* Layer mode: the scene on a small set of ASurfaceControl layers - see sc_layer.h and
* ZERO_COPY_SPIKE.md. The ASurfaceControl/ASurfaceTransaction API is dlsym'd from libandroid.so
* (API 29+), the same way the X11 renderers' scanout code does it, so the library still loads on
* older devices and the prototype stays off every other path. */
@@ -572,7 +572,7 @@ static int layer_geometry(int w, int h, int scene_w, int scene_h, int r[8]) {
/* The GAME layer's last placement, kept across SurfaceControl retires and swaps (l->geo_valid is
* per-SurfaceControl and is cleared by both). This is the src -> dst the display actually works
* with, and it is what decides whether a second layer is affordable — see overlay_affordable(). */
* with, and it is what decides whether a second layer is affordable - see overlay_affordable(). */
static ARect g_game_src, g_game_dst;
static int g_game_geo_known;
@@ -610,8 +610,8 @@ static void apply_frame_rate(ASurfaceTransaction *tx, struct layer *l) {
}
/* ---- colour (HDR, banner_color.h) -------------------------------------------------------------
* The layer is told what its buffer's pixels MEAN — the dataspace (BT2020_PQ for an HDR10 frame) and
* the game's mastering / content-light metadata — in the same transaction as the buffer, so the
* The layer is told what its buffer's pixels MEAN - the dataspace (BT2020_PQ for an HDR10 frame) and
* the game's mastering / content-light metadata - in the same transaction as the buffer, so the
* display never shows an HDR frame decoded as sRGB or the other way round. A layer that has never been
* tagged is never touched: sessions without an HDR description make no colour call at all. */
/* How much of the screen the layer's picture covers ("covers 80% of the screen (1920x1080 of 2400x1080)"):
@@ -722,19 +722,19 @@ static void log_layer_count(void) {
* marked for a swap and the swap rides the NEXT frame: a fresh SurfaceControl is created here, the
* frame is put on it, and the old one is hidden and unparented IN THE SAME TRANSACTION. Because
* SurfaceFlinger applies a transaction atomically there is never a composited frame with neither
* layer on it — no black frame, and no dropped frame beyond the one layer creation. The outgoing
* layer on it - no black frame, and no dropped frame beyond the one layer creation. The outgoing
* buffer is released through the OLD SurfaceControl's callback, which also releases it.
*
* ⚠️ MEASURED 2026-09-14, and the news is bad: on the Pocket FIT this does NOT bring hardware
* MEASURED 2026-09-14, and the news is bad: on the Pocket FIT this does NOT bring hardware
* composition back. The swap fires 6 ms after the overlay goes, SurfaceFlinger really does hand out
* a new layer (its id changes), the game never drops a frame — and the composer still reports
* a new layer (its id changes), the game never drops a frame - and the composer still reports
* `DEVICE/CLIENT` 24 s later. Neither does dropping the layer path entirely and re-creating the
* SurfaceControl after a gap. The ONE thing that clears it is HOME + resume, which re-creates the
* app's whole window and SurfaceView (`VRI[XServerDisplayActivity]#0` becomes `#4`) — so the sticky
* app's whole window and SurfaceView (`VRI[XServerDisplayActivity]#0` becomes `#4`) - so the sticky
* client-composition state belongs to the PARENT surface (or the display), not to this child layer.
* The phase-4 note that "only re-creating the GAME layer's SurfaceControl clears it" was inferred
* from HOME + resume and is wrong; see sc_layer.h. The swap is kept because it is free and correct
* and the mechanism may differ on hardware that does not rotate every layer — but do not claim it
* and the mechanism may differ on hardware that does not rotate every layer - but do not claim it
* restores DEVICE composition, and do not log as if it did.
*
* Returns the old SurfaceControl (the caller must add retire_ops for it to the same transaction and
@@ -753,7 +753,7 @@ static ASurfaceControl *swap_sc_begin(struct layer *l) {
ASurfaceControl *old = l->sc;
l->sc = fresh;
/* These describe the SurfaceControl, not the layer: the new one carries none of them yet.
* cur_slot / cur_token are deliberately NOT cleared — they name the buffer still on the OLD
* cur_slot / cur_token are deliberately NOT cleared - they name the buffer still on the OLD
* SurfaceControl, which is what the caller passes to add_complete_on as the one being replaced. */
l->shown = 0;
l->geo_valid = 0;
@@ -848,7 +848,7 @@ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, voi
apply_colour(tx, l, color);
if (!l->shown) api.setVisibility(tx, l->sc, ASC_VISIBILITY_SHOW);
if (old) retire_ops(tx, old);
/* Same buffer, new SurfaceControl: it is NOT free, so no release is reported for it — the swap
/* Same buffer, new SurfaceControl: it is NOT free, so no release is reported for it - the swap
* moved it rather than taking it off screen. */
if (add_complete_on(tx, l, old ? old : l->sc, l->cur_slot,
l->cur_token == token ? NULL : l->cur_token, old ? 1 : 0) != 0) {
@@ -977,19 +977,19 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct
/* ---- is a SECOND display layer affordable on this display? -----------------------------------
* MEASURED on the Pocket FIT (2026-09-14, three times): while a second layer is up, this hardware
* composer hands the WHOLE frame back to the GPU (`DEVICE/CLIENT`), and nothing short of re-creating
* the app's window brings it back — so on this display the overlay layer costs more than it saves,
* the app's window brings it back - so on this display the overlay layer costs more than it saves,
* every time, for the rest of the session. Where it costs nothing it is still the better path (the
* game keeps copy-free frames with a window on top), so this is a GATE, not a removal.
*
* The gate is read from what the layer path actually knows, never from a device or panel list:
* - rotation: vkp_surface_rotation_degrees(), i.e. VkSurfaceCapabilitiesKHR::currentTransform —
* - rotation: vkp_surface_rotation_degrees(), i.e. VkSurfaceCapabilitiesKHR::currentTransform -
* what the presentation engine says it does to every layer we hand it;
* - scale: the GAME layer's own src -> dst rectangles, the ones passed to setGeometry.
* Rotated AND scaled is the combination that was measured to cost composition (the DPU's rotator
* has to take a scaled source); either alone, or neither, is left as it was.
*
* Why not just measure the composition type after raising the layer and back out if it comes back
* CLIENT — which would beat predicting? Because no app can read it. The composition type lives in
* CLIENT - which would beat predicting? Because no app can read it. The composition type lives in
* the Composer HAL; ASurfaceTransactionStats exposes latch time and fences and nothing else, and
* the only place the value is published is `dumpsys android.hardware.graphics.composer3.IComposer`,
* which needs android.permission.DUMP and string-parsing. It would also arrive at least a frame
@@ -1006,7 +1006,7 @@ static int overlay_affordable(int *deg, int sw[2], int dw[2]) {
}
/* -1 = not decided yet, 0 = raising the overlay, 1 = declining it. Logged on every change, so a
* tester's log says why they are seeing the copy path instead of two layers — and says it again if
* tester's log says why they are seeing the copy path instead of two layers - and says it again if
* the placement changes the answer (e.g. a fullscreen mode that stops scaling the game). */
static int g_overlay_declined = -1;
+14 -14
View File
@@ -2,8 +2,8 @@
#define SC_LAYER_H
/*
* Layer mode (BANNER_WAYLAND_ZERO_COPY=1): the scene is handed to SurfaceFlinger as a small,
* deliberately ordered SET of Android display layers — ASurfaceControl children of the
* compositor's SurfaceView — instead of being blitted into the compositor's own swapchain.
* deliberately ordered SET of Android display layers - ASurfaceControl children of the
* compositor's SurfaceView - instead of being blitted into the compositor's own swapchain.
*
* app window ......... Compose UI, the in-game drawer, the HUD, the on-screen controls and
* the pointer arrow: ordinary Android views, ALWAYS above everything
@@ -21,21 +21,21 @@
*
* MEASURED on the Pocket FIT (Adreno 750, portrait panel + landscape session, so every layer is
* ROT_90 + scaled), `dumpsys android.hardware.graphics.composer3.IComposer/default`: one layer is
* `composition: DEVICE/DEVICE`, effects on the layer keep it DEVICE/DEVICE — but a SECOND layer
* `composition: DEVICE/DEVICE`, effects on the layer keep it DEVICE/DEVICE - but a SECOND layer
* flips the whole frame to `DEVICE/CLIENT`. It does not come back when the overlay goes away.
* The overlay layer is retired rather than hidden for that reason, but the fallback outlives it.
* The likely mechanism is the DPU's rotator budget (one rotated+scaled layer), not the layer count
* as such, so a device or orientation needing no rotation may well take both on the DPU. Even in
* client composition the overlay layer is not a loss — SurfaceFlinger does the one blit the
* compositor would have done — but the hardware-composition win is only real for one layer.
* client composition the overlay layer is not a loss - SurfaceFlinger does the one blit the
* compositor would have done - but the hardware-composition win is only real for one layer.
*
* COMPOSITION RECOVERY (sc_layer.c, swap_sc_begin): retiring the overlay arms the game layer, and
* the NEXT frame is presented on a brand-new SurfaceControl while the old one is hidden and
* unparented in the SAME transaction. SurfaceFlinger applies a transaction atomically, so no
* composited frame is ever missing the game — no black frame, no dropped frame beyond the layer
* composited frame is ever missing the game - no black frame, no dropped frame beyond the layer
* creation itself. One `layer` line is written when it happens.
*
* ⚠️ It does NOT restore hardware composition on this panel, and that was measured, not assumed
* It does NOT restore hardware composition on this panel, and that was measured, not assumed
* (2026-09-14, Wizardry on the game layer + Wine's Task Manager on the overlay, reproduced twice):
* - two layers -> DEVICE/CLIENT
* - overlay retired + fresh game SurfaceControl -> still DEVICE/CLIENT, 24 s later too
@@ -43,12 +43,12 @@
* - drawer opened and closed, no second window -> DEVICE/DEVICE (control: the drawer is not the cause)
* - HOME + resume -> DEVICE/DEVICE
* HOME + resume re-creates the app's whole window and SurfaceView (`VRI[XServerDisplayActivity]#0`
* becomes `#4`), not just this child layer — so the sticky state belongs to the parent surface or
* becomes `#4`), not just this child layer - so the sticky state belongs to the parent surface or
* the display. Re-creating the parent would cost a real black frame and a swapchain rebuild, which
* is worse than the few percent of GPU that client composition costs. The cure that actually keeps
* the win is PREVENTION, and it IS implemented: sc_layer_overlay_affordable() declines the second
* layer when the game layer is both rotated and scaled, and the window above the game goes down the
* copy path instead (what the pre-phase-4 code did) — one blit SurfaceFlinger would have done
* copy path instead (what the pre-phase-4 code did) - one blit SurfaceFlinger would have done
* anyway, and the session keeps DEVICE composition throughout. Where a second layer costs nothing,
* nothing changes.
*
@@ -56,7 +56,7 @@
* - the game's own gralloc buffer (ahb_swapchain.c, true zero-copy: no copy anywhere);
* - one blit of the game's frame into a compositor-allocated AHardwareBuffer (sc_layer_present);
* - with screen effects on, the compositor pass's result blitted into such a buffer
* (sc_layer_present_pass) — the game STAYS on its layer while a Look is applied, and the
* (sc_layer_present_pass) - the game STAYS on its layer while a Look is applied, and the
* scene -> output mapping is done by the display (setGeometry) instead of a second GPU blit.
* The compositor never alpha-blends (it composes with blits, which overwrite), so every layer is
* marked OPAQUE and the overlay layer is cropped to the window it carries: the picture is the
@@ -141,9 +141,9 @@ void sc_layer_set_frame_rate(float fps);
* layer, not after: on a display where the answer is no, the whole scene must go down the copy path
* from the start, or the game layer would be shown and hidden again on every frame.
*
* The answer is computed from what the layer path knows — the rotation the presentation engine
* The answer is computed from what the layer path knows - the rotation the presentation engine
* applies to everything we hand it (VkSurfaceCapabilitiesKHR::currentTransform) and the game
* layer's own src -> dst rectangles — and never from a device or panel allowlist. ROTATED AND
* layer's own src -> dst rectangles - and never from a device or panel allowlist. ROTATED AND
* SCALED is the combination measured to cost hardware composition (below); either alone is fine.
* The reason is written to the session log once, and again if the answer changes. */
int sc_layer_overlay_affordable(void);
@@ -156,7 +156,7 @@ int sc_layer_present_overlay(struct vkp_image *src, const int geo[8]);
/* The scene is not a single fullscreen window this frame: hide every layer that is up. */
void sc_layer_hide(void);
/* Only the overlay layer: nothing is above the game any more. The game keeps its layer, but its
* SurfaceControl is swapped for a fresh one on the next frame (composition recovery, above — which
* SurfaceControl is swapped for a fresh one on the next frame (composition recovery, above - which
* on this panel does not in fact win hardware composition back; see the measurement there). */
void sc_layer_hide_overlay(void);
@@ -164,7 +164,7 @@ void sc_layer_hide_overlay(void);
void sc_layer_window_gone(void);
/* Frames the compositor put on a layer through one of its own buffers (a blit or the effects
* pass) since the last call — the zero-copy frames ahb_swapchain.c counts are NOT included.
* pass) since the last call - the zero-copy frames ahb_swapchain.c counts are NOT included.
* For the 10 s summary. */
unsigned sc_layer_frames_take(void);
/* Frames dropped since the last call because every layer buffer was still held by the display (the
@@ -1,167 +0,0 @@
/* Compositor-side dmabuf import into Turnip (see vk_import.h). */
#define _POSIX_C_SOURCE 200809L
#include "vk_import.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <vulkan/vulkan.h>
#define MOD_INVALID 0x00ffffffffffffffULL
static int g_ready = -1; /* -1 = untried, 0 = ok, 1 = init failed */
static VkInstance g_inst;
static VkPhysicalDevice g_pd;
static VkDevice g_dev;
static PFN_vkGetMemoryFdPropertiesKHR p_getMemFdProps;
static int has_ext(VkExtensionProperties *e, uint32_t n, const char *name) {
for (uint32_t i = 0; i < n; i++)
if (!strcmp(e[i].extensionName, name)) return 1;
return 0;
}
static int vk_init(void) {
if (g_ready >= 0) return g_ready == 0 ? 0 : -1;
VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "spike-compositor-import",
.apiVersion = VK_API_VERSION_1_1};
VkInstanceCreateInfo ici = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &app};
if (vkCreateInstance(&ici, NULL, &g_inst) != VK_SUCCESS) {
fprintf(stderr, "[imp] vkCreateInstance failed\n");
g_ready = 1; return -1;
}
uint32_t npd = 0;
vkEnumeratePhysicalDevices(g_inst, &npd, NULL);
if (!npd) { g_ready = 1; return -1; }
VkPhysicalDevice pds[8];
if (npd > 8) npd = 8;
vkEnumeratePhysicalDevices(g_inst, &npd, pds);
g_pd = pds[0]; /* Turnip is forced via VK_ICD_FILENAMES */
const char *want[] = {"VK_KHR_external_memory_fd",
"VK_EXT_external_memory_dma_buf",
"VK_EXT_image_drm_format_modifier",
"VK_KHR_image_format_list"};
uint32_t ne = 0;
vkEnumerateDeviceExtensionProperties(g_pd, NULL, &ne, NULL);
VkExtensionProperties *exts = calloc(ne, sizeof(*exts));
vkEnumerateDeviceExtensionProperties(g_pd, NULL, &ne, exts);
for (unsigned i = 0; i < 4; i++)
if (!has_ext(exts, ne, want[i]))
fprintf(stderr, "[imp] WARNING device missing %s\n", want[i]);
free(exts);
float prio = 1.0f;
VkDeviceQueueCreateInfo qci = {.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = 0, .queueCount = 1,
.pQueuePriorities = &prio};
VkDeviceCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.queueCreateInfoCount = 1, .pQueueCreateInfos = &qci,
.enabledExtensionCount = 4, .ppEnabledExtensionNames = want};
if (vkCreateDevice(g_pd, &dci, NULL, &g_dev) != VK_SUCCESS) {
fprintf(stderr, "[imp] vkCreateDevice failed\n");
g_ready = 1; return -1;
}
p_getMemFdProps = (PFN_vkGetMemoryFdPropertiesKHR)vkGetDeviceProcAddr(
g_dev, "vkGetMemoryFdPropertiesKHR");
g_ready = 0;
fprintf(stderr, "[imp] Vulkan import device ready (Turnip)\n");
return 0;
}
static VkFormat drm_to_vk(uint32_t drm) {
/* XR24/AR24 (XRGB/ARGB8888, little-endian bytes B,G,R,X) -> BGRA8. */
return VK_FORMAT_B8G8R8A8_UNORM;
}
int vk_import_dmabuf(int fd, uint32_t drm_format, uint64_t modifier, int w, int h,
uint32_t stride, uint32_t offset) {
if (modifier == MOD_INVALID) {
fprintf(stderr, "[imp] skip: INVALID modifier (needs implicit path)\n");
return -1;
}
if (vk_init() != 0) return -1;
VkSubresourceLayout plane = {.offset = offset, .rowPitch = stride};
VkImageDrmFormatModifierExplicitCreateInfoEXT modInfo = {
.sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT,
.drmFormatModifier = modifier,
.drmFormatModifierPlaneCount = 1,
.pPlaneLayouts = &plane};
VkExternalMemoryImageCreateInfo extImg = {
.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
.pNext = &modInfo,
.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT};
VkImageCreateInfo ici = {
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.pNext = &extImg,
.imageType = VK_IMAGE_TYPE_2D,
.format = drm_to_vk(drm_format),
.extent = {w, h, 1},
.mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
VkImage image;
VkResult r = vkCreateImage(g_dev, &ici, NULL, &image);
if (r != VK_SUCCESS) {
fprintf(stderr, "[imp] vkCreateImage(drm_modifier) -> %d\n", r);
return -1;
}
int dupfd = dup(fd);
uint32_t allowed_types = 0xffffffff;
if (p_getMemFdProps) {
VkMemoryFdPropertiesKHR fp = {.sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR};
if (p_getMemFdProps(g_dev, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
dupfd, &fp) == VK_SUCCESS)
allowed_types = fp.memoryTypeBits;
}
VkMemoryRequirements req;
vkGetImageMemoryRequirements(g_dev, image, &req);
uint32_t bits = req.memoryTypeBits & allowed_types;
int type_idx = -1;
for (int i = 0; i < 32; i++)
if (bits & (1u << i)) { type_idx = i; break; }
if (type_idx < 0) {
fprintf(stderr, "[imp] no compatible memory type\n");
vkDestroyImage(g_dev, image, NULL); close(dupfd); return -1;
}
VkImportMemoryFdInfoKHR imp = {
.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR,
.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
.fd = dupfd};
VkMemoryDedicatedAllocateInfo ded = {
.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO,
.pNext = &imp, .image = image};
VkMemoryAllocateInfo mai = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.pNext = &ded,
.allocationSize = req.size,
.memoryTypeIndex = (uint32_t)type_idx};
VkDeviceMemory mem;
r = vkAllocateMemory(g_dev, &mai, NULL, &mem);
if (r != VK_SUCCESS) {
fprintf(stderr, "[imp] vkAllocateMemory(import fd) -> %d\n", r);
vkDestroyImage(g_dev, image, NULL); close(dupfd); return -1;
}
r = vkBindImageMemory(g_dev, image, mem, 0);
if (r != VK_SUCCESS) {
fprintf(stderr, "[imp] vkBindImageMemory -> %d\n", r);
vkFreeMemory(g_dev, mem, NULL); vkDestroyImage(g_dev, image, NULL);
return -1;
}
fprintf(stderr,
"[imp] *** IMPORTED dmabuf -> VkImage OK (memType %d, size %llu) "
"==> compositor can turn game frames into VkImages\n",
type_idx, (unsigned long long)req.size);
/* import took ownership of dupfd; free our objects */
vkFreeMemory(g_dev, mem, NULL);
vkDestroyImage(g_dev, image, NULL);
return 0;
}
@@ -1,13 +0,0 @@
#ifndef VK_IMPORT_H
#define VK_IMPORT_H
#include <stdint.h>
/*
* Import a received dmabuf into the compositor's own Vulkan (Turnip) device as a
* VkImage and bind its memory. This is the M4-import de-risk: if we can import +
* bind, sampling/blitting it to screen is standard Vulkan. Non-fatal: returns 0
* on success, negative on failure, and logs details. Does NOT consume `fd`
* (dups internally).
*/
int vk_import_dmabuf(int fd, uint32_t drm_format, uint64_t modifier,
int w, int h, uint32_t stride, uint32_t offset);
#endif
+1 -1
View File
@@ -2,7 +2,7 @@
#define VK_LOADER_H
/*
* Loads Turnip via adrenotools (the same path the app uses for the guest renderer)
* and resolves every Vulkan entry point through its vkGetInstanceProcAddr — NOT the
* and resolves every Vulkan entry point through its vkGetInstanceProcAddr - NOT the
* process-default system Adreno driver, which lacks VK_EXT_image_drm_format_modifier /
* dmabuf import. All compositor Vulkan calls go through g_vk.* .
*/
+106 -8
View File
@@ -1,4 +1,4 @@
/* Android-surface render backend — see vk_present.h. Uses Turnip via vk_loader
/* Android-surface render backend - see vk_present.h. Uses Turnip via vk_loader
* (g_vk.*), not the process-default system Adreno driver. */
#define _POSIX_C_SOURCE 200809L
#include "vk_present.h"
@@ -118,7 +118,7 @@ static VkResult timed_wait(VkFence fence, uint64_t timeout) {
}
/* The scene image (effects path only): every draw composited 1:1 at scene size, the input of the
* screen-effect chain (effects_chain.c) — and of frame generation once that lands. Recreated on a
* screen-effect chain (effects_chain.c) - and of frame generation once that lands. Recreated on a
* scene size change; frames are fenced, so never while the GPU reads it. */
static struct { VkImage img; VkDeviceMemory mem; int w, h; } g_scene;
@@ -175,7 +175,7 @@ static struct {
static int64_t g_swap_retry_at_ns;
static int g_swap_fail_logged;
/* Implemented in waylandcomp_jni.c — notifies Java (dismiss launch overlay). */
/* Implemented in waylandcomp_jni.c - notifies Java (dismiss launch overlay). */
extern void banner_on_first_frame(void);
static char g_gpu_name[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];
@@ -311,7 +311,7 @@ static const char *align_name(int a) {
}
}
/* Mirror of ViewTransformation.update(outer = output, inner = scene, mode, alignment) — keep the
/* Mirror of ViewTransformation.update(outer = output, inner = scene, mode, alignment) - keep the
* arithmetic identical: the app maps touch input through that class with the same inputs, so any
* difference here puts the pointer beside what it is pointing at. OFF and FIT are both an
* aspect-preserving letterbox (OFF only differs in the app's fullscreen gates); TOP/BOTTOM confine
@@ -379,7 +379,7 @@ static int has_ext(VkExtensionProperties *e, uint32_t n, const char *name) {
static int dev_init(void) {
if (g_dev_state != 0) return g_dev_state == 1 ? 0 : -1;
/* Load Turnip (adrenotools) and its entry points — NOT the system driver. */
/* Load Turnip (adrenotools) and its entry points - NOT the system driver. */
if (vk_loader_open(g_driver_path, g_library_name, g_native_lib_dir) != 0) {
LOGE("present: vk_loader_open failed"); g_dev_state = -1; return -1;
}
@@ -656,7 +656,7 @@ static int swap_init_locked(void) {
/* Remembered for the layer path: currentTransform is the rotation the presentation engine (and
* therefore SurfaceFlinger and the DPU) applies to everything we put on this display. Anything
* but IDENTITY means every display layer we hand over is a ROTATED layer. sc_layer.c needs that
* to decide whether a second layer is affordable — see sc_layer_present_overlay(). */
* to decide whether a second layer is affordable - see sc_layer_present_overlay(). */
g_surface_transform = caps.currentTransform;
g_surface_transform_known = 1;
g_caps_extent = caps.currentExtent; /* check_surface_changed() compares against this */
@@ -724,7 +724,7 @@ static int swap_init_locked(void) {
/* Use IDENTITY preTransform when the surface supports it. Setting preTransform =
* currentTransform tells the presentation engine our content is ALREADY pre-rotated by
* that amount — but our blit doesn't rotate, so on a device whose surface reports a 90°
* that amount - but our blit doesn't rotate, so on a device whose surface reports a 90°
* currentTransform the display then rotates our upright frame 90° (game shows sideways).
* IDENTITY = "don't rotate what I present", which is what we want. */
VkSurfaceTransformFlagBitsKHR pretrans =
@@ -874,7 +874,7 @@ const char *vkp_modifier_name(uint64_t modifier) {
}
/* Can the driver create the image vkp_image_import_dmabuf() creates for this format+modifier
* (2D, DRM_FORMAT_MODIFIER tiling, TRANSFER_SRC, dma-buf memory) — and import it? */
* (2D, DRM_FORMAT_MODIFIER tiling, TRANSFER_SRC, dma-buf memory) - and import it? */
static int modifier_importable(VkFormat fmt, uint64_t modifier) {
if (!g_vk.GetPhysicalDeviceImageFormatProperties2) return 1; /* can't ask; the create will tell */
VkPhysicalDeviceExternalImageFormatInfo ext = {
@@ -1910,6 +1910,104 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) {
return 0;
}
/* A client's GPU cursor read back to the CPU, for the app's pointer overlay (compositor.c,
* wl_pointer.set_cursor). wlroots hands a nested compositor its cursor as a dma-buf from the parent's
* formats - on Adreno usually UBWC, which the CPU cannot read - so it is blitted into a small linear,
* host-visible image first. Rare (a cursor changes shape, it is not redrawn per frame) and waited
* for on the CPU. out: w*h pixels, B8G8R8A8 in memory = 0xAARRGGBB ints. 0 = ok. */
static struct vkp_image *g_rb; /* the readback image, kept for the next shape */
int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) {
if (!src || !out || src->w <= 0 || src->h <= 0 || src->w * src->h > max_px ||
g_dev_state == -2 || dev_init() != 0)
return -1;
if (g_rb && (g_rb->w != src->w || g_rb->h != src->h)) { vkp_image_destroy(g_rb); g_rb = NULL; }
if (!g_rb) {
struct vkp_image *img = calloc(1, sizeof(*img));
if (!img) return -1;
img->w = src->w; img->h = src->h;
VkImageCreateInfo ici = {
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
.format = VK_FORMAT_B8G8R8A8_UNORM, .extent = {src->w, src->h, 1}, .mipLevels = 1,
.arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_LINEAR,
.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
if (g_vk.CreateImage(g_dev, &ici, NULL, &img->image) != VK_SUCCESS) { free(img); return -1; }
VkMemoryRequirements req;
g_vk.GetImageMemoryRequirements(g_dev, img->image, &req);
int idx = memory_type(req.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VkMemoryAllocateInfo mai = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = req.size, .memoryTypeIndex = (uint32_t)idx};
if (idx < 0 || g_vk.AllocateMemory(g_dev, &mai, NULL, &img->mem) != VK_SUCCESS) {
g_vk.DestroyImage(g_dev, img->image, NULL); free(img); return -1;
}
g_vk.BindImageMemory(g_dev, img->image, img->mem, 0);
VkImageSubresource subr = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0};
VkSubresourceLayout lay;
g_vk.GetImageSubresourceLayout(g_dev, img->image, &subr, &lay);
img->offset = lay.offset;
img->row_pitch = lay.rowPitch;
if (g_vk.MapMemory(g_dev, img->mem, 0, req.size, 0, &img->map) != VK_SUCCESS) {
vkp_image_destroy(img); return -1;
}
g_rb = img;
}
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VkCommandBuffer cmd = g_cmds[0];
g_vk.ResetCommandBuffer(cmd, 0);
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
g_vk.BeginCommandBuffer(cmd, &bi);
VkImageMemoryBarrier acq[2] = {
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
.srcQueueFamilyIndex = src->dmabuf ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED,
.image = src->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT},
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_rb->image, .subresourceRange = range,
.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}};
g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, NULL, 0, NULL, 2, acq);
VkImageBlit blit = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.srcOffsets = {{0, 0, 0}, {src->w, src->h, 1}},
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.dstOffsets = {{0, 0, 0}, {src->w, src->h, 1}}};
g_vk.CmdBlitImage(cmd, src->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
g_rb->image, VK_IMAGE_LAYOUT_GENERAL, 1, &blit, VK_FILTER_NEAREST);
VkImageMemoryBarrier done[2] = {
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_rb->image, .subresourceRange = range,
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_HOST_READ_BIT},
/* the client's buffer goes back to it */
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = src->dmabuf ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_IGNORED,
.image = src->image, .subresourceRange = range, .srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT}};
g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
0, 0, NULL, 0, NULL, 2, done);
g_vk.EndCommandBuffer(cmd);
VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &cmd};
g_vk.ResetFences(g_dev, 1, &g_fence);
VkResult qr = g_vk.QueueSubmit(g_queue, 1, &si, g_fence);
if (qr != VK_SUCCESS) {
if (qr == VK_ERROR_DEVICE_LOST) device_lost("cursor readback");
return -1;
}
VkResult fr = timed_wait(g_fence, 1000000000ULL);
if (fr == VK_ERROR_DEVICE_LOST) { device_lost("cursor readback"); return -1; }
if (fr != VK_SUCCESS) return -1;
for (int y = 0; y < src->h; y++)
memcpy(out + (size_t)y * src->w, (const uint8_t *)g_rb->map + g_rb->offset + (size_t)y * g_rb->row_pitch,
(size_t)src->w * 4);
return 0;
}
/* ---------------------------------------------------------------- the compositor pass, off-screen
*
* Layer mode with screen effects on: the game's frame still has to go through the compositor pass
@@ -7,12 +7,12 @@
* Owns a Turnip VkDevice + a swapchain on the SurfaceView's ANativeWindow. Each
* client buffer becomes an image (a dmabuf from winewayland's Vulkan WSI is
* imported zero-copy; a wl_shm buffer is copied into a host-visible image), and
* every frame blits the whole scene — desktop, windows, subsurfaces — in order.
* every frame blits the whole scene - desktop, windows, subsurfaces - in order.
*/
// Set the Turnip driver to load (adrenotools). Call before the first frame.
// NULL args -> the backend falls back to the system libvulkan (dmabuf import will
// likely fail — Adreno lacks drm_format_modifier). driver_path ends with '/'.
// likely fail - Adreno lacks drm_format_modifier). driver_path ends with '/'.
void vk_present_set_driver(const char *driver_path, const char *library_name,
const char *native_lib_dir);
@@ -146,6 +146,9 @@ int vkp_pass_begin_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, i
* wait_fd: a sync_file the copy must wait for before writing dst (the display's release fence of that
* buffer), -1 = none. Always consumed (handed to the GPU, or waited for and closed). */
int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd);
/* A client's GPU cursor (an imported dma-buf, often UBWC) read back as w*h 0xAARRGGBB pixels through a
* linear host-visible copy; waited for on the CPU. Compositor thread, between frames. 0 = ok. */
int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px);
/* Refresh the scene -> output mapping for this scene size without presenting (creates the
* swapchain if needed, since the mapping is in output pixels). 0 = mapping valid. */
int vkp_update_map(int scene_w, int scene_h);
@@ -157,7 +160,7 @@ int vkp_map_rect(int img_w, int img_h, int scene_w, int scene_h, int out[8]);
/* The output window frames go to (NULL = none); compositor thread. */
/* Degrees the display rotates every layer we present by (0/90/180/270), -1 before the swapchain
* exists. From VkSurfaceCapabilitiesKHR::currentTransform, i.e. what the presentation engine says
* it does — never a device allowlist. The layer path uses it to decide whether a SECOND display
* it does - never a device allowlist. The layer path uses it to decide whether a SECOND display
* layer is affordable on this display (sc_layer_present_overlay). */
int vkp_surface_rotation_degrees(void);
@@ -23,6 +23,8 @@
extern int banner_wayland_run(void);
extern void banner_wayland_send_pointer(int action, int x, int y);
extern void banner_wayland_send_touch(int action, int pointer_id, int x, int y);
extern int banner_cursor_snapshot(int *out, int cap);
extern void banner_wayland_send_key(int evdev, int state);
extern void banner_wayland_send_scene_input(int type, int a, int b);
extern void banner_wayland_vsync(int64_t frame_time_ns);
@@ -53,7 +55,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
g_jvm = vm;
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) == JNI_OK) {
jclass c = (*env)->FindClass(env, "com/steamdeck/launcher/wayland/WaylandCompositor");
jclass c = (*env)->FindClass(env, "com/droiddeck/launcher/wayland/WaylandCompositor");
if (c) {
g_compositor_cls = (*env)->NewGlobalRef(env, c);
g_on_first_frame = (*env)->GetStaticMethodID(env, g_compositor_cls,
@@ -147,7 +149,7 @@ void banner_on_pointer_lock(int locked, int x, int y) {
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
}
/* A program copied text (UTF-8, not NUL-terminated for the app's sake — a byte[] so emoji and
/* A program copied text (UTF-8, not NUL-terminated for the app's sake - a byte[] so emoji and
* NULs survive JNI). Compositor thread. */
void banner_on_clipboard_text(const char *utf8, int len) {
JNIEnv *env;
@@ -222,14 +224,14 @@ static void start_thread(void) {
/* The compositor thread's tid, or 0 before it starts. The app opens a PerformanceHintManager session
* on it and reports each presented frame's interval, so the governor sees a frame that ran long. */
JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeCompositorTid(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCompositorTid(JNIEnv *env, jclass clazz) {
(void)env; (void)clazz;
return (jint)g_comp_tid;
}
/* Headless start (no output window) — used for bring-up tests. */
/* Headless start (no output window) - used for bring-up tests. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeStart(JNIEnv *env, jclass clazz,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStart(JNIEnv *env, jclass clazz,
jstring xdgRuntimeDir) {
set_runtime_dir(env, xdgRuntimeDir);
start_thread();
@@ -246,7 +248,7 @@ static char *dup_jstr(JNIEnv *env, jstring s) {
/* Start with a real output Surface + the container's Turnip driver (adrenotools).
* Frames committed by clients are composited to this Surface via Turnip. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeStartWithSurface(
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStartWithSurface(
JNIEnv *env, jclass clazz, jobject surface, jstring xdgRuntimeDir,
jstring driverPath, jstring libraryName, jstring nativeLibDir) {
set_runtime_dir(env, xdgRuntimeDir);
@@ -266,34 +268,55 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeStartWithSurface(
/* Inject a pointer event from the Android SurfaceView touch listener (UI thread).
* action: 0=down 1=move 2=up; x/y in output space (0..1919, 0..1079). */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSendPointer(
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendPointer(
JNIEnv *env, jclass clazz, jint action, jint x, jint y) {
banner_wayland_send_pointer(action, x, y);
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendTouch(
JNIEnv *env, jclass clazz, jint action, jint pointer_id, jint x, jint y) {
banner_wayland_send_touch(action, pointer_id, x, y);
}
/* The client's pointer image and whether it wants one at all (wl_pointer.set_cursor).
* out = [serial, hidden, w, h, hotspotX, hotspotY, ARGB pixels...]; returns ints written, 0 if the
* array is too small. The app polls the serial and only rebuilds its overlay when it changes. */
JNIEXPORT jint JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot(
JNIEnv *env, jclass clazz, jintArray out) {
if (!out) return 0;
jint cap = (*env)->GetArrayLength(env, out);
jint *buf = (*env)->GetIntArrayElements(env, out, NULL);
if (!buf) return 0;
int n = banner_cursor_snapshot((int *)buf, (int)cap);
(*env)->ReleaseIntArrayElements(env, out, buf, 0);
return n;
}
/* Inject a key event. evdev = Linux input keycode (KEY_A=30…); state 1=down 0=up. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSendKey(
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendKey(
JNIEnv *env, jclass clazz, jint evdev, jint state) {
banner_wayland_send_key(evdev, state);
}
/* App X-server input in scene (virtual desktop) coordinates; see banner_wayland_send_scene_input. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSendSceneInput(
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendSceneInput(
JNIEnv *env, jclass clazz, jint type, jint a, jint b) {
banner_wayland_send_scene_input(type, a, b);
}
/* One screen refresh (Choreographer frame callback, UI thread): the compositor draws once. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeVsync(JNIEnv *env, jclass clazz, jlong frameTimeNanos) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeVsync(JNIEnv *env, jclass clazz, jlong frameTimeNanos) {
banner_wayland_vsync((int64_t)frameTimeNanos);
}
/* Shortcut launches: don't draw explorer's windows (desktop, taskbar), like X11's unviewable classes. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHideShell(JNIEnv *env, jclass clazz, jboolean hide) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHideShell(JNIEnv *env, jclass clazz, jboolean hide) {
g_hide_shell = hide ? 1 : 0;
}
@@ -302,7 +325,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHideShell(JNIEnv
* thread, any time: before the compositor starts it is the launch default; afterwards the compositor
* thread flips the state, tells the bound games to rebuild their swapchains for it and redraws. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetZeroCopy(JNIEnv *env, jclass clazz, jboolean on) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetZeroCopy(JNIEnv *env, jclass clazz, jboolean on) {
int live = banner_get_display() != NULL;
if (!live) g_zero_copy = on ? 1 : 0; /* read by ahb_swapchain_init before the queue drains */
banner_host_zero_copy(on ? 1 : 0, live);
@@ -312,13 +335,13 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetZeroCopy(JNIEnv *
/* Milliseconds since the compositor last put a game frame on the layer without a copy; -1 = never
* this session. The drawer's status line ("switching..." until the frames arrive / stop). */
JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeZeroCopyLastFrameAgeMs(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeZeroCopyLastFrameAgeMs(JNIEnv *env, jclass clazz) {
return (jint)ahb_swapchain_last_frame_age_ms();
}
/* Zero-copy frames in the last completed 10 s stats window (on_stats_timer), for the drawer's live line. */
JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeZeroCopyFrames(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeZeroCopyFrames(JNIEnv *env, jclass clazz) {
return (jint)g_zero_copy_last;
}
@@ -326,7 +349,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeZeroCopyFrames(JNIEn
* the renderer's driver imports it (default on; BANNER_WAYLAND_UBWC=0 = off). Set before the compositor
* starts. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetUbwc(JNIEnv *env, jclass clazz, jboolean on) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUbwc(JNIEnv *env, jclass clazz, jboolean on) {
g_ubwc = on ? 1 : 0;
__android_log_print(ANDROID_LOG_INFO, TAG, "compressed (UBWC) game buffers %s", on ? "on" : "off");
}
@@ -334,7 +357,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetUbwc(JNIEnv *env,
/* Debug: name no DRM device in the dma-buf feedback (BANNER_WAYLAND_NO_RENDER_NODE=1), the way a
* phone that exposes no /dev/dri node to apps does. Set before the compositor starts. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetNoRenderNode(JNIEnv *env, jclass clazz, jboolean on) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetNoRenderNode(JNIEnv *env, jclass clazz, jboolean on) {
g_no_render_node = on ? 1 : 0;
if (on) __android_log_print(ANDROID_LOG_INFO, TAG, "debug: advertising no DRM device (main device 0:0)");
}
@@ -344,27 +367,27 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetNoRenderNode(JNIE
* without this SurfaceFlinger never sees the game's cadence. 0 = clear the vote. Any thread, any
* time; the compositor applies it with its next layer transaction and logs the change. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetLayerFrameRate(JNIEnv *env, jclass clazz, jfloat hz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLayerFrameRate(JNIEnv *env, jclass clazz, jfloat hz) {
(void)env; (void)clazz;
sc_layer_set_frame_rate((float)hz);
}
/* The panel's refresh rate (Hz) for the advertised wl_output mode. Set before the compositor starts. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetOutputRefreshRate(JNIEnv *env, jclass clazz, jfloat hz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetOutputRefreshRate(JNIEnv *env, jclass clazz, jfloat hz) {
g_output_refresh_mhz = hz > 1.0f ? (int)(hz * 1000.0f + 0.5f) : 0;
}
/* The container's screen size for the advertised wl_output mode. Set before the compositor starts. */
extern void vkp_reset_first_frame(void);
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeResetFirstFrame(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeResetFirstFrame(JNIEnv *env, jclass clazz) {
(void)env; (void)clazz;
vkp_reset_first_frame();
}
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetOutputSize(JNIEnv *env, jclass clazz, jint w, jint h) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetOutputSize(JNIEnv *env, jclass clazz, jint w, jint h) {
g_output_w = w > 0 ? w : 0;
g_output_h = h > 0 ? h : 0;
}
@@ -372,7 +395,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetOutputSize(JNIEnv
/* Fullscreen mode + screen alignment (Container.FULLSCREEN_* / ALIGN_* values): how the scene is
* fitted onto the output. Any thread, any time; the next frame uses it. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetScaleMode(JNIEnv *env, jclass clazz, jint mode, jint alignment) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetScaleMode(JNIEnv *env, jclass clazz, jint mode, jint alignment) {
vk_present_set_scale_mode(mode, alignment);
__android_log_print(ANDROID_LOG_INFO, TAG, "scale mode %d alignment %d", mode, alignment);
}
@@ -383,38 +406,38 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetScaleMode(JNIEnv
/* Scaling mode: 0=None 1=Linear 2=Nearest 3=SGSR 4=FSR 5=FSR Fit 6=Sharpen 7=NIS 8=SGSR HQ. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetUpscaler(JNIEnv *env, jclass clazz, jint mode) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUpscaler(JNIEnv *env, jclass clazz, jint mode) {
vkp_effects_set_scaling(mode);
}
/* The upscaler's sharpness slider 0..100 (RCAS lobe / SGSR edge / NIS sharpness). */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetUpscaleSharpness(JNIEnv *env, jclass clazz, jint sharpness) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUpscaleSharpness(JNIEnv *env, jclass clazz, jint sharpness) {
vkp_effects_set_upscale_sharpness(sharpness);
}
/* AMD CAS sharpen toggle + level 0..100 (0 = pass off). */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetCas(JNIEnv *env, jclass clazz, jboolean enabled, jint sharpness) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCas(JNIEnv *env, jclass clazz, jboolean enabled, jint sharpness) {
vkp_effects_set_cas(enabled ? 1 : 0, sharpness);
}
/* Fake-HDR toggle. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHdr(JNIEnv *env, jclass clazz, jboolean enabled) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdr(JNIEnv *env, jclass clazz, jboolean enabled) {
vkp_effects_set_hdr(enabled ? 1 : 0);
}
/* Terminal debanding toggle + strength 0..200 (100 = 1 LSB). */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetDeband(JNIEnv *env, jclass clazz, jboolean enabled, jint strength) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetDeband(JNIEnv *env, jclass clazz, jboolean enabled, jint strength) {
vkp_effects_set_deband(enabled ? 1 : 0, strength);
}
/* Colour grade (slider units: brightness/contrast -100..100, gamma 0.5..3, saturation 0..200 %) and the
* FXAA / Toon / CRT / NTSC toggles — VulkanRenderer.setScreenEffects. */
* FXAA / Toon / CRT / NTSC toggles - VulkanRenderer.setScreenEffects. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetScreenEffects(JNIEnv *env, jclass clazz, jfloat brightness,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetScreenEffects(JNIEnv *env, jclass clazz, jfloat brightness,
jfloat contrast, jfloat gamma, jfloat saturation, jboolean fxaa, jboolean toon, jboolean crt, jboolean ntsc) {
vkp_effects_set_screen(brightness, contrast, gamma, saturation, fxaa ? 1 : 0, toon ? 1 : 0, crt ? 1 : 0, ntsc ? 1 : 0);
}
@@ -423,7 +446,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetScreenEffects(JNI
* framework (the panel's HDR capability) rather than in the compositor. banner_log() mirrors to
* logcat and appends to the session file when one is open, so this is safe at any point. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeLogDisplay(JNIEnv *env, jclass clazz, jstring message) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogDisplay(JNIEnv *env, jclass clazz, jstring message) {
char *s = dup_jstr(env, message);
if (s) banner_log("display", "%s", s);
free(s);
@@ -432,7 +455,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeLogDisplay(JNIEnv *e
/* The same under the "perf" area: facts the app knows about the session's performance setup (the CPU
* cores the game is pinned to, say). Same safety as nativeLogDisplay. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeLogPerf(JNIEnv *env, jclass clazz, jstring message) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogPerf(JNIEnv *env, jclass clazz, jstring message) {
char *s = dup_jstr(env, message);
if (s) banner_log("perf", "%s", s);
free(s);
@@ -442,7 +465,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeLogPerf(JNIEnv *env,
/* The opt-in: mode 0 off, 1 BANNER_WAYLAND_HDR=1, 2 =force (testing). Before the compositor starts. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHdrRequest(JNIEnv *env, jclass clazz, jint mode,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrRequest(JNIEnv *env, jclass clazz, jint mode,
jstring source, jboolean dxvkHdr, jboolean zeroCopyForced) {
char *s = dup_jstr(env, source);
banner_color_set_request((int)mode, s, dxvkHdr ? 1 : 0, zeroCopyForced ? 1 : 0);
@@ -451,7 +474,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHdrRequest(JNIEnv
/* The game's display as android.view.Display reports it (before the start; again on every change). */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHdrDisplay(JNIEnv *env, jclass clazz, jint id, jstring name,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrDisplay(JNIEnv *env, jclass clazz, jint id, jstring name,
jstring formats, jboolean hdr10, jfloat maxLum, jfloat maxAvg, jfloat minLum, jboolean ratioAvailable,
jfloat ratio, jint api) {
char *n = dup_jstr(env, name), *f = dup_jstr(env, formats);
@@ -462,38 +485,38 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHdrDisplay(JNIEnv
/* One Display.getHdrSdrRatio() reading (listener = from the display's ratio listener). Any thread. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrSdrRatioSample(JNIEnv *env, jclass clazz, jfloat ratio,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrSdrRatioSample(JNIEnv *env, jclass clazz, jfloat ratio,
jboolean listener) {
banner_color_ratio_sample((float)ratio, listener ? 1 : 0);
}
/* ms since an HDR frame last reached a display layer, -1 = never this session. Any thread. */
JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrLastFrameAgeMs(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrLastFrameAgeMs(JNIEnv *env, jclass clazz) {
return (jint)banner_color_last_frame_age_ms();
}
/* -1 = not decided yet, 0 = closed, 1 = open. Any thread. */
JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrGateState(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrGateState(JNIEnv *env, jclass clazz) {
return (jint)banner_color_gate_state();
}
/* The session is ending: the "HDR on screen: ..." summary line. Any thread, once. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrSessionEnd(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrSessionEnd(JNIEnv *env, jclass clazz) {
banner_color_session_end();
}
/* HDR frames really on screen right now (the HUD badge). Any thread. */
JNIEXPORT jboolean JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrOnScreen(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrOnScreen(JNIEnv *env, jclass clazz) {
return banner_color_hdr_on_screen() ? JNI_TRUE : JNI_FALSE;
}
/* One "color" line in the session log from Java. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeLogColor(JNIEnv *env, jclass clazz, jstring message) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogColor(JNIEnv *env, jclass clazz, jstring message) {
char *s = dup_jstr(env, message);
if (s) banner_log("color", "%s", s);
free(s);
@@ -502,7 +525,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeLogColor(JNIEnv *env
/* One line in the session log under an area Java names ("gpu", "nvapi"): launch facts the app decides.
* The area is the log's 9-character column; an empty one reads "app". */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeLog(JNIEnv *env, jclass clazz, jstring area, jstring message) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLog(JNIEnv *env, jclass clazz, jstring area, jstring message) {
char *a = dup_jstr(env, area), *s = dup_jstr(env, message);
if (s) banner_log(a && a[0] ? a : "app", "%s", s);
free(a);
@@ -511,26 +534,26 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeLog(JNIEnv *env, jcl
/* SDR content's level inside an HDR picture, in nits (default 203). */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHdrSdrWhite(JNIEnv *env, jclass clazz, jfloat nits) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrSdrWhite(JNIEnv *env, jclass clazz, jfloat nits) {
banner_color_set_sdr_white((float)nits);
}
/* The drawer's live HDR output switch: on = HDR frames as HDR, off = the same frames tone-mapped to SDR.
* Applied on the compositor thread (logged there, with a redraw). */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHdrOutput(JNIEnv *env, jclass clazz, jboolean on) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrOutput(JNIEnv *env, jclass clazz, jboolean on) {
if (banner_get_display()) banner_host_hdr_output(on ? 1 : 0);
else banner_color_set_output(on ? 1 : 0); /* no compositor thread yet: nothing is drawing either */
}
JNIEXPORT jboolean JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrOutput(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrOutput(JNIEnv *env, jclass clazz) {
return banner_color_output() ? JNI_TRUE : JNI_FALSE;
}
/* Device evidence for the HDR lines: thermal status + headroom, brightness + mode. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrEnvSample(JNIEnv *env, jclass clazz, jint thermal,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrEnvSample(JNIEnv *env, jclass clazz, jint thermal,
jfloat headroom, jint brightness, jint mode) {
banner_color_env_sample((int)thermal, (float)headroom != (float)headroom ? -1.0f : (float)headroom,
(int)brightness, (int)mode);
@@ -538,18 +561,18 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrEnvSample(JNIEnv
/* Display.getHighestHdrSdrRatio() (Android 16+), <= 0 = not reported. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetHdrHighestRatio(JNIEnv *env, jclass clazz, jfloat ratio) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrHighestRatio(JNIEnv *env, jclass clazz, jfloat ratio) {
banner_color_set_highest_ratio((float)ratio == (float)ratio ? (float)ratio : -1.0f);
}
/* The HDR headroom the screen surface should ask for (HDR10 swapchain frames in the last 1.5 s), 0 = none. */
JNIEXPORT jfloat JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroom(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroom(JNIEnv *env, jclass clazz) {
return (jfloat)banner_color_screen_headroom(NULL, 0);
}
JNIEXPORT jstring JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroomWhy(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroomWhy(JNIEnv *env, jclass clazz) {
char why[200];
banner_color_screen_headroom(why, sizeof(why));
return (*env)->NewStringUTF(env, why);
@@ -557,25 +580,25 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroomWhy
/* The app's screen-surface request, for the no-headroom lines: > 0 asked, 0 cleared, -1 not possible. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrNoteHeadroomRequest(JNIEnv *env, jclass clazz, jfloat ratio) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrNoteHeadroomRequest(JNIEnv *env, jclass clazz, jfloat ratio) {
banner_color_note_headroom_request((float)ratio);
}
/* 0 none, 1 HDR frames on screen with headroom, 2 HDR frames on screen without headroom for 5 s+. */
JNIEXPORT jint JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrState(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrState(JNIEnv *env, jclass clazz) {
return (jint)banner_color_hdr_state();
}
/* An HDR game's frames were shown tone-mapped to SDR in the last 1.5 s (the drawer's status line). */
JNIEXPORT jboolean JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrToneMappedOnScreen(JNIEnv *env, jclass clazz) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrToneMappedOnScreen(JNIEnv *env, jclass clazz) {
return banner_color_tonemapped_on_screen() ? JNI_TRUE : JNI_FALSE;
}
/* The Look the controls currently match (null = Custom) — only named in the session log. */
/* The Look the controls currently match (null = Custom) - only named in the session log. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetLookName(JNIEnv *env, jclass clazz, jstring name) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLookName(JNIEnv *env, jclass clazz, jstring name) {
char *s = dup_jstr(env, name);
vkp_effects_set_look(s);
free(s);
@@ -583,14 +606,14 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetLookName(JNIEnv *
/* The in-game FPS limiter: frames per second, 0 = unlimited. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetFpsLimit(JNIEnv *env, jclass clazz, jint fps) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFpsLimit(JNIEnv *env, jclass clazz, jint fps) {
g_fps_limit = fps > 0 ? fps : 0;
__android_log_print(ANDROID_LOG_INFO, TAG, "fps limit %d", fps);
}
/* Swap/clear the output window (e.g. SurfaceView recreated/destroyed). */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetSurface(
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetSurface(
JNIEnv *env, jclass clazz, jobject surface) {
if (surface) {
vk_present_set_window(ANativeWindow_fromSurface(env, surface));
@@ -616,7 +639,7 @@ static char *dup_bytes(JNIEnv *env, jbyteArray arr, int *len) {
/* Android's clipboard text becomes the guest's selection (empty/null = clear). */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNIEnv *env, jclass clazz, jbyteArray utf8) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNIEnv *env, jclass clazz, jbyteArray utf8) {
int len;
char *buf = dup_bytes(env, utf8, &len);
banner_host_clipboard_text(buf ? buf : "", len);
@@ -625,7 +648,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNI
/* Soft-keyboard text committed to the program accepting text input. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeTextInputCommit(JNIEnv *env, jclass clazz, jbyteArray utf8) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputCommit(JNIEnv *env, jclass clazz, jbyteArray utf8) {
int len;
char *buf = dup_bytes(env, utf8, &len);
if (buf && len) banner_host_text_commit(buf, len);
@@ -634,7 +657,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeTextInputCommit(JNIE
/* Composing (pre-edit) text; cursorBegin/cursorEnd are character indexes into it, -1 = end. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeTextInputPreedit(JNIEnv *env, jclass clazz, jbyteArray utf8,
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputPreedit(JNIEnv *env, jclass clazz, jbyteArray utf8,
jint cursorBegin, jint cursorEnd) {
int len;
char *buf = dup_bytes(env, utf8, &len);
@@ -644,6 +667,6 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeTextInputPreedit(JNI
/* The IME deleted characters around the caret. */
JNIEXPORT void JNICALL
Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeTextInputDelete(JNIEnv *env, jclass clazz, jint before, jint after) {
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputDelete(JNIEnv *env, jclass clazz, jint before, jint after) {
banner_host_text_delete(before, after);
}
@@ -1,6 +1,6 @@
/*
* Clipboard: wl_data_device_manager (core, v3) and zwlr_data_control_manager_v1 (v1), selection
* only — drag and drop is refused. Both protocols share one selection:
* only - drag and drop is refused. Both protocols share one selection:
*
* owner CLIENT a program's wl_data_source / zwlr_data_control_source_v1 (its mime list)
* owner HOST text the app pushed from Android's clipboard (offered under the text mimes)
@@ -1,6 +1,6 @@
/*
* wp_color_manager_v1 (wayland-protocols staging color-management-v1, version 1) — the subset Mesa's
* Wayland WSI binds — plus the HDR gate, the per-surface image descriptions and the session log's
* wp_color_manager_v1 (wayland-protocols staging color-management-v1, version 1) - the subset Mesa's
* Wayland WSI binds - plus the HDR gate, the per-surface image descriptions and the session log's
* HDR evidence. See banner_color.h for the design and HDR_RECON.md for why it is shaped this way.
*
* Strictness policy. A protocol error disconnects the client, and the client here is a GAME, so
@@ -3,7 +3,7 @@
* bridge winewayland.drv uses for IME text (our soft keyboard).
*
* Focus model. winewayland enables its text input as soon as our `enter` names a surface and
* routes every IME update to that surface's window — inside that window's process (the update
* routes every IME update to that surface's window - inside that window's process (the update
* list is per process). Keyboard focus in this compositor sits on the desktop surface (the
* desktop owner's process), so text input can't follow it; it follows banner_ime_target():
* the program window last clicked, else the topmost program window. Clicking into notepad
@@ -1,7 +1,7 @@
package com.steamdeck.launcher
package com.droiddeck.launcher
import android.app.Application
import com.steamdeck.launcher.session.CrashHandler
import com.droiddeck.launcher.session.CrashHandler
/** Process-wide setup: the crash handler, so a session's folder is finished even when we die. */
class App : Application() {
Loaded 100 of 310 files, more files were not shown because too many files have changed in this diff. Show more