mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
Merge pull request #8 from The412Banner/chore/cleanup
chore: simplify UI copy and repository text
This commit is contained in:
97 files changed
+678
-1613
No files matched your search
@@ -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"
|
||||
@@ -1,7 +1,7 @@
|
||||
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.
|
||||
# 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:
|
||||
@@ -76,7 +76,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
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
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 +98,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
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
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}'; }
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
|
||||
# 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.
|
||||
# System.loadLibrary the moment a session started - a check this step would have caught.
|
||||
- name: Check the native dependency closure
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -329,12 +329,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)."
|
||||
;;
|
||||
failure)
|
||||
echo "### ❌ Build failed"
|
||||
echo "### ERROR: Build failed"
|
||||
echo
|
||||
echo "The compiler's errors ([full log]($RUN_URL)):"
|
||||
echo
|
||||
@@ -352,7 +352,7 @@ 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))."
|
||||
;;
|
||||
|
||||
@@ -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
|
||||
@@ -1,571 +1,21 @@
|
||||
# SteamDeck
|
||||
|
||||
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/SteamDeck/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 and the Android SDK/NDK installed. The APK is written to `app/build/outputs/apk/release/app-release.apk`. To install it 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 | closed |
|
||||
| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#3](https://github.com/The412Banner/SteamDeck/pull/3) Fix hardware back in Steam session | 2026-09-23 | merged 2026-09-23 |
|
||||
| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#4](https://github.com/The412Banner/SteamDeck/pull/4) Update app launcher icon | 2026-09-23 | merged 2026-09-23 |
|
||||
| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#5](https://github.com/The412Banner/SteamDeck/pull/5) Add reproducible local build and deploy helpers | 2026-09-23 | merged 2026-09-23 |
|
||||
| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#6](https://github.com/The412Banner/SteamDeck/pull/6) Add Steam and QAM touch controls | 2026-09-23 | merged 2026-09-23 |
|
||||
| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#7](https://github.com/The412Banner/SteamDeck/pull/7) Add session suspend modes and audio recovery | 2026-09-23 | draft |
|
||||
| [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx) | [#8](https://github.com/The412Banner/SteamDeck/pull/8) chore: simplify UI copy and repository text | 2026-09-23 | open |
|
||||
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/SteamDeck/` when diagnosing problems.
|
||||
|
||||
- **@xXJSONDeruloXx**: 7 submitted, 4 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
|
||||
|
||||
The repeatable local build is `tools/build_local.sh`. It uses Docker's Ubuntu cross-toolchain for
|
||||
the two glibc ARM64 preloads (`libfakeinput.so`, `libblsession.so`), the Android SDK/NDK already
|
||||
installed on the machine, and the same release signing and native dependency checks as CI. The
|
||||
APK is written to `app/build/outputs/apk/release/app-release.apk`.
|
||||
|
||||
```sh
|
||||
tools/build_local.sh
|
||||
```
|
||||
|
||||
CI remains available through the `Build APK` workflow. Both paths cross-compile the preloads from
|
||||
the sources in this repo before the app build, so those cannot drift from what ships.
|
||||
|
||||
With an authorized Android device attached over USB or ADB, install the locally built APK with:
|
||||
|
||||
```sh
|
||||
tools/deploy_local.sh
|
||||
```
|
||||
|
||||
Set `ADB_SERIAL` when more than one device is attached. The deploy helper installs over the
|
||||
existing `com.steamdeck.launcher` package using the repository's stable test key and verifies the
|
||||
package path afterward; it does not launch a session automatically.
|
||||
|
||||
## 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); audio, gamepad, and session foundations build on Winlator (brunodev85). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
+2
-2
@@ -40,7 +40,7 @@ android {
|
||||
|
||||
signingConfigs {
|
||||
// AOSP testkey (android@android.com), 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 +69,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'
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
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
|
||||
@@ -86,7 +86,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# HDR on the Wayland backend — reconnaissance
|
||||
# HDR on the Wayland backend - reconnaissance
|
||||
|
||||
> **2026-09-14: round 1 of Phase A is built** (branch `feat/wayland-hdr`, opt-in, testing only) —
|
||||
> **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.
|
||||
|
||||
@@ -24,7 +24,7 @@ carries `dxgi.enableHDR` / `DXVK_HDR` and the `VK_COLOR_SPACE_HDR10_ST2084_EXT`
|
||||
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
|
||||
`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
|
||||
@@ -35,7 +35,7 @@ in §8: **not now.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Game side — how far an HDR request gets today
|
||||
## 1. Game side - how far an HDR request gets today
|
||||
|
||||
### 1.1 What a Windows game does
|
||||
|
||||
@@ -62,7 +62,7 @@ Swapchain formats DXVK will accept per colour space (`Presenter::pickFormat`,
|
||||
* `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
|
||||
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.
|
||||
|
||||
@@ -82,7 +82,7 @@ DXGI enumeration for D3D12 titles is still DXVK's `dxgi.dll`, so `DXVK_HDR` stil
|
||||
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.
|
||||
`DXVK_HDR=1` and nothing else. For us that is one container env var - no launcher work.
|
||||
|
||||
### 1.2 The failure mode today, exactly
|
||||
|
||||
@@ -90,19 +90,19 @@ DXVK's `DXGI_OUTPUT_DESC1` will claim HDR10 as soon as `DXVK_HDR=1`, regardless
|
||||
`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
|
||||
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
|
||||
### 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.
|
||||
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
|
||||
@@ -111,7 +111,7 @@ HDR luminances (`wsi_edid.h:38-56`: min 0.01 / max 1499 / maxFullFrame 799 nits)
|
||||
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
|
||||
### 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
|
||||
@@ -125,11 +125,11 @@ 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 —
|
||||
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`:
|
||||
**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
|
||||
@@ -140,10 +140,10 @@ case VK_FORMAT_A2B10G10R10_UNORM_PACK32: return BANNER_AHB_FORMAT_R10G10B10A2_U
|
||||
|
||||
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
|
||||
(: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
|
||||
### 1.5 X11 has no path - confirmed
|
||||
|
||||
`app/src/main/cpp/winlator/VulkanRendererContext.cpp:439` hardcodes
|
||||
`swapchainFmt = VK_FORMAT_R8G8B8A8_UNORM` and `:492` hardcodes
|
||||
@@ -170,7 +170,7 @@ Implementers: KWin (reference impl, most mature), Mutter (merged 2025-02-25 but
|
||||
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**.
|
||||
**v3** and that *"no compositor supports it yet"* - everybody is converged on **v1**.
|
||||
|
||||
**We only need v1.** That is what Mesa binds.
|
||||
|
||||
@@ -178,7 +178,7 @@ Proton-CachyOS's changelog notes that its `winewayland.drv` `windows_bt2100` pat
|
||||
|
||||
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_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
|
||||
@@ -195,9 +195,9 @@ Then implement: `create_parametric_creator` → `wp_image_description_creator_pa
|
||||
**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
|
||||
### 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
|
||||
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).
|
||||
|
||||
@@ -227,7 +227,7 @@ merged 2025-02-25, Mesa 25.1).
|
||||
|
||||
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
|
||||
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 /
|
||||
@@ -248,7 +248,7 @@ Globals (`compositor.c:2527-2538`, plus `ahb_swapchain.c:406`,
|
||||
`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 —
|
||||
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`):
|
||||
@@ -267,14 +267,14 @@ Good news: the machinery around it is already format-agnostic.
|
||||
`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
|
||||
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 +
|
||||
> `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.
|
||||
|
||||
@@ -283,7 +283,7 @@ chain are 8-bit (`vk_present.c:54` `SCENE_FMT = VK_FORMAT_R8G8B8A8_UNORM`; `effe
|
||||
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
|
||||
`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.**
|
||||
|
||||
@@ -301,16 +301,16 @@ a driver patch (new wcp) to buy something Mesa already gives us for free. **Impl
|
||||
| `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_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** | — |
|
||||
| `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
|
||||
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
|
||||
@@ -333,10 +333,10 @@ Every layer on the device right now reports `dataspace=UNKNOWN (0)`, which SF tr
|
||||
### 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:
|
||||
- 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
|
||||
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.
|
||||
@@ -345,7 +345,7 @@ 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.
|
||||
whatever format the game allocated - it just never labels it.
|
||||
|
||||
### 3.4 What SurfaceFlinger does with an HDR layer on an SDR panel
|
||||
|
||||
@@ -353,7 +353,7 @@ From `services/surfaceflinger/CompositionEngine/src/Output.cpp`, `getBestDataspa
|
||||
`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
|
||||
`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
|
||||
@@ -364,7 +364,7 @@ From `services/surfaceflinger/CompositionEngine/src/Output.cpp`, `getBestDataspa
|
||||
* **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**.
|
||||
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`
|
||||
@@ -373,7 +373,7 @@ From `services/surfaceflinger/CompositionEngine/src/Output.cpp`, `getBestDataspa
|
||||
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.
|
||||
`setExtendedRangeBrightness` - last call wins. Not available on the test device at all.
|
||||
|
||||
### 3.5 `has_HDR_display` vs `getHdrCapabilities()`
|
||||
|
||||
@@ -382,7 +382,7 @@ that the device *has* a wide-gamut, high-luminance panel; it gates SF-internal c
|
||||
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."
|
||||
do.** An empty list does not mean rejection - it means "tone-map it."
|
||||
|
||||
### 3.6 External display
|
||||
|
||||
@@ -390,7 +390,7 @@ On hotplug, the framework queries `getDisplayConfigs`, `getColorModes`, `getHdrC
|
||||
`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
|
||||
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.**
|
||||
|
||||
---
|
||||
@@ -441,7 +441,7 @@ Reading:
|
||||
* `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
|
||||
* `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
|
||||
@@ -456,13 +456,13 @@ 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** |
|
||||
| `/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
|
||||
- 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.
|
||||
@@ -474,11 +474,11 @@ dataspace. So for the **copy path**, the compositor's output HDR is genuinely ju
|
||||
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)
|
||||
### 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** |
|
||||
| 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** |
|
||||
@@ -488,7 +488,7 @@ or `dumpsys`, not by eye. **App-only unless marked.**
|
||||
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)
|
||||
### Phase B - the copy path and the effects chain (only if A is kept)
|
||||
|
||||
| # | Repo | Work | Size |
|
||||
|---|---|---|---|
|
||||
@@ -498,7 +498,7 @@ tagged `BT2020_PQ` with real mastering metadata, and `dumpsys SurfaceFlinger` pr
|
||||
|
||||
**Phase B total: ~3 days. App-only. No new wcp.**
|
||||
|
||||
### Phase C — scRGB, and the one thing that needs a wcp
|
||||
### Phase C - scRGB, and the one thing that needs a wcp
|
||||
|
||||
| # | Repo | Work | Size |
|
||||
|---|---|---|---|
|
||||
@@ -508,17 +508,17 @@ tagged `BT2020_PQ` with real mastering metadata, and `dumpsys SurfaceFlinger` pr
|
||||
|
||||
---
|
||||
|
||||
## 6. Cheap partial wins — and which are visible on a 500-nit panel
|
||||
## 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. |
|
||||
| **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,
|
||||
**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.**
|
||||
|
||||
---
|
||||
@@ -535,13 +535,13 @@ candidates, easiest first:
|
||||
*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
|
||||
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.
|
||||
here** - it is the cheapest signal and needs no HDR display at all.
|
||||
|
||||
### 7.2 Platform-side verification (not by eye)
|
||||
|
||||
@@ -553,11 +553,11 @@ Order of proof, each step gating the next:
|
||||
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.
|
||||
- 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
|
||||
- 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
|
||||
@@ -567,14 +567,14 @@ Order of proof, each step gating the next:
|
||||
`UNKNOWN (0)`.
|
||||
6. **Only then, by eye.**
|
||||
|
||||
### 7.3 Getting a display that can actually show it — three options, ranked
|
||||
### 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 —
|
||||
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 —
|
||||
`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,
|
||||
@@ -587,7 +587,7 @@ Order of proof, each step gating the next:
|
||||
"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
|
||||
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.**
|
||||
|
||||
@@ -600,7 +600,7 @@ Order of proof, each step gating the next:
|
||||
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
|
||||
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.
|
||||
@@ -616,7 +616,7 @@ The reasoning, plainly:
|
||||
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
|
||||
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:**
|
||||
@@ -626,16 +626,16 @@ The reasoning, plainly:
|
||||
* **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.
|
||||
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
|
||||
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 —
|
||||
`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
|
||||
@@ -656,7 +656,7 @@ The reasoning, plainly:
|
||||
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
|
||||
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.
|
||||
@@ -674,7 +674,7 @@ The reasoning, plainly:
|
||||
|
||||
---
|
||||
|
||||
## 10. Round 1 — as built (2026-09-14, `feat/wayland-hdr`)
|
||||
## 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,
|
||||
@@ -691,7 +691,7 @@ Open only when ALL hold when the compositor starts:
|
||||
| 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 —
|
||||
`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
|
||||
@@ -706,13 +706,13 @@ only right on the game's own layer) and exports `BANNER_WSI_AHB=1` to match.
|
||||
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),
|
||||
`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
|
||||
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).
|
||||
@@ -727,7 +727,7 @@ puts it back to `UNKNOWN` and clears the metadata. The zero-copy frame gets the
|
||||
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
|
||||
### 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:
|
||||
@@ -741,7 +741,7 @@ shader pass is its own project). Instead:
|
||||
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
|
||||
### 10.5 Proof without root - what a tester's logs say
|
||||
|
||||
`Download/Wayland-logs/wayland-*.log`, tag `color` (plus the existing `display` capability line):
|
||||
|
||||
@@ -754,14 +754,14 @@ shader pass is its own project). Instead:
|
||||
| `<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 |
|
||||
| `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)
|
||||
## 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
|
||||
@@ -771,7 +771,7 @@ 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
|
||||
= 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
|
||||
@@ -780,14 +780,14 @@ The gate itself is unchanged (§10.1) apart from its first input.
|
||||
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
|
||||
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
|
||||
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`)
|
||||
@@ -907,4 +907,4 @@ hot, not recording, brightness 8-223 manual - and the HDR/SDR ratio at **1.00 th
|
||||
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.
|
||||
- scRGB (FP16) swapchains (Phase C) - unchanged: needs FP16 gralloc and a layer change.
|
||||
@@ -3,23 +3,23 @@
|
||||
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.
|
||||
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.
|
||||
(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
|
||||
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
|
||||
is the imagefs - add via `ImageFsInstaller` (new `installWaylandLibs()`), same
|
||||
pattern as `installFFmpeg8()`.
|
||||
|
||||
## Integration plan (M4)
|
||||
@@ -36,13 +36,13 @@ The staged `src/` here is that proven code, to grow into the app-embedded compos
|
||||
and point the container at the winewayland wcp.
|
||||
|
||||
## Status
|
||||
- ✅ **Native lib foundation done + compile-verified.** Compositor + vk_import + pre-generated
|
||||
- **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`
|
||||
- **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
|
||||
@@ -126,7 +126,7 @@ small interface in `src/banner_ext.h` (compositor.c only gained hook calls + acc
|
||||
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
|
||||
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).
|
||||
@@ -166,15 +166,15 @@ small interface in `src/banner_ext.h` (compositor.c only gained hook calls + acc
|
||||
- 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
|
||||
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`
|
||||
## 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
|
||||
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
|
||||
@@ -240,7 +240,7 @@ again on every flip. The compositor owns the state:
|
||||
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
|
||||
- **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
|
||||
@@ -248,8 +248,8 @@ again on every flip. The compositor owns the state:
|
||||
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
|
||||
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.
|
||||
@@ -289,7 +289,7 @@ app window ....... Compose UI, the in-game drawer, the perf HUD, the on-screen c
|
||||
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
|
||||
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
|
||||
@@ -298,29 +298,29 @@ app window ....... Compose UI, the in-game drawer, the perf HUD, the on-screen c
|
||||
- **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
|
||||
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
|
||||
- **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
|
||||
- **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
|
||||
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
|
||||
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.
|
||||
@@ -332,37 +332,37 @@ app window ....... Compose UI, the in-game drawer, the perf HUD, the on-screen c
|
||||
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
|
||||
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
|
||||
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
|
||||
`| 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**:
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -371,7 +371,7 @@ app window ....... Compose UI, the in-game drawer, the perf HUD, the on-screen c
|
||||
## 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
|
||||
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`):**
|
||||
@@ -384,7 +384,7 @@ scene (every draw blitted 1:1 into a scene-sized R8G8B8A8 image; compositor pas
|
||||
-> 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
|
||||
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`).
|
||||
@@ -409,12 +409,12 @@ them logs an `error` and effects stay off for the session). The app wires the dr
|
||||
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
|
||||
**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
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -432,7 +432,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
|
||||
|
||||
@@ -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"
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.* .
|
||||
*/
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -157,7 +157,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);
|
||||
|
||||
|
||||
@@ -147,7 +147,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;
|
||||
@@ -227,7 +227,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeCompositorTid(JNIEnv
|
||||
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,
|
||||
jstring xdgRuntimeDir) {
|
||||
@@ -412,7 +412,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeSetDeband(JNIEnv *en
|
||||
}
|
||||
|
||||
/* 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,
|
||||
jfloat contrast, jfloat gamma, jfloat saturation, jboolean fxaa, jboolean toon, jboolean crt, jboolean ntsc) {
|
||||
@@ -573,7 +573,7 @@ Java_com_steamdeck_launcher_wayland_WaylandCompositor_nativeHdrToneMappedOnScree
|
||||
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) {
|
||||
char *s = dup_jstr(env, name);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -493,8 +493,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -56,8 +56,8 @@ import com.steamdeck.launcher.session.GameStorage
|
||||
|
||||
/**
|
||||
* The whole app outside a session: is the runtime installed, is there a newer one, frame
|
||||
* generation, and one button that starts Steam. Everything a Steam client can do — the library,
|
||||
* the store, downloads, settings — is the client's own job once [SessionActivity] has it on screen.
|
||||
* generation, and one button that starts Steam. Everything a Steam client can do - the library,
|
||||
* the store, downloads, settings - is the client's own job once [SessionActivity] has it on screen.
|
||||
*/
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val ui = Handler(Looper.getMainLooper())
|
||||
@@ -480,7 +480,7 @@ class MainActivity : ComponentActivity() {
|
||||
val auto = td.autoId()
|
||||
androidRows = buildList {
|
||||
add(DriverRow(
|
||||
TurnipDriver.AUTO, "Auto — picked by GPU",
|
||||
TurnipDriver.AUTO, "Auto - picked by GPU",
|
||||
if (auto == "system") "system Vulkan: no bundled build for this GPU" else "${td.displayName(auto)} (bundled)",
|
||||
false,
|
||||
))
|
||||
@@ -491,8 +491,8 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Import off the main thread — a driver zip is a few MB and the glibc check reads the whole
|
||||
* library — then say what happened. A refusal's message is the user-facing reason.
|
||||
* Import off the main thread - a driver zip is a few MB and the glibc check reads the whole
|
||||
* library - then say what happened. A refusal's message is the user-facing reason.
|
||||
*/
|
||||
private fun importDriver(uri: Uri, linux: Boolean) {
|
||||
val name = displayNameOf(uri)
|
||||
@@ -544,7 +544,7 @@ class MainActivity : ComponentActivity() {
|
||||
}.getOrNull().let { v ->
|
||||
when (v?.lowercase()) {
|
||||
"false", "0" -> null
|
||||
else -> "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, the client and its helpers, Wine. Where that is left on, the client dies with nothing in its log, because nothing in the session did it. Some phones have a \"restrict child processes\" switch in Developer options — turn it off. Otherwise, over adb:\n\n adb shell settings put global settings_enable_monitor_phantom_procs false\n\nThis phone " + (if (v == null) "has not been set either way, so the ROM's default applies." else "currently reports it as on.")
|
||||
else -> "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, the client and its helpers, Wine. Where that is left on, the client dies with nothing in its log, because nothing in the session did it. Some phones have a \"restrict child processes\" switch in Developer options - turn it off. Otherwise, over adb:\n\n adb shell settings put global settings_enable_monitor_phantom_procs false\n\nThis phone " + (if (v == null) "has not been set either way, so the ROM's default applies." else "currently reports it as on.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,13 +56,13 @@ import java.io.File
|
||||
|
||||
/**
|
||||
* The session's screen: our Wayland compositor presenting onto this activity's Surface, and the
|
||||
* input that reaches it. The session itself — gamescope, the Steam client, audio — belongs to
|
||||
* input that reaches it. The session itself - gamescope, the Steam client, audio - belongs to
|
||||
* [SessionService] and keeps running when this activity does not exist, which is what lets the
|
||||
* user leave Big Picture for another app and come back to it still signed in and still
|
||||
* downloading.
|
||||
*
|
||||
* Three layers: the SurfaceView the compositor draws into, the on-screen pad (a canvas View,
|
||||
* since it is input rather than a menu), and one Compose layer on top for everything else —
|
||||
* since it is input rather than a menu), and one Compose layer on top for everything else -
|
||||
* the HUD line, the loading overlay, the drawer and its dialogs.
|
||||
*/
|
||||
class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
@@ -95,7 +95,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
/**
|
||||
* Shows the on-screen pad when nothing is plugged in and takes it away the moment something
|
||||
* is — a user with a controller in their hands should not be looking at buttons they cannot
|
||||
* is - a user with a controller in their hands should not be looking at buttons they cannot
|
||||
* press, and a user without one must not be left with no way to answer Big Picture.
|
||||
*/
|
||||
private val deviceListener = object : InputManager.InputDeviceListener {
|
||||
@@ -264,7 +264,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// The output size belongs to the session, not to the Surface: gamescope's display is
|
||||
// sized once when the session starts and cannot change. A foldable recreates the Surface
|
||||
// on the other panel, and recomputing the size there told the compositor a 21:9 buffer
|
||||
// was 16:9 — the picture came back squashed sideways. While a session runs, keep its size.
|
||||
// was 16:9 - the picture came back squashed sideways. While a session runs, keep its size.
|
||||
val size = if (SessionState.running) SessionState.outputSize else outputSize()
|
||||
SessionState.outputSize = size
|
||||
onScreenControls?.setPicture(drawnRect())
|
||||
@@ -318,7 +318,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// period, so a long frame raises CPU clocks now rather than after the load averages up.
|
||||
PerfHints.arm(this, refreshHz())
|
||||
// The service owns everything below the compositor. It is started whenever no session is
|
||||
// running — NOT only when the compositor was just started: the compositor lives for the
|
||||
// running - NOT only when the compositor was just started: the compositor lives for the
|
||||
// whole process, so the second Play after a session ended used to re-attach the Surface,
|
||||
// start nothing, and leave the loading panel counting up over a dead session.
|
||||
if (!SessionState.running) {
|
||||
@@ -344,7 +344,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// The on-screen controls follow the picture: into the bars beside or under it when there are any.
|
||||
onScreenControls?.setPicture(drawnRect())
|
||||
if (resized) {
|
||||
Log.i(TAG, "surface resized to ${width}x$height — rebinding the compositor")
|
||||
Log.i(TAG, "surface resized to ${width}x$height - rebinding the compositor")
|
||||
CompositorHost.resize(holder.surface) { applyFrameGen() }
|
||||
}
|
||||
}
|
||||
@@ -374,7 +374,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val panelH = minOf(bounds.width(), bounds.height()).toFloat()
|
||||
// Never narrower than 16:9. A foldable's inner panel is nearly square, and a game handed a
|
||||
// square display draws for the frame it was made for and cuts the sides off itself.
|
||||
// Wider than 16:9 is fine — games and the client cope with a phone's 20:9 — so the
|
||||
// Wider than 16:9 is fine - games and the client cope with a phone's 20:9 - so the
|
||||
// panel's aspect is kept above that, unless the user pinned 16:9 for a foldable, and the
|
||||
// compositor letterboxes onto a squarer panel.
|
||||
val aspect = if (SessionPrefs.shapeMode(this) == SessionPrefs.SHAPE_WIDE) 16f / 9f
|
||||
@@ -516,12 +516,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val down = event.action == KeyEvent.ACTION_DOWN
|
||||
if (down || event.action == KeyEvent.ACTION_UP) {
|
||||
var evdev = EvdevKeys.fromKeyCode(event.keyCode)
|
||||
// What the key stands for, and the key it would sit on without Shift — non-zero
|
||||
// What the key stands for, and the key it would sit on without Shift - non-zero
|
||||
// only for a character Shift puts there: every capital and the symbol row.
|
||||
val ch = event.unicodeChar
|
||||
val plain = if (ch > 0) EvdevKeys.unshiftedChar(ch) else 0
|
||||
// A soft keyboard's symbol keys are in neither the table nor a scan code, so they
|
||||
// reached the session as nothing at all — a sign-in took an address without its @.
|
||||
// reached the session as nothing at all - a sign-in took an address without its @.
|
||||
// Work back from the character instead: which key carries it.
|
||||
if (evdev <= 0 && ch > 0) {
|
||||
val code = EvdevKeys.fromKeyCode(EvdevKeys.keycodeForChar(if (plain != 0) plain else ch))
|
||||
|
||||
@@ -23,7 +23,7 @@ import java.util.ArrayList;
|
||||
* needs no translating.
|
||||
*
|
||||
* <p>Ported down from Bannerlator's component: no sink suspend/resume and no route-change
|
||||
* recreate — those need the pasink native client, and a session here is a foreground activity
|
||||
* recreate - those need the pasink native client, and a session here is a foreground activity
|
||||
* that does not background the way a game container does.
|
||||
*/
|
||||
public class PulseAudioComponent extends EnvironmentComponent {
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* The two fetches this app makes: the runtime catalog (a few hundred bytes of JSON) and the
|
||||
* runtime tarball itself (~790 MB). Plain HttpURLConnection — nothing here needs a client library,
|
||||
* runtime tarball itself (~790 MB). Plain HttpURLConnection - nothing here needs a client library,
|
||||
* and one fewer dependency is one fewer thing that can fail a release build.
|
||||
*/
|
||||
public final class Downloader {
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.steamdeck.launcher.core;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* One piece of a running session — the audio daemon, the network-link file, the session process
|
||||
* One piece of a running session - the audio daemon, the network-link file, the session process
|
||||
* itself. The session starts them in order and stops them in reverse.
|
||||
*/
|
||||
public abstract class EnvironmentComponent {
|
||||
|
||||
@@ -18,7 +18,7 @@ public final class FileUtils {
|
||||
|
||||
private FileUtils() {}
|
||||
|
||||
/** Recursive delete that does not follow symlinks — a rootfs is full of them. */
|
||||
/** Recursive delete that does not follow symlinks - a rootfs is full of them. */
|
||||
public static void delete(File file) {
|
||||
if (file == null || !file.exists() && !isSymlink(file)) return;
|
||||
if (file.isDirectory() && !isSymlink(file)) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Starts the session's host processes — proot and the audio daemon — and reports the exit status.
|
||||
* Starts the session's host processes - proot and the audio daemon - and reports the exit status.
|
||||
* The pid is read out of the hidden field on {@code java.lang.Process} because stopping a session
|
||||
* means killing that exact process: proot runs with {@code --kill-on-exit}, so the whole guest
|
||||
* tree goes with it.
|
||||
@@ -89,7 +89,7 @@ public final class ProcessHelper {
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/** Splits on spaces, honouring backslash-escaped spaces — paths under /data are full of them. */
|
||||
/** Splits on spaces, honouring backslash-escaped spaces - paths under /data are full of them. */
|
||||
public static String[] splitCommand(String command) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* The small zstd tarballs that ship inside the apk (the Turnip driver, the PulseAudio modules).
|
||||
* The runtime rootfs is not one of these — it needs hard links and symlinks preserved, which
|
||||
* The runtime rootfs is not one of these - it needs hard links and symlinks preserved, which
|
||||
* {@link com.steamdeck.launcher.runtime.LinuxRuntimeInstaller} handles itself.
|
||||
*/
|
||||
public final class TarZst {
|
||||
|
||||
@@ -257,7 +257,7 @@ fun CollapsibleRail(
|
||||
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f))
|
||||
|
||||
// ── Sections + items (SCROLLABLE so a tall rail — e.g. many File Manager favourites — is
|
||||
// ── Sections + items (SCROLLABLE so a tall rail - e.g. many File Manager favourites - is
|
||||
// fully reachable in both orientations; the header above and footer below stay pinned) ──
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -307,7 +307,7 @@ fun CollapsibleRail(
|
||||
val iconTint = if (active) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val labelColor = if (active) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
if (collapsed) {
|
||||
// Icon-only width is unchanged — a TINY label goes UNDER the icon (2 lines max,
|
||||
// Icon-only width is unchanged - a TINY label goes UNDER the icon (2 lines max,
|
||||
// centre-aligned, wrapping/abbreviated) so every button stays identifiable without
|
||||
// widening the rail.
|
||||
Column(
|
||||
@@ -362,7 +362,7 @@ fun CollapsibleRail(
|
||||
/**
|
||||
* Portrait counterpart to [CollapsibleRail]: the same navigation [items] laid out as a horizontal
|
||||
* icon-over-label bar pinned across the top, with an accent underline under the selected one. Mirrors
|
||||
* the container editor's top tab bar so every editor reads the same — pin this in portrait wherever a
|
||||
* the container editor's top tab bar so every editor reads the same - pin this in portrait wherever a
|
||||
* [CollapsibleRail] is used in landscape. Optional [links] (Help / Reset …) are appended after a
|
||||
* faint separator, exactly like the rail's links.
|
||||
*
|
||||
@@ -407,7 +407,7 @@ fun RailTopTabs(
|
||||
}
|
||||
}
|
||||
|
||||
/** One cell of [RailTopTabs] — icon over a small label, with an accent underline (zero-width when
|
||||
/** One cell of [RailTopTabs] - icon over a small label, with an accent underline (zero-width when
|
||||
* unselected, so every cell keeps the same height). Mirrors the container editor's TopCell. */
|
||||
@Composable
|
||||
private fun TopTabCell(
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.json.JSONArray
|
||||
*
|
||||
* Stores an ordered (insertion-order) list of absolute path strings as a JSON array
|
||||
* under "file_manager_favorites" in SharedPreferences "file_manager_prefs". Only the
|
||||
* absolute path is kept — everything shown on a favourite card (drive label, container
|
||||
* absolute path is kept - everything shown on a favourite card (drive label, container
|
||||
* name, display path) is derived live, so a renamed or deleted container resolves
|
||||
* correctly for free.
|
||||
*/
|
||||
|
||||
@@ -137,21 +137,21 @@ import java.util.Locale
|
||||
/**
|
||||
* What to do when a pasted item already exists at the destination.
|
||||
*
|
||||
* OVERWRITE and MERGE resolve to the same call — `copyWithProgress` recurses into an existing
|
||||
* directory and truncates existing files — but they mean different things to the user, so both are
|
||||
* OVERWRITE and MERGE resolve to the same call - `copyWithProgress` recurses into an existing
|
||||
* directory and truncates existing files - but they mean different things to the user, so both are
|
||||
* offered and the wording is chosen per item type (files overwrite, folders merge).
|
||||
*/
|
||||
enum class ConflictChoice { OVERWRITE, MERGE, KEEP_BOTH, SKIP }
|
||||
|
||||
/**
|
||||
* Ordering for the file list. Folders always lead regardless of direction — a descending sort that
|
||||
* Ordering for the file list. Folders always lead regardless of direction - a descending sort that
|
||||
* buries every folder under the files is never what someone means by "Z to A".
|
||||
*/
|
||||
/**
|
||||
* Shortens a path from the LEFT, keeping whole segments.
|
||||
*
|
||||
* Compose's TextOverflow can only ellipsise the tail, which for a path throws away the part that
|
||||
* matters — `/storage/emulated/0/Winlator/Game…` tells you nothing about where you are.
|
||||
* matters - `/storage/emulated/0/Winlator/Game…` tells you nothing about where you are.
|
||||
*/
|
||||
private fun elidePathStart(path: String, max: Int): String {
|
||||
if (path.length <= max) return path
|
||||
@@ -196,7 +196,7 @@ private fun isWithin(child: File, ancestor: File): Boolean {
|
||||
|
||||
// ── DOS file attributes (Read-only / Hidden) ──
|
||||
// The in-container file manager (wfm.exe) exposes these in its Properties dialog; this mirrors the
|
||||
// same two toggles for files browsed from the app side. No root — we only touch permission bits the
|
||||
// same two toggles for files browsed from the app side. No root - we only touch permission bits the
|
||||
// app owns (container files) and the Wine DOS-attribute xattr.
|
||||
|
||||
// FILE_ATTRIBUTE_HIDDEN, as Wine encodes it in the user.DOSATTRIB extended attribute.
|
||||
@@ -204,7 +204,7 @@ private const val FILE_ATTRIBUTE_HIDDEN = 0x2
|
||||
private const val DOSATTRIB_XATTR = "user.DOSATTRIB"
|
||||
|
||||
// Snapshot of a file's two toggleable attributes. hiddenSupported is false when the underlying
|
||||
// filesystem can't store the DOSATTRIB xattr (the FUSE /storage volumes) — the Hidden toggle is then
|
||||
// filesystem can't store the DOSATTRIB xattr (the FUSE /storage volumes) - the Hidden toggle is then
|
||||
// disabled while Read-only keeps working.
|
||||
private data class FileAttrState(
|
||||
val readOnly: Boolean,
|
||||
@@ -226,7 +226,7 @@ private fun parseDosAttrib(raw: ByteArray): Int {
|
||||
// comes back hiddenSupported=false (Hidden toggle disabled), Read-only always resolves.
|
||||
private fun readFileAttrs(file: File): FileAttrState {
|
||||
val path = file.absolutePath
|
||||
// Read-only reflects the OWNER write bit — that's what Wine maps to FILE_ATTRIBUTE_READONLY.
|
||||
// Read-only reflects the OWNER write bit - that's what Wine maps to FILE_ATTRIBUTE_READONLY.
|
||||
val readOnly = runCatching {
|
||||
(android.system.Os.stat(path).st_mode and android.system.OsConstants.S_IWUSR) == 0
|
||||
}.getOrDefault(!file.canWrite())
|
||||
@@ -309,7 +309,7 @@ fun describeLocation(file: File): FavLocation {
|
||||
}
|
||||
|
||||
// Semantic identity colours for the favourite-card drive badge. Intentionally NOT theme
|
||||
// accent colours — they identify the storage source at a glance. Returns (background, foreground).
|
||||
// accent colours - they identify the storage source at a glance. Returns (background, foreground).
|
||||
private fun badgeColors(loc: FavLocation): Pair<Color, Color> {
|
||||
val white = Color(0xFFFFFFFF)
|
||||
return when {
|
||||
@@ -350,13 +350,13 @@ fun FileManagerScreen(
|
||||
val rootDir = remember {
|
||||
// Both modes: honour an explicit caller-supplied start dir (e.g. Log Manager's game-log
|
||||
// folder), else open at the INTERNAL STORAGE ROOT. Selection screens (drive-folder pick,
|
||||
// local component pick, imports) previously defaulted to Download which — combined with the
|
||||
// currentRoot floor below — trapped users in Download with no way up (reported bug).
|
||||
// local component pick, imports) previously defaulted to Download which - combined with the
|
||||
// currentRoot floor below - trapped users in Download with no way up (reported bug).
|
||||
initialDir?.takeIf { it.isDirectory } ?: File("/storage/emulated/0")
|
||||
}
|
||||
|
||||
var currentDir by remember { mutableStateOf(rootDir) }
|
||||
// The up/back FLOOR — back + the up-arrow are disabled while currentDir == currentRoot. It MUST be
|
||||
// The up/back FLOOR - back + the up-arrow are disabled while currentDir == currentRoot. It MUST be
|
||||
// the VOLUME ROOT of the start dir (internal /storage/emulated/0, or an SD card /storage/XXXX-XXXX),
|
||||
// NOT the start dir itself: otherwise opening at any subfolder disables up/back and traps the user
|
||||
// there. (Mirrors the volume-root logic in favLocationOf above.)
|
||||
@@ -377,7 +377,7 @@ fun FileManagerScreen(
|
||||
var selectedEntry by remember { mutableStateOf<File?>(null) }
|
||||
var showMenuFor by remember { mutableStateOf<File?>(null) }
|
||||
// Clipboard holds a LIST so one paste can carry a whole selection. Cut/copy semantics are a
|
||||
// flag on the batch rather than per item — mixing the two in one clipboard has no sane meaning.
|
||||
// flag on the batch rather than per item - mixing the two in one clipboard has no sane meaning.
|
||||
var clipboardFiles by remember { mutableStateOf<List<File>>(emptyList()) }
|
||||
var isCutOperation by remember { mutableStateOf(false) }
|
||||
// Multi-select. Keyed by absolute path rather than File so a directory reload (which builds
|
||||
@@ -398,10 +398,10 @@ fun FileManagerScreen(
|
||||
var sortDesc by remember { mutableStateOf(browsePrefs.getBoolean("fmSortDesc", false)) }
|
||||
var showHidden by remember { mutableStateOf(browsePrefs.getBoolean("fmShowHidden", true)) }
|
||||
var showSortMenu by remember { mutableStateOf(false) }
|
||||
// View mode: list of cards (default) or a thumbnail grid. Density applies to the list only —
|
||||
// View mode: list of cards (default) or a thumbnail grid. Density applies to the list only -
|
||||
// a grid tile has no second line to compact.
|
||||
// The grid/list toggle is the SOURCE OF TRUTH in BOTH orientations (it drives the view and its
|
||||
// choice persists across rotation). Grid is the default — most useful in landscape, and in
|
||||
// choice persists across rotation). Grid is the default - most useful in landscape, and in
|
||||
// portrait GridCells.Adaptive naturally renders fewer columns (~2). Do NOT force portrait to list:
|
||||
// that broke the toggle on-device (tapping it did nothing in portrait).
|
||||
var gridView by remember { mutableStateOf(browsePrefs.getBoolean("fmGridView", true)) }
|
||||
@@ -502,7 +502,7 @@ fun FileManagerScreen(
|
||||
suspend fun askConflict(file: File): ConflictChoice? {
|
||||
pendingConflict = file
|
||||
conflictChoice = null
|
||||
// Poll rather than plumb a CompletableDeferred through Compose state — the dialog answers
|
||||
// Poll rather than plumb a CompletableDeferred through Compose state - the dialog answers
|
||||
// by setting conflictChoice, and this coroutine is already off the critical path.
|
||||
while (pendingConflict != null && conflictChoice == null) kotlinx.coroutines.delay(50)
|
||||
return conflictChoice
|
||||
@@ -556,7 +556,7 @@ fun FileManagerScreen(
|
||||
operationLabel = buildString {
|
||||
append(if (cut) "Moving" else "Copying")
|
||||
if (sources.size > 1) append(" ${done + 1}/${sources.size}")
|
||||
append(" — ").append(src.name)
|
||||
append(" - ").append(src.name)
|
||||
}
|
||||
var lastPct = -1
|
||||
val onProgress = FileOps.ProgressCallback { copied, total ->
|
||||
@@ -745,7 +745,7 @@ fun FileManagerScreen(
|
||||
isOperationRunning = true
|
||||
var failed = 0
|
||||
victims.forEachIndexed { i, f ->
|
||||
operationLabel = "Deleting ${i + 1}/${victims.size} — ${f.name}"
|
||||
operationLabel = "Deleting ${i + 1}/${victims.size} - ${f.name}"
|
||||
if (!withContext(Dispatchers.IO) { FileOps.delete(f) }) failed++
|
||||
}
|
||||
isOperationRunning = false
|
||||
@@ -761,7 +761,7 @@ fun FileManagerScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Paste conflict — one per colliding item, with "apply to all" for a long batch.
|
||||
// Paste conflict - one per colliding item, with "apply to all" for a long batch.
|
||||
pendingConflict?.let { conflict ->
|
||||
val isDir = conflict.isDirectory
|
||||
OutlinedAlertDialog(
|
||||
@@ -856,7 +856,7 @@ fun FileManagerScreen(
|
||||
val driveChipAlpha = if (showFavorites) 0.45f else 1f
|
||||
Box {
|
||||
// The drive/location selector opens the drive dropdown, so give it the same outlined
|
||||
// look as the "New Folder" button + the rail location items — it reads as a button, not
|
||||
// look as the "New Folder" button + the rail location items - it reads as a button, not
|
||||
// plain text. Border uses the theme accent token; behaviour unchanged.
|
||||
val driveChipShape = RoundedCornerShape(8.dp)
|
||||
Text(
|
||||
@@ -909,7 +909,7 @@ fun FileManagerScreen(
|
||||
|
||||
if (showFavorites) {
|
||||
Text(
|
||||
text = "★ Favorites",
|
||||
text = "Favorites",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
@@ -918,12 +918,12 @@ fun FileManagerScreen(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
} else if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
// PORTRAIT: hide the current-folder name — it's redundant with the path bar directly
|
||||
// PORTRAIT: hide the current-folder name - it's redundant with the path bar directly
|
||||
// below (which shows the full path). The spacer keeps the action icons right-aligned.
|
||||
Spacer(Modifier.weight(1f))
|
||||
} else {
|
||||
// LANDSCAPE: the CURRENT FOLDER, not the full path. A path ellipsised on the right
|
||||
// hides its tail — the only part that says where you are ("…/Winlator/Game…"). The
|
||||
// hides its tail - the only part that says where you are ("…/Winlator/Game…"). The
|
||||
// full path moves to the line below, where it has room.
|
||||
Text(
|
||||
text = currentDir.name.ifBlank { currentDir.absolutePath },
|
||||
@@ -1029,7 +1029,7 @@ fun FileManagerScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Free space on the volume being browsed — worth knowing before starting a 60 GB copy.
|
||||
// Free space on the volume being browsed - worth knowing before starting a 60 GB copy.
|
||||
val freeSpace = remember(currentDir.absolutePath, entries) {
|
||||
runCatching { currentDir.usableSpace }.getOrDefault(0L)
|
||||
}
|
||||
@@ -1225,7 +1225,7 @@ fun FileManagerScreen(
|
||||
val locationSections = buildList {
|
||||
add(RailSection("STORAGE", storageItems))
|
||||
if (quickItems.isNotEmpty()) add(RailSection("QUICK", quickItems))
|
||||
if (favItems.isNotEmpty()) add(RailSection("★ FAVORITES", favItems))
|
||||
if (favItems.isNotEmpty()) add(RailSection("FAVORITES", favItems))
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||
@@ -1431,7 +1431,7 @@ fun FileManagerScreen(
|
||||
|
||||
// The context-menu item list shared by the list rows (FileItemRow) and the grid tiles
|
||||
// (FileGridTile), so both open the identical menu. Gating (isDir/canRun/isInno/looksLikeArchive) is
|
||||
// recomputed from [file] here — one source of truth for what each item shows. Every item dismisses
|
||||
// recomputed from [file] here - one source of truth for what each item shows. Every item dismisses
|
||||
// the menu first, then runs its action.
|
||||
@Composable
|
||||
private fun FileContextMenuItems(
|
||||
@@ -1462,7 +1462,7 @@ private fun FileContextMenuItems(
|
||||
onClick = { onDismissMenu(); onProperties() },
|
||||
)
|
||||
MenuItemDivider()
|
||||
// Favorites are directories — only folders get the pin toggle.
|
||||
// Favorites are directories - only folders get the pin toggle.
|
||||
if (isDir) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (isFavorite) "Remove from Favorites" else "Add to Favorites") },
|
||||
@@ -1708,7 +1708,7 @@ private fun FavoritesList(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "No favorites yet — pin a folder with its ⋮ menu to jump back here fast.",
|
||||
text = "No favorites yet - pin a folder with its ⋮ menu to jump back here fast.",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
@@ -1729,7 +1729,7 @@ private fun FavoritesList(
|
||||
}
|
||||
}
|
||||
|
||||
// A single favourite — matches the FileItemRow card style (surfaceContainer + outline +
|
||||
// A single favourite - matches the FileItemRow card style (surfaceContainer + outline +
|
||||
// RoundedCornerShape(10.dp)). Shows the folder name, a coloured drive badge + origin text,
|
||||
// and the full display path; tapping jumps into it, the filled star unpins.
|
||||
@Composable
|
||||
@@ -1818,7 +1818,7 @@ private fun FavoriteCard(
|
||||
/**
|
||||
* One entry in the File Manager's grid view: a big thumbnail with the name under it.
|
||||
*
|
||||
* Deliberately drops size and date — at this width they truncate to noise. The grid is for
|
||||
* Deliberately drops size and date - at this width they truncate to noise. The grid is for
|
||||
* recognising things by sight (screenshots, covers, game folders); the list stays the view for
|
||||
* reading details.
|
||||
*/
|
||||
@@ -1852,7 +1852,7 @@ private fun FileGridTile(
|
||||
if (bmp != null) exeIcon = bmp.asImageBitmap()
|
||||
}
|
||||
}
|
||||
// The tile has no ⋮ button — long-press opens this menu, anchored to the Box around the Card.
|
||||
// The tile has no ⋮ button - long-press opens this menu, anchored to the Box around the Card.
|
||||
Box {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
@@ -1942,7 +1942,7 @@ private fun FileGridTile(
|
||||
|
||||
/**
|
||||
* Properties sheet for a single file or folder: basic info plus the two Windows/Wine file attributes
|
||||
* the in-container file manager (wfm.exe) also exposes — Read-only and Hidden. Each toggle applies
|
||||
* the in-container file manager (wfm.exe) also exposes - Read-only and Hidden. Each toggle applies
|
||||
* immediately (off the main thread) and refreshes the listing via [onChanged].
|
||||
*
|
||||
* State is keyed on the file so it always reflects the entry it was opened for. On a filesystem that
|
||||
@@ -1980,7 +1980,7 @@ private fun FilePropertiesDialog(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
PropertyLine("Location", file.parent ?: "—")
|
||||
PropertyLine("Location", file.parent ?: "-")
|
||||
PropertyLine(
|
||||
"Type",
|
||||
if (file.isDirectory) "Folder"
|
||||
@@ -2033,7 +2033,7 @@ private fun FilePropertiesDialog(
|
||||
attrs = attrs?.copy(hidden = want)
|
||||
onChanged()
|
||||
} else {
|
||||
// The write failed after all — disable the toggle rather than lie.
|
||||
// The write failed after all - disable the toggle rather than lie.
|
||||
attrs = attrs?.copy(hiddenSupported = false)
|
||||
Toast.makeText(context, "Couldn't change Hidden on this storage", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
|
||||
// Single source of truth for the app's AlertDialogs: a drop-in wrapper around the Material3
|
||||
// slot-based AlertDialog that adds a 1dp outline around the whole popup box — matching the
|
||||
// slot-based AlertDialog that adds a 1dp outline around the whole popup box - matching the
|
||||
// outline that Modifier.outlinedMenuCard() (MenuStyle.kt) puts on the app's menus. Signature
|
||||
// mirrors androidx.compose.material3.AlertDialog exactly (compose-bom 2024.02.00 → material3
|
||||
// 1.2.0), so every call site can swap AlertDialog( → OutlinedAlertDialog( unchanged. The
|
||||
|
||||
@@ -11,8 +11,8 @@ import kotlin.math.max
|
||||
*
|
||||
* Walks the PE resource directory to the first RT_GROUP_ICON, picks the largest /
|
||||
* deepest-colour entry it references, then decodes that RT_ICON image. Icon images
|
||||
* are either PNG (Vista+) — decoded directly — or a classic DIB (BITMAPINFOHEADER +
|
||||
* XOR pixels + 1-bpp AND mask) — decoded by hand here.
|
||||
* are either PNG (Vista+) - decoded directly - or a classic DIB (BITMAPINFOHEADER +
|
||||
* XOR pixels + 1-bpp AND mask) - decoded by hand here.
|
||||
*
|
||||
* Everything is best-effort: any malformed/oversized/unsupported input returns null.
|
||||
*/
|
||||
|
||||
@@ -30,9 +30,9 @@ data class StorageRoot(
|
||||
* sandbox) even though it is mounted and healthy. The framework knows about it regardless, so the
|
||||
* volume set is built from several independent sources:
|
||||
*
|
||||
* 1. [StorageManager.getStorageVolumes] — the authoritative list, read over Binder.
|
||||
* 2. [Context.getExternalFilesDirs] — per-app directories, granted separately from shared storage.
|
||||
* 3. `/storage` and `/mnt/media_rw` directory listings — the filesystem view, as a backstop.
|
||||
* 1. [StorageManager.getStorageVolumes] - the authoritative list, read over Binder.
|
||||
* 2. [Context.getExternalFilesDirs] - per-app directories, granted separately from shared storage.
|
||||
* 3. `/storage` and `/mnt/media_rw` directory listings - the filesystem view, as a backstop.
|
||||
*
|
||||
* Those sources overlap heavily: one SD card is reachable as `/storage/<uuid>`, as
|
||||
* `/mnt/media_rw/<uuid>`, and via its app-specific directory, and typically only some of them are
|
||||
@@ -71,7 +71,7 @@ object StorageRoots {
|
||||
val appDirs = appSpecificDirs(context)
|
||||
val storageManager = context.getSystemService(StorageManager::class.java)
|
||||
|
||||
// 1 + 2 — framework-reported volumes. Authoritative, and unaffected by our mount view.
|
||||
// 1 + 2 - framework-reported volumes. Authoritative, and unaffected by our mount view.
|
||||
storageManager?.storageVolumes.orEmpty()
|
||||
.filter { isMounted(it.state) }
|
||||
.forEach { volume ->
|
||||
@@ -92,7 +92,7 @@ object StorageRoots {
|
||||
appDirs.filter { belongsTo(storageManager!!, it, volume) }.forEach { entry.seeds += it }
|
||||
}
|
||||
|
||||
// 3 — filesystem backstop, for any volume the framework did not report.
|
||||
// 3 - filesystem backstop, for any volume the framework did not report.
|
||||
File("/storage").listFiles().orEmpty().forEach { child ->
|
||||
if (!child.isDirectory || child.name == "self" || child.name == "emulated") return@forEach
|
||||
volumeOf(child.name).candidates += child
|
||||
@@ -179,7 +179,7 @@ object StorageRoots {
|
||||
|
||||
/**
|
||||
* Names a volume the way the rest of the file manager does. The framework description is only
|
||||
* used when it says something more useful than "SD card" — on some devices it is a bare disk
|
||||
* used when it says something more useful than "SD card" - on some devices it is a bare disk
|
||||
* name like "android", which tells the user nothing about which card they are looking at.
|
||||
*/
|
||||
private fun labelFor(context: Context, volume: StorageVolume, uuid: String?): String {
|
||||
|
||||
@@ -20,7 +20,7 @@ object FrameGen {
|
||||
|
||||
/** Win-FG's Performance preset, pinned: changing it live rebuilds the whole chain. */
|
||||
private const val WINFG_PERF_PRESET = 2
|
||||
/** Bidirectional flow with the occlusion gate — the chain's own default. */
|
||||
/** Bidirectional flow with the occlusion gate - the chain's own default. */
|
||||
private const val WINFG_MODEL = 4
|
||||
/** Optical-flow resolution as a fraction of the output; Bannerlator's per-engine defaults. */
|
||||
private const val FLOW_SCALE_WINFG = 0.60f
|
||||
@@ -39,7 +39,7 @@ object FrameGen {
|
||||
.putString("engine", engine).putInt("multiplier", multiplier).apply()
|
||||
}
|
||||
|
||||
/** "Off", "Win-FG 2×", "LSFG 3×" — for the main screen. */
|
||||
/** "Off", "Win-FG 2×", "LSFG 3×" - for the main screen. */
|
||||
fun label(context: Context): String = when (engine(context)) {
|
||||
ENGINE_WINFG -> "Win-FG ${multiplier(context)}×"
|
||||
ENGINE_LSFG -> "LSFG ${multiplier(context)}×"
|
||||
@@ -84,12 +84,12 @@ object FrameGen {
|
||||
|
||||
/**
|
||||
* Off, or an engine at 2x/3x/4x; saved on pick, then [onChanged]. The LSFG rows are greyed
|
||||
* and unpickable until Lossless Scaling is installed in the Steam client — the engine cannot
|
||||
* and unpickable until Lossless Scaling is installed in the Steam client - the engine cannot
|
||||
* run without its DLL, so offering it would only produce a setting that silently does nothing.
|
||||
*/
|
||||
fun showPicker(activity: android.app.Activity, onChanged: () -> Unit) {
|
||||
val lsfgReady = LsfgNative.isInstalled(activity)
|
||||
val lsfgNote = if (lsfgReady) "" else " — install Lossless Scaling in Steam"
|
||||
val lsfgNote = if (lsfgReady) "" else " - install Lossless Scaling in Steam"
|
||||
val choices = listOf("Off",
|
||||
"Win-FG 2×", "Win-FG 3×", "Win-FG 4×",
|
||||
"LSFG 2×$lsfgNote", "LSFG 3×$lsfgNote", "LSFG 4×$lsfgNote")
|
||||
|
||||
@@ -13,11 +13,11 @@ import java.util.List;
|
||||
* choice per mode covers all of it.
|
||||
*
|
||||
* <p>Distinct from the Android driver the app's compositor loads to put the finished frame on the
|
||||
* screen ({@link TurnipDriver}), which every session ends in — and NOT interchangeable with it:
|
||||
* screen ({@link TurnipDriver}), which every session ends in - and NOT interchangeable with it:
|
||||
* these are glibc processes, that one is bionic.
|
||||
*
|
||||
* <p>"" (the default) means the driver the runtime was built with, at
|
||||
* {@code usr/lib/libvulkan_freedreno.so} inside it — nothing is set and nothing is touched.
|
||||
* {@code usr/lib/libvulkan_freedreno.so} inside it - nothing is set and nothing is touched.
|
||||
* Otherwise the value is an imported driver's id ({@link LinuxVulkanDriverManager}), and the
|
||||
* session is handed that driver's ICD manifest in {@code BL_VK_DRIVER}: the session script points
|
||||
* the Vulkan loader at it with {@code VK_DRIVER_FILES} after checking both the manifest and the
|
||||
@@ -47,13 +47,13 @@ public final class LinuxVulkanDriver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Label for a stored value. An id that is no longer installed says so — the launch path falls
|
||||
* Label for a stored value. An id that is no longer installed says so - the launch path falls
|
||||
* back to the runtime's own driver for it.
|
||||
*/
|
||||
public static String optionLabel(Context context, String value) {
|
||||
if (value == null || value.isEmpty()) return "Runtime default (built into the runtime)";
|
||||
LinuxVulkanDriverManager m = new LinuxVulkanDriverManager(context);
|
||||
if (!m.isInstalled(value)) return value + " (imported, missing — uses the runtime default)";
|
||||
if (!m.isInstalled(value)) return value + " (imported, missing - uses the runtime default)";
|
||||
String ver = m.getDriverVersion(value);
|
||||
return m.getDriverName(value) + (ver.isEmpty() ? "" : " " + ver) + " (imported)";
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import java.util.zip.ZipInputStream;
|
||||
|
||||
/**
|
||||
* Imported LINUX Vulkan drivers: glibc Turnip ICDs ({@code Turnip-<tag>[-variant]-Linux.zip} from
|
||||
* Banners-Turnip) for the runtime — the gamescope session that runs Valve's native ARM64 Steam
|
||||
* Banners-Turnip) for the runtime - the gamescope session that runs Valve's native ARM64 Steam
|
||||
* client, and the labwc desktop beside it. This is the driver that DRAWS there: the client's own UI
|
||||
* (OpenGL through the runtime's Zink), every game the client launches (D3D through Proton's
|
||||
* DXVK/VKD3D) and everything on the desktop. Putting the frame on the screen stays the Android
|
||||
@@ -31,7 +31,7 @@ import java.util.zip.ZipInputStream;
|
||||
*
|
||||
* <p>Not interchangeable with that one: the client and its games are glibc processes and cannot
|
||||
* load a bionic object at all, and this one cannot be loaded by the app. The check that separates
|
||||
* them is the libc the driver links against — {@code libc.so.6} here, {@code libc.so} there.
|
||||
* them is the libc the driver links against - {@code libc.so.6} here, {@code libc.so} there.
|
||||
*
|
||||
* <p>Layout (under the app's files dir, which the session sees by its full host path):
|
||||
* <pre>
|
||||
@@ -171,7 +171,7 @@ public class LinuxVulkanDriverManager {
|
||||
// are in the driver's .dynstr, so a byte scan is enough and needs no ELF parsing:
|
||||
// glibc's is versioned ("libc.so.6"), bionic's is not ("libc.so").
|
||||
if (!containsAscii(so, "libc.so.6")) {
|
||||
throw new IllegalArgumentException(soName + " is not a glibc driver — it links Android's libc. "
|
||||
throw new IllegalArgumentException(soName + " is not a glibc driver - it links Android's libc. "
|
||||
+ "The Linux runtime needs a \"-Linux\" zip; a plain or \"-Wayland\" Turnip cannot be "
|
||||
+ "loaded by the Steam client at all.");
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.io.File;
|
||||
*
|
||||
* <p>The 25 compute shaders are not redistributable. They live as resources inside the user's
|
||||
* paid {@code Lossless.dll} (Steam app 993090), which in this app the user installs from the
|
||||
* Steam client itself — so the DLL sits in the runtime's own Steam library. It is mapped
|
||||
* Steam client itself - so the DLL sits in the runtime's own Steam library. It is mapped
|
||||
* read-only and parsed as data, never loaded or executed. Extraction is slow enough to be worth
|
||||
* doing once (the DXBC chain is translated to SPIR-V), so the result is cached, keyed on the
|
||||
* DLL's size and hash. Call {@link #ensureCache} off the main thread.
|
||||
|
||||
@@ -27,19 +27,19 @@ import java.util.zip.ZipInputStream;
|
||||
* The Vulkan driver the in-app compositor runs on. It has to be Turnip: the system Adreno driver
|
||||
* does not implement VK_EXT_image_drm_format_modifier, so importing the dma-bufs gamescope hands
|
||||
* over fails and the session renders nothing. Two builds ship in the apk, one per Adreno
|
||||
* generation, and the GPU decides which is unpacked — unless the user has imported an AdrenoTools
|
||||
* generation, and the GPU decides which is unpacked - unless the user has imported an AdrenoTools
|
||||
* zip of their own and chosen it, which then wins.
|
||||
*
|
||||
* <p>The guest's own Turnip is a different copy entirely — a glibc build inside the rootfs, or an
|
||||
* <p>The guest's own Turnip is a different copy entirely - a glibc build inside the rootfs, or an
|
||||
* imported one ({@link LinuxVulkanDriverManager}). This one is the bionic build the app process
|
||||
* loads through adrenotools, and it is what every session ends in: whatever draws inside the
|
||||
* runtime, this is the driver that puts the frame on the panel.
|
||||
*
|
||||
* <p>Every driver, bundled or imported, lives under {@code files/graphics_driver/<id>/} with a
|
||||
* {@code meta.json} whose {@code libraryName} names the .so beside it — the AdrenoTools layout.
|
||||
* {@code meta.json} whose {@code libraryName} names the .so beside it - the AdrenoTools layout.
|
||||
* The import path is ported from Bannerlator's {@code AdrenotoolsManager} (GPL-3.0), including
|
||||
* the refusal of zips that belong in the Linux runtime list: a "-Linux" (glibc) Turnip names no
|
||||
* library on purpose, and installed here it would put the compositor on the system Vulkan — a
|
||||
* library on purpose, and installed here it would put the compositor on the system Vulkan - a
|
||||
* black screen wearing a driver's name.
|
||||
*/
|
||||
public final class TurnipDriver {
|
||||
@@ -52,11 +52,11 @@ public final class TurnipDriver {
|
||||
public static final List<String> BUNDLED = Collections.unmodifiableList(Arrays.asList(DRIVER_A7XX, DRIVER_A8XX));
|
||||
/** Overrides the pick, for a device we cannot reach: a7xx, a8xx or system. */
|
||||
private static final String OVERRIDE_FILE = "Download/steamdeck-driver";
|
||||
/** Stored choice meaning "pick by GPU" — the default, and what a removed import falls back to. */
|
||||
/** Stored choice meaning "pick by GPU" - the default, and what a removed import falls back to. */
|
||||
public static final String AUTO = "";
|
||||
|
||||
public static final String HELP_TEXT =
|
||||
"The driver the app's compositor puts frames on the screen with — the last step of every "
|
||||
"The driver the app's compositor puts frames on the screen with - the last step of every "
|
||||
+ "session, Steam or desktop. AdrenoTools zips only (vulkan.adXXXX.so): a \"-Linux\" Turnip "
|
||||
+ "belongs in the Linux runtime list above and is refused here. The compositor loads its "
|
||||
+ "driver once per app process, so a change takes effect after the app is fully closed and "
|
||||
@@ -70,7 +70,7 @@ public final class TurnipDriver {
|
||||
this.contentDir = new File(context.getFilesDir(), "graphics_driver");
|
||||
}
|
||||
|
||||
/** Absolute directory holding the driver, with a trailing slash — adrenotools wants both. */
|
||||
/** Absolute directory holding the driver, with a trailing slash - adrenotools wants both. */
|
||||
public String driverPath(String driverId) {
|
||||
return new File(contentDir, driverId).getAbsolutePath() + "/";
|
||||
}
|
||||
@@ -111,7 +111,7 @@ public final class TurnipDriver {
|
||||
return library != null && !library.isEmpty() && new File(new File(contentDir, driverId), library).isFile();
|
||||
}
|
||||
|
||||
/** Imported drivers only — the bundled two are listed by {@link #BUNDLED}. */
|
||||
/** Imported drivers only - the bundled two are listed by {@link #BUNDLED}. */
|
||||
public List<String> enumerateImported() {
|
||||
ArrayList<String> ids = new ArrayList<>();
|
||||
File[] dirs = contentDir.listFiles();
|
||||
@@ -141,7 +141,7 @@ public final class TurnipDriver {
|
||||
/**
|
||||
* Import an AdrenoTools zip. Returns the new driver id.
|
||||
* @throws IllegalArgumentException with a user-facing reason when the zip is not an AdrenoTools
|
||||
* driver — including when it is a Linux runtime driver, which says which list it belongs in;
|
||||
* driver - including when it is a Linux runtime driver, which says which list it belongs in;
|
||||
* {@link IOException} on read/extract failures.
|
||||
*/
|
||||
public String installFromZip(Uri zipUri, String displayName) throws IOException {
|
||||
@@ -220,7 +220,7 @@ public final class TurnipDriver {
|
||||
return libraryName + " is not a 64-bit AArch64 ELF shared library";
|
||||
// A glibc build that happens to carry a libraryName would load nothing in this process.
|
||||
if (LinuxVulkanDriverManager.containsAscii(library, "libc.so.6"))
|
||||
return libraryName + " links glibc — a \"-Linux\" Turnip, which belongs under \"Linux runtime driver\"";
|
||||
return libraryName + " links glibc - a \"-Linux\" Turnip, which belongs under \"Linux runtime driver\"";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ public final class TurnipDriver {
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's choice first — an imported driver, or one of the bundled two pinned by hand —
|
||||
* The user's choice first - an imported driver, or one of the bundled two pinned by hand -
|
||||
* then the Downloads override, then the GPU. A chosen import that has since been removed logs
|
||||
* and falls through, so a stale preference never blocks a session.
|
||||
*/
|
||||
@@ -291,7 +291,7 @@ public final class TurnipDriver {
|
||||
String model = gpuModel();
|
||||
Log.i(TAG, "gpu model: " + (model == null ? "unknown" : model));
|
||||
if (model != null) {
|
||||
// "Adreno750", "adreno_830" — the generation is the first digit of the three.
|
||||
// "Adreno750", "adreno_830" - the generation is the first digit of the three.
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d)\\d\\d").matcher(model);
|
||||
if (m.find()) {
|
||||
char generation = m.group(1).charAt(0);
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.nio.channels.FileChannel;
|
||||
* snapshot is published alongside under a seqlock so the native reader can replay
|
||||
* a full keyframe to heal any desync (open, ring overflow) without duplicate
|
||||
* input. Ring layout (magic/version/offsets) is a paired ABI with the native
|
||||
* {@code FakeInputRingHeader} — keep the two in lockstep.
|
||||
* {@code FakeInputRingHeader} - keep the two in lockstep.
|
||||
*/
|
||||
public class FakeInputWriter {
|
||||
private static final String TAG = "FakeInputWriter";
|
||||
@@ -73,7 +73,7 @@ public class FakeInputWriter {
|
||||
private static final int BUFFER_SIZE = EVENT_SIZE * MAX_EVENTS_PER_UPDATE;
|
||||
private static final int MAX_FAKE_INPUT_SLOTS = 4;
|
||||
|
||||
// Ring layout — MUST match FakeInputRingHeader in fakeinput.cpp.
|
||||
// Ring layout - MUST match FakeInputRingHeader in fakeinput.cpp.
|
||||
private static final int RING_CAPACITY_EVENTS = 4096;
|
||||
private static final int RING_HEADER_SIZE = 64;
|
||||
private static final int RING_SIZE = RING_HEADER_SIZE + (RING_CAPACITY_EVENTS * EVENT_SIZE);
|
||||
|
||||
@@ -69,9 +69,9 @@ class OnScreenControls(context: Context, private val pad: PadBridge) : View(cont
|
||||
|
||||
private val controls = listOf(
|
||||
Control("up", "▲", -1, 0, -1, dp(26f)),
|
||||
Control("right", "▶", -1, 1, -1, dp(26f)),
|
||||
Control("right", ">", -1, 1, -1, dp(26f)),
|
||||
Control("down", "▼", -1, 2, -1, dp(26f)),
|
||||
Control("left", "◀", -1, 3, -1, dp(26f)),
|
||||
Control("left", "<", -1, 3, -1, dp(26f)),
|
||||
Control("ls", "L", -1, -1, 0, dp(48f)),
|
||||
Control("rs", "R", -1, -1, 1, dp(48f)),
|
||||
Control("a", "A", 0, -1, -1, dp(30f)),
|
||||
|
||||
@@ -71,7 +71,7 @@ public final class PadBridge {
|
||||
/** True when the device this event came from is a gamepad or joystick, not the touchscreen. */
|
||||
public static boolean isFromController(InputDevice device) {
|
||||
if (device == null) return false;
|
||||
// A virtual device is the system's own synthetic input, not a pad somebody is holding —
|
||||
// A virtual device is the system's own synthetic input, not a pad somebody is holding -
|
||||
// counting it would hide the on-screen controls with nothing to replace them.
|
||||
if (device.isVirtual()) return false;
|
||||
int sources = device.getSources();
|
||||
|
||||
@@ -17,7 +17,7 @@ import kotlin.math.roundToInt
|
||||
* - two fingers up / down .......... scroll wheel
|
||||
*
|
||||
* The listener gets pointer positions in the view's pixels; the activity maps them onto the
|
||||
* compositor. Mouse events are not handled here — a mouse has real buttons.
|
||||
* compositor. Mouse events are not handled here - a mouse has real buttons.
|
||||
*/
|
||||
class PointerGestures(
|
||||
private val slopPx: Float,
|
||||
|
||||
@@ -8,8 +8,8 @@ import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* The hosted packages that go into the runtime on request — the desktop, the emulators, and the
|
||||
* builds we mirror or make ourselves — described by desktop.json beside the runtime's own catalog.
|
||||
* The hosted packages that go into the runtime on request - the desktop, the emulators, and the
|
||||
* builds we mirror or make ourselves - described by desktop.json beside the runtime's own catalog.
|
||||
*
|
||||
* Two kinds. A `tar` extracts over the rootfs like the runtime itself (the same extractor, links
|
||||
* and modes preserved). An `appimage` is one file dropped in /opt/appimages with a .desktop entry
|
||||
@@ -68,7 +68,7 @@ object DesktopCatalog {
|
||||
if (entry.sha256.isNotEmpty()) {
|
||||
listener?.onProgress("Verifying", -1)
|
||||
val actual = LinuxRuntimeInstaller.sha256(download)
|
||||
if (!entry.sha256.equals(actual, ignoreCase = true)) return "Checksum mismatch — nothing was changed"
|
||||
if (!entry.sha256.equals(actual, ignoreCase = true)) return "Checksum mismatch - nothing was changed"
|
||||
}
|
||||
listener?.onProgress("Installing ${entry.name}", -1)
|
||||
when (entry.kind) {
|
||||
|
||||
@@ -73,7 +73,7 @@ class LinuxNetworkLinkComponent(
|
||||
|
||||
/**
|
||||
* The guest's resolver, from the network it is actually on. glibc reads /etc/resolv.conf,
|
||||
* and the image ships two public servers there, which is why Steam resolves at all — but a
|
||||
* and the image ships two public servers there, which is why Steam resolves at all - but a
|
||||
* captive portal, a private-DNS network or a v6-only carrier wants the network's own
|
||||
* servers. Those come first, IPv4 before IPv6 and never a link-local one (Android lists
|
||||
* fe80:: resolvers a guest cannot reach; handing Bannerlator's Wine guests exactly that was
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.List;
|
||||
* The glibc arm64 rootfs at {@code files/linuxfs} and the proot invocation that runs a program in
|
||||
* it as this app's own uid. It is a second runtime beside the Wine imagefs, not a container: no
|
||||
* Wine, no box64, no FEX. proot is packaged as {@code libproot.so} so the installer places it, with
|
||||
* its loader, in the native library directory — the only place an app on targetSdk 28 may execute
|
||||
* its loader, in the native library directory - the only place an app on targetSdk 28 may execute
|
||||
* a file from.
|
||||
*
|
||||
* <p>Ported from WinNative's gamescope runtime (GPL-3.0).
|
||||
@@ -61,7 +61,7 @@ public final class LinuxRuntime {
|
||||
/**
|
||||
* proot, preferred from the installed runtime and falling back to the copy in the apk.
|
||||
*
|
||||
* <p>It is an Android binary rather than part of the rootfs — it is what creates the rootfs —
|
||||
* <p>It is an Android binary rather than part of the rootfs - it is what creates the rootfs -
|
||||
* but it travels in the runtime tarball so that reinstalling the app cannot replace the one
|
||||
* binary everything else depends on, and so a device with no working packaged proot can still
|
||||
* run the runtime.
|
||||
@@ -106,8 +106,8 @@ public final class LinuxRuntime {
|
||||
|
||||
/**
|
||||
* The proot command line running {@code guestCommand} inside the rootfs. Host paths the session
|
||||
* needs — the app's files directory for the compositor and audio sockets, external storage for
|
||||
* the user's games — are bound at their own paths, so nothing on either side needs translating
|
||||
* needs - the app's files directory for the compositor and audio sockets, external storage for
|
||||
* the user's games - are bound at their own paths, so nothing on either side needs translating
|
||||
* and proot never touches the fds a dma-buf travels in. Android has no /dev/shm; a directory
|
||||
* under the cache stands in, which glibc's shm_open and Chromium's shared memory accept.
|
||||
*/
|
||||
@@ -116,7 +116,7 @@ public final class LinuxRuntime {
|
||||
return command(context, sessionRoot, runtimeDir, externalStorage, null, guestCommand);
|
||||
}
|
||||
|
||||
/** As above, plus {@code host:guest} bind specs — the installed games handed to Steam. */
|
||||
/** As above, plus {@code host:guest} bind specs - the installed games handed to Steam. */
|
||||
public static List<String> command(Context context, File sessionRoot, File runtimeDir,
|
||||
File externalStorage, List<String> extraBinds,
|
||||
List<String> guestCommand) {
|
||||
@@ -205,8 +205,8 @@ public final class LinuxRuntime {
|
||||
}
|
||||
|
||||
/**
|
||||
* An app process may not open {@code /dev/dri} — the nodes exist but are labelled
|
||||
* {@code graphics_device}, which stock policy grants surfaceflinger and not us — yet libdrm and
|
||||
* An app process may not open {@code /dev/dri} - the nodes exist but are labelled
|
||||
* {@code graphics_device}, which stock policy grants surfaceflinger and not us - yet libdrm and
|
||||
* everything built on it identify a GPU by its render node, and gamescope refuses to offer
|
||||
* linux-dmabuf without one. The KGSL device Turnip actually drives ({@code gpu_device}, which we
|
||||
* may open) stands in: it appears as a render node with the sysfs entries libdrm reads, and our
|
||||
|
||||
@@ -8,8 +8,8 @@ import java.io.File
|
||||
/**
|
||||
* Starting the Steam client signed out of Valve's servers.
|
||||
*
|
||||
* The client can run without a connection — installed games launch, Proton and the frame-gen are
|
||||
* all local — but only if it has credentials from an earlier successful sign-in. Two keys in
|
||||
* The client can run without a connection - installed games launch, Proton and the frame-gen are
|
||||
* all local - but only if it has credentials from an earlier successful sign-in. Two keys in
|
||||
* loginusers.vdf decide what it does when it cannot reach a CM: WantsOfflineMode makes it go
|
||||
* offline on its own, SkipOfflineModeWarning stops it asking first. Both are read once, while the
|
||||
* client starts, which is why this belongs to the main screen and not to the in-session drawer:
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.concurrent.Executors
|
||||
* ADPF (Android 12+): a PerformanceHintManager session over the compositor thread, told the
|
||||
* panel's frame period as its target and each presented frame's interval as the actual. When a
|
||||
* frame runs long the power HAL raises CPU clocks for the next one at once, instead of the
|
||||
* governor waiting for the load average to climb — the difference between a menu that stutters
|
||||
* governor waiting for the load average to climb - the difference between a menu that stutters
|
||||
* on the first scroll and one that does not.
|
||||
*
|
||||
* The session may only carry this process's threads, so the guest (gamescope, the Steam client
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* The numbers behind the top-right line: the game's own frame rate and, when frame generation
|
||||
* is on, what the screen is actually being shown — "60 → 118 fps" is the proof that the engine
|
||||
* is on, what the screen is actually being shown - "60 → 118 fps" is the proof that the engine
|
||||
* is doing something, and "FG starting" or a reason is the proof that it is not.
|
||||
*
|
||||
* The base rate is counted here from the compositor's per-frame callback, so it is right with or
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.steamdeck.launcher.runtime.LinuxRuntime
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Third-party Proton builds for the client — GE-Proton and proton-cachyos, both published as
|
||||
* Third-party Proton builds for the client - GE-Proton and proton-cachyos, both published as
|
||||
* native ARM64 tarballs. The runtime's own registrar (bannerlator-proton-extra) downloads and
|
||||
* installs one into the client's compatibilitytools.d and patches it past pressure-vessel; this
|
||||
* side only leaves it the request, one line in ~/.bl-proton-extra, which the session script reads
|
||||
|
||||
@@ -7,14 +7,14 @@ import android.widget.Toast
|
||||
|
||||
/**
|
||||
* Other apps on the device that sign into the user's Steam account with their own client. Steam
|
||||
* allows one session per account: the moment one of them signs in, ours is signed out — which
|
||||
* allows one session per account: the moment one of them signs in, ours is signed out - which
|
||||
* shows up as "Session Replaced" in the client's log 2-3 seconds after every login, and looks
|
||||
* like a bug in this app.
|
||||
*
|
||||
* Installed is the most a normal app can know (since Android 7 `getRunningAppProcesses()` returns
|
||||
* only the caller's own), so the list is checked for presence and then asked to stop.
|
||||
* `killBackgroundProcesses` is a normal permission: it ends an app's background processes and
|
||||
* leaves anything in the foreground alone — and background is exactly the case that keeps
|
||||
* leaves anything in the foreground alone - and background is exactly the case that keeps
|
||||
* happening, because GameHub declares boot receivers and is running from the moment the phone
|
||||
* starts without ever having been opened. Telling someone to close an app they never opened is
|
||||
* not much help. Ported from Bannerlator's Linux session (GPL-3.0).
|
||||
@@ -56,8 +56,8 @@ object RivalClients {
|
||||
}
|
||||
Toast.makeText(
|
||||
context,
|
||||
if (asked) "Closed $who in the background — it signs into your Steam account."
|
||||
else "If $who is open, close it first — it signs into your Steam account and will sign this client out.",
|
||||
if (asked) "Closed $who in the background - it signs into your Steam account."
|
||||
else "If $who is open, close it first - it signs into your Steam account and will sign this client out.",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
|
||||
@@ -107,8 +107,8 @@ object SessionFiles {
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the session writes its log. Downloads is the point — a failed run is handed over as a
|
||||
* folder rather than dug out of app-private storage — but the session script redirects its own
|
||||
* Where the session writes its log. Downloads is the point - a failed run is handed over as a
|
||||
* folder rather than dug out of app-private storage - but the session script redirects its own
|
||||
* output there with `exec`, and a redirection a non-interactive shell cannot open ends that
|
||||
* shell. So a public directory is used only once it is proven writable; otherwise the app's
|
||||
* own files directory, which is bound into the session anyway, stands in.
|
||||
|
||||
@@ -42,7 +42,7 @@ import java.util.Locale
|
||||
* keep all three alive while the app is not on screen.
|
||||
*
|
||||
* The session deliberately does **not** belong to the activity. Android demotes a process the
|
||||
* moment it loses its last visible activity, and the low-memory killer then reaps the guest — so
|
||||
* moment it loses its last visible activity, and the low-memory killer then reaps the guest - so
|
||||
* leaving Big Picture to answer a message would come back to a dead Steam. A foreground service
|
||||
* holds the process at perceptible priority, a partial wake lock keeps the CPU from dropping the
|
||||
* guest's threads, and a high-performance WiFi lock keeps the radio out of power-save so a
|
||||
@@ -305,8 +305,8 @@ class SessionService : Service() {
|
||||
SessionState.fakeInputDir = fakeInputDir
|
||||
}
|
||||
// The desktop is wlroots (labwc), and wlroots allocates its buffers through gbm on a real
|
||||
// DRM render node. Ours is a KGSL stand-in that gbm cannot use — labwc dies at "unable to
|
||||
// create allocator" — so the desktop shell is composited by pixman (software, a shm
|
||||
// DRM render node. Ours is a KGSL stand-in that gbm cannot use - labwc dies at "unable to
|
||||
// create allocator" - so the desktop shell is composited by pixman (software, a shm
|
||||
// allocator, no DRM). Accelerated clients on it pay a CPU copy; a 2D emulator does not
|
||||
// notice, a demanding one does. steamdeck-wlr-renderer in Downloads (pixman/vulkan/gles2)
|
||||
// overrides it, for trying acceleration on a device that has a real node.
|
||||
@@ -561,7 +561,7 @@ class SessionService : Service() {
|
||||
// seccomp-trapped syscall then failed with ENOSYS, they spun on retries at a full core
|
||||
// for over an hour, and the reaper could not even open /proc to kill them. So: SIGTERM,
|
||||
// a grace for proot's own cleanup, SIGKILL only if it will not go, then a sweep of the
|
||||
// tree it had — snapshotted first, each pid checked against its start time so a number
|
||||
// tree it had - snapshotted first, each pid checked against its start time so a number
|
||||
// reused by a new process is never touched. Same shape as Bannerlator's fix (4509d788).
|
||||
if (sessionPid != -1) {
|
||||
val prootPid = sessionPid
|
||||
@@ -595,7 +595,7 @@ class SessionService : Service() {
|
||||
* guest would survive as an orphan holding the rootfs and the GPU. Treat the swipe as "quit".
|
||||
*/
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
Log.i(TAG, "task removed — ending the session")
|
||||
Log.i(TAG, "task removed - ending the session")
|
||||
stopSession(0)
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
@@ -620,7 +620,7 @@ class SessionService : Service() {
|
||||
}
|
||||
Log.i(TAG, "wake lock held=${wakeLock?.isHeld}")
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "no wake lock (${t.message}) — the session may be killed in the background")
|
||||
Log.w(TAG, "no wake lock (${t.message}) - the session may be killed in the background")
|
||||
}
|
||||
try {
|
||||
val wifi = applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
|
||||
@@ -634,7 +634,7 @@ class SessionService : Service() {
|
||||
} catch (t: Throwable) {
|
||||
// A partial wake lock keeps the process alive but does not stop WiFi power-save from
|
||||
// throttling a backgrounded download to nothing, which is what this lock is for.
|
||||
Log.w(TAG, "no wifi lock (${t.message}) — a backgrounded download may stall")
|
||||
Log.w(TAG, "no wifi lock (${t.message}) - a backgrounded download may stall")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,6 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
|
||||
/**
|
||||
* The pointer, drawn by the app. The nested desktop asks its parent compositor to show a cursor
|
||||
* and ours does not, and the Steam session has none either; this is the arrow for both, at the
|
||||
* position the app last sent. Takes no touches.
|
||||
*/
|
||||
@Composable
|
||||
fun CursorOverlay(position: Offset, visible: Boolean, scale: Float) {
|
||||
if (!visible) return
|
||||
|
||||
@@ -19,19 +19,12 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/** One hosted package as the dialog shows it. */
|
||||
class PackageRow(
|
||||
val id: String, val name: String, val tier: Int, val version: String, val size: String,
|
||||
val notes: String, val installed: String?,
|
||||
/** What the package can launch on its own under gamescope: label to path inside the runtime. */
|
||||
val launchers: List<Pair<String, String>> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* The desktop and everything that goes on it, installed into the runtime on request. Tier 1 is
|
||||
* packaged natively for this hardware; tier 2 is an upstream ARM64 build we mirror and expect
|
||||
* little of on a phone; tier 3 is built by us from source.
|
||||
*/
|
||||
@Composable
|
||||
fun DesktopAppsDialog(
|
||||
rows: List<PackageRow>?,
|
||||
@@ -39,7 +32,6 @@ fun DesktopAppsDialog(
|
||||
busyPercent: Int,
|
||||
onInstall: (String) -> Unit,
|
||||
onRemove: (String) -> Unit,
|
||||
/** Start [path] as its own session under gamescope. */
|
||||
onLaunch: (path: String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
@@ -58,7 +50,7 @@ fun DesktopAppsDialog(
|
||||
rows == null -> Text("Could not reach the package catalog", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
rows.isEmpty() -> Text("Loading the catalog…", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
else -> Text(
|
||||
"▶ starts an emulator fullscreen under gamescope, with the GPU — on the desktop, Vulkan programs cannot draw. " +
|
||||
" starts an emulator fullscreen under gamescope, with the GPU - on the desktop, Vulkan programs cannot draw. " +
|
||||
"ROMs are at /root/ROMs, the phone at /root/Storage.",
|
||||
style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -70,8 +62,8 @@ fun DesktopAppsDialog(
|
||||
Text(
|
||||
when (row.tier) {
|
||||
1 -> "Native for this hardware"
|
||||
2 -> "Upstream ARM64 builds — experimental on a phone"
|
||||
else -> "Built from source — experimental"
|
||||
2 -> "Upstream ARM64 builds - experimental on a phone"
|
||||
else -> "Built from source - experimental"
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
@@ -83,7 +75,7 @@ fun DesktopAppsDialog(
|
||||
Text(row.name, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
(if (row.installed != null) "installed ${row.installed}" else "${row.version} · ${row.size}") +
|
||||
(if (row.notes.isNotEmpty()) " — ${row.notes}" else ""),
|
||||
(if (row.notes.isNotEmpty()) " - ${row.notes}" else ""),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -91,14 +83,11 @@ fun DesktopAppsDialog(
|
||||
if (row.installed != null) TextButton(enabled = busyStage == null, onClick = { onRemove(row.id) }) { Text("Remove") }
|
||||
else TextButton(enabled = busyStage == null, onClick = { onInstall(row.id) }) { Text("Install") }
|
||||
}
|
||||
// An emulator starts as a session of its own, fullscreen under gamescope: that
|
||||
// is where the GPU is. On the desktop (software-composited) a Vulkan program
|
||||
// has no swapchain to draw into and dies at its first frame.
|
||||
if (row.installed != null && row.launchers.isNotEmpty()) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(start = 12.dp, bottom = 2.dp)) {
|
||||
for ((label, path) in row.launchers) {
|
||||
TextButton(enabled = busyStage == null, onClick = { onLaunch(path) }) {
|
||||
Text("▶ $label", style = MaterialTheme.typography.labelMedium)
|
||||
Text(" $label", style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,6 @@ import com.steamdeck.launcher.gpu.FrameGen
|
||||
import java.io.File
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/** Everything the front end shows; the activity owns the values and the work behind them. */
|
||||
class FrontEndState(
|
||||
val installed: String?,
|
||||
val ready: Boolean,
|
||||
@@ -127,14 +126,11 @@ class FrontEndState(
|
||||
val logsEnabled: Boolean,
|
||||
val steamGames: List<Library.SteamGame>,
|
||||
val emulators: List<Library.Emulator>,
|
||||
/** A session alive in the background: what it is, or null. */
|
||||
val running: String?,
|
||||
val frameGenEngine: String = FrameGen.ENGINE_OFF,
|
||||
val frameGenMultiplier: Int = 2,
|
||||
val lsfgReady: Boolean = false,
|
||||
/** A page shown in the pane instead of the selection ("settings:steam", "settings:lxqt", "performance"), or null. */
|
||||
val pageKey: String? = null,
|
||||
/** The colour theme in use (ui/Themes ids). */
|
||||
val theme: String = Themes.PAPER,
|
||||
)
|
||||
|
||||
@@ -159,18 +155,11 @@ class FrontEndActions(
|
||||
val onOffline: () -> Unit,
|
||||
val onEmulatorHelp: () -> Unit,
|
||||
val onCredits: () -> Unit,
|
||||
/** Leaves the page in the pane (back key, the rail, or the page's own Back). */
|
||||
val onPageBack: () -> Unit = {},
|
||||
val onTheme: (String) -> Unit = {},
|
||||
)
|
||||
|
||||
// ───────────────────────────── Motion ─────────────────────────────
|
||||
|
||||
/**
|
||||
* One place for every duration and spring on this screen. The durations honour the system
|
||||
* animator scale, so a device set to "no animations" in developer options collapses them to
|
||||
* an instant snap instead of ignoring the user's choice.
|
||||
*/
|
||||
internal object Motion {
|
||||
var scale = 1f
|
||||
val Ease = CubicBezierEasing(0.2f, 0.8f, 0.2f, 1f)
|
||||
@@ -183,15 +172,10 @@ internal object Motion {
|
||||
private val Shape10 = RoundedCornerShape(10.dp)
|
||||
private val Shape12 = RoundedCornerShape(12.dp)
|
||||
|
||||
/** A colour for a thing that has no art: stable per name, so it does not change between visits. */
|
||||
private fun hueOf(name: String) = (name.hashCode().toUInt() % 360u).toFloat()
|
||||
private fun tint(h: Float, s: Float = 0.7f, v: Float = 0.58f) = Color.hsv(h, s, v)
|
||||
private fun artBrush(h: Float) = Brush.linearGradient(listOf(tint(h), tint((h + 32f) % 360f, 0.65f, 0.30f), tint((h + 64f) % 360f, 0.6f, 0.14f)))
|
||||
|
||||
/**
|
||||
* A row that rises into place: the i-th row of a page starts 60 ms after the one before it, so
|
||||
* a page change reads as one cascade rather than a cut.
|
||||
*/
|
||||
@Composable
|
||||
internal fun Rise(i: Int, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||
val state = remember { MutableTransitionState(false) }.apply { targetState = true }
|
||||
@@ -203,7 +187,6 @@ internal fun Rise(i: Int, modifier: Modifier = Modifier, content: @Composable ()
|
||||
) { content() }
|
||||
}
|
||||
|
||||
/** Alpha + lift on first composition, delayed by the item's index: sub-list children stagger in. */
|
||||
@Composable
|
||||
private fun Modifier.staggerIn(i: Int): Modifier {
|
||||
val t = remember { Animatable(0f) }
|
||||
@@ -211,7 +194,6 @@ private fun Modifier.staggerIn(i: Int): Modifier {
|
||||
return graphicsLayer { alpha = t.value; translationY = (1f - t.value) * 10.dp.toPx() }
|
||||
}
|
||||
|
||||
/** A diagonal band of light that sweeps across the content once, whenever [trigger] turns on. */
|
||||
@Composable
|
||||
private fun Modifier.shine(trigger: Boolean, strength: Float = 0.22f): Modifier {
|
||||
val x = remember { Animatable(-1f) }
|
||||
@@ -232,20 +214,10 @@ private fun Modifier.shine(trigger: Boolean, strength: Float = 0.22f): Modifier
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── Screen ─────────────────────────────
|
||||
|
||||
/**
|
||||
* The front end: a rail of what can be launched - Steam and its games, the desktop and its
|
||||
* emulators and their games - and the chosen thing on the right with its launch button. A
|
||||
* session running in the background is the first thing on the rail, and tapping it goes back
|
||||
* to it. Landscape puts the rail beside the content; a narrow screen puts it above.
|
||||
*/
|
||||
@Composable
|
||||
fun FrontEndScreen(s: FrontEndState, a: FrontEndActions, page: (@Composable () -> Unit)? = null) {
|
||||
var selected by rememberSaveable { mutableStateOf("steam") }
|
||||
// Every category starts folded when the app opens: three headers and their counts, and the
|
||||
// rail unfolds only what the user reaches for. (rememberSaveable keeps a fold across a
|
||||
// rotation, not across launches, which is the point.)
|
||||
var openDesktop by rememberSaveable { mutableStateOf(false) }
|
||||
var openSteam by rememberSaveable { mutableStateOf(false) }
|
||||
var openEmu by rememberSaveable { mutableStateOf("") }
|
||||
@@ -278,7 +250,6 @@ fun FrontEndScreen(s: FrontEndState, a: FrontEndActions, page: (@Composable () -
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── Rail ─────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun Rail(
|
||||
@@ -311,7 +282,6 @@ private fun Rail(
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp))
|
||||
}
|
||||
|
||||
// A session in the background: the way back to it, first.
|
||||
var lastRunning by remember { mutableStateOf("") }
|
||||
if (s.running != null) lastRunning = s.running
|
||||
AnimatedVisibility(
|
||||
@@ -320,7 +290,6 @@ private fun Rail(
|
||||
exit = shrinkVertically(Motion.tw(220)) + fadeOut(Motion.tw(180)),
|
||||
) { RunningTile(lastRunning, a.onResume) }
|
||||
|
||||
// The rail's selection is one pill that glides between rows; the rows only recolour.
|
||||
val pal = LocalPalette.current
|
||||
var navOrigin by remember { mutableStateOf(Offset.Zero) }
|
||||
val positions = remember { mutableStateMapOf<String, Rect>() }
|
||||
@@ -370,8 +339,6 @@ private fun Rail(
|
||||
NavItem("Settings", "settings:lxqt", selected == "settings:lxqt", small = true, tiny = true, muted = true, i = s.emulators.count { it.installed }, register = register, unregister = unregister) { a.onDesktopSettings() }
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
// Setup: the same fold as Steam and Desktop. A row that holds a value opens a small menu
|
||||
// in place; a row that opens a page lights up while the page is shown.
|
||||
val menus = remember { MenuHost() }
|
||||
val item: @Composable (String, String?, Int, String?, () -> Unit) -> Unit = { label, value, i, key, act ->
|
||||
NavItem(label, key ?: "x", key != null && selected == key, small = true, tiny = true, muted = true, value = value, i = i, register = register, unregister = unregister) { act() }
|
||||
@@ -385,7 +352,6 @@ private fun Rail(
|
||||
item("Frame generation", s.frameGenLabel, 3, null) { menus.open = "fg" }
|
||||
AnchoredMenu(
|
||||
menus.open == "fg", onDismiss = { if (menus.open == "fg") menus.open = null }, title = "Frame generation",
|
||||
note = "Extra frames between the real ones on the way to the screen, so 30 fps looks like 60. Takes effect at once, mid-game included.",
|
||||
) {
|
||||
val need = if (s.lsfgReady) null else "install Lossless Scaling in Steam"
|
||||
MenuItem("Off", checked = s.frameGenEngine == FrameGen.ENGINE_OFF) { a.onFrameGenPick(FrameGen.ENGINE_OFF, 2); menus.open = null }
|
||||
@@ -399,7 +365,6 @@ private fun Rail(
|
||||
item("Session logs", if (s.logsEnabled) "on" else "off", 6, null) { menus.open = "logs" }
|
||||
AnchoredMenu(
|
||||
menus.open == "logs", onDismiss = { if (menus.open == "logs") menus.open = null }, title = "Session logs",
|
||||
note = "Each session leaves a folder in Downloads: the guest log, the device and network reports, the app's own log.",
|
||||
) {
|
||||
MenuItem("On", checked = s.logsEnabled) { if (!s.logsEnabled) a.onLogs(); menus.open = null }
|
||||
MenuItem("Off", checked = !s.logsEnabled) { if (s.logsEnabled) a.onLogs(); menus.open = null }
|
||||
@@ -409,7 +374,6 @@ private fun Rail(
|
||||
item("Start offline", when { s.offlineAccount == null -> "sign in first"; s.offline -> "on"; else -> "off" }, 7, null) { if (s.offlineAccount != null) menus.open = "offline" }
|
||||
AnchoredMenu(
|
||||
menus.open == "offline", onDismiss = { if (menus.open == "offline") menus.open = null }, title = "Start offline",
|
||||
note = "The client starts as ${s.offlineAccount ?: "the saved account"} without the network: the library and installed games, no store.",
|
||||
) {
|
||||
MenuItem("On", checked = s.offline) { if (!s.offline) a.onOffline(); menus.open = null }
|
||||
MenuItem("Off", checked = !s.offline) { if (s.offline) a.onOffline(); menus.open = null }
|
||||
@@ -423,8 +387,8 @@ private fun Rail(
|
||||
}, 8, null, a.onRuntime)
|
||||
Box {
|
||||
item("Theme", Themes.byId(s.theme).label, 9, null) { menus.open = "theme" }
|
||||
AnchoredMenu(menus.open == "theme", onDismiss = { if (menus.open == "theme") menus.open = null }, title = "Theme", note = "The icon's three colours, dealt out three ways. Applies at once.") {
|
||||
for (t in Themes.all) MenuItem(t.label, checked = s.theme == t.id, detail = t.detail) { a.onTheme(t.id); menus.open = null }
|
||||
AnchoredMenu(menus.open == "theme", onDismiss = { if (menus.open == "theme") menus.open = null }, title = "Theme") {
|
||||
for (t in Themes.all) MenuItem(t.label, checked = s.theme == t.id) { a.onTheme(t.id); menus.open = null }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -469,7 +433,6 @@ private fun RunningTile(name: String, onResume: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
/** A sub-list that unfolds; its children stagger in through [NavItem]'s index. */
|
||||
@Composable
|
||||
private fun Sub(open: Boolean, content: @Composable () -> Unit) {
|
||||
val line = LocalPalette.current.line
|
||||
@@ -517,7 +480,7 @@ private fun NavItem(
|
||||
.padding(horizontal = 10.dp, vertical = if (small) 7.dp else 9.dp),
|
||||
) {
|
||||
if (caret != null) {
|
||||
Text("▶", fontSize = 9.sp, color = sub, modifier = Modifier.width(14.dp).rotate(rot))
|
||||
Text("›", fontSize = 16.sp, color = sub, modifier = Modifier.width(14.dp).rotate(rot))
|
||||
}
|
||||
Text(
|
||||
label, fontSize = if (tiny) 13.sp else if (small) 14.sp else 15.sp,
|
||||
@@ -532,9 +495,7 @@ private fun NavItem(
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── Pane ─────────────────────────────
|
||||
|
||||
/** The chosen thing, behind a blurred wash of its own colour; a change sinks the old page out and cascades the new one in. */
|
||||
@Composable
|
||||
private fun Pane(s: FrontEndState, selected: String, a: FrontEndActions, page: (@Composable () -> Unit)?, modifier: Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
@@ -591,8 +552,6 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
when {
|
||||
selected == "steam" -> {
|
||||
Rise(0) { Eyebrow("Steam") }
|
||||
Rise(1) { Title("Valve's native client, under gamescope") }
|
||||
Rise(2) { Lede("Big Picture with a controller, or the client's desktop UI. Installed games launch straight from the rail or the shelf below.") }
|
||||
Rise(3) {
|
||||
Actions {
|
||||
PrimaryButton("Play", enabled = s.ready && !s.busy, onClick = a.onPlay)
|
||||
@@ -601,7 +560,7 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
}
|
||||
}
|
||||
Rise(4) { SectionTitle("Installed", "${s.steamGames.size} game${if (s.steamGames.size == 1) "" else "s"}") }
|
||||
if (s.steamGames.isEmpty()) Rise(5) { Note("Nothing installed yet. Press Play, sign in, and install from the store; games appear here and on the left.") }
|
||||
if (s.steamGames.isEmpty()) Rise(5) { Note("No games installed.") }
|
||||
else Rise(5, Modifier.weight(1f).fillMaxWidth()) { ArtGrid(s.steamGames.map { g -> Tile(g.name, g.library, g.art, "steam:${g.appId}", null) { a.onSteamGame(g) } }) }
|
||||
}
|
||||
selected.startsWith("app:") -> {
|
||||
@@ -612,7 +571,6 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
Rise(2) {
|
||||
Row {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Lede("Starts the Steam session and launches the game straight away (steam://rungameid/${g.appId}).")
|
||||
Actions {
|
||||
PrimaryButton("Launch", enabled = s.ready && !s.busy) { a.onSteamGame(g) }
|
||||
Cog(a.onSteamSettings)
|
||||
@@ -631,8 +589,6 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
}
|
||||
selected == "desktop" -> {
|
||||
Rise(0) { Eyebrow("Desktop") }
|
||||
Rise(1) { Title("Linux desktop environment") }
|
||||
Rise(2) { Lede("Files, Firefox and the emulators' own windows. Emulators and their games launch from the rail, under gamescope, where the GPU is.") }
|
||||
Rise(3) {
|
||||
Actions {
|
||||
PrimaryButton(if (s.desktopInstalled) "Desktop" else "Install the desktop first", enabled = s.ready && !s.busy && s.desktopInstalled, onClick = a.onDesktop)
|
||||
@@ -642,7 +598,7 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
}
|
||||
Rise(4) { SectionTitle("Emulators", "${s.emulators.count { it.installed }} installed · ${s.emulators.count { !it.installed }} available") }
|
||||
Rise(5) {
|
||||
Text("? why emulators do not run on the desktop", fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(bottom = 6.dp).clip(Shape10).clickable(onClick = a.onEmulatorHelp).padding(horizontal = 6.dp, vertical = 4.dp))
|
||||
Text("? Why emulators?", fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(bottom = 6.dp).clip(Shape10).clickable(onClick = a.onEmulatorHelp).padding(horizontal = 6.dp, vertical = 4.dp))
|
||||
}
|
||||
Rise(6, Modifier.weight(1f).fillMaxWidth()) {
|
||||
ArtGrid(s.emulators.map { e -> Tile(e.name, if (e.installed) (if (e.id == "retroarch") "browses its own games" else "${e.games.size} game${if (e.games.size == 1) "" else "s"}") else "not installed", null, "emu:${e.id}", e.iconRes, dim = !e.installed) { if (e.installed) a.onEmulator(e) else a.onApps() } })
|
||||
@@ -653,7 +609,6 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
if (e == null) Note("Not installed.") else {
|
||||
Rise(0) { Eyebrow("Desktop · ${e.system}") }
|
||||
Rise(1) { Title(e.name) }
|
||||
Rise(2) { Lede("Opens fullscreen under gamescope. Games are the ${e.system} files in the ROMs folder; each launches straight in.") }
|
||||
Rise(3) {
|
||||
Actions {
|
||||
Image(painterResource(e.iconRes), null, modifier = Modifier.size(40.dp))
|
||||
@@ -710,7 +665,6 @@ private fun Content(s: FrontEndState, selected: String, a: FrontEndActions, modi
|
||||
|
||||
private class Tile(val title: String, val sub: String, val art: File?, val key: String, val iconRes: Int? = null, val dim: Boolean = false, val onClick: () -> Unit)
|
||||
|
||||
// ───────────────────────────── Type + small parts ─────────────────────────────
|
||||
|
||||
@Composable
|
||||
internal fun Eyebrow(t: String) {
|
||||
@@ -746,11 +700,9 @@ private fun Chip(t: String, ok: Boolean) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Hover/focus/press state shared by every control that lifts, glows or squashes. */
|
||||
@Composable
|
||||
private fun rememberHot(src: MutableInteractionSource): Boolean = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value
|
||||
|
||||
/** The launch button: gradient, a sheen that sweeps on focus, a squash on press. */
|
||||
@Composable
|
||||
private fun PrimaryButton(text: String, enabled: Boolean = true, onClick: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -760,7 +712,6 @@ private fun PrimaryButton(text: String, enabled: Boolean = true, onClick: () ->
|
||||
val pressed by src.collectIsPressedAsState()
|
||||
val scale by animateFloatAsState(if (pressed) 0.955f else if (hot) 1.02f else 1f, Motion.sp(0.5f, Spring.StiffnessMedium), label = "btnScale")
|
||||
val lift by animateFloatAsState(if (hot) 14f else 6f, Motion.tw(300), label = "btnLift")
|
||||
val nudge by animateFloatAsState(if (hot) 2f else 0f, Motion.sp(0.5f), label = "tri")
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
@@ -771,7 +722,6 @@ private fun PrimaryButton(text: String, enabled: Boolean = true, onClick: () ->
|
||||
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, enabled = enabled, onClick = onClick)
|
||||
.padding(horizontal = 18.dp, vertical = 11.dp),
|
||||
) {
|
||||
Text("▶", fontSize = 11.sp, color = if (enabled) colors.onPrimary else colors.onSurfaceVariant, modifier = Modifier.graphicsLayer { translationX = nudge.dp.toPx() })
|
||||
Text(text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 0.5.sp, color = if (enabled) colors.onPrimary else colors.onSurfaceVariant, maxLines = 1)
|
||||
}
|
||||
}
|
||||
@@ -824,14 +774,12 @@ private fun Poster(art: File?, name: String, modifier: Modifier) {
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── Grid + tiles ─────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ArtGrid(tiles: List<Tile>, wide: Boolean = false) {
|
||||
val square = tiles.isNotEmpty() && tiles.all { it.art == null && it.iconRes != null }
|
||||
LazyVerticalGrid(
|
||||
// Thumbnails to recognise a game by, not posters; icon tiles are squares.
|
||||
columns = GridCells.Adaptive(minSize = if (square) 64.dp else if (wide) 92.dp else 70.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(top = 6.dp, bottom = 14.dp, start = 4.dp, end = 4.dp),
|
||||
@@ -845,7 +793,6 @@ private fun ArtGrid(tiles: List<Tile>, wide: Boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
/** A tile lifts, rings and shines when the pad lands on it; its play badge pops in. */
|
||||
@Composable
|
||||
private fun GameTile(t: Tile, wide: Boolean, square: Boolean, src: MutableInteractionSource, hot: Boolean) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -866,7 +813,6 @@ private fun GameTile(t: Tile, wide: Boolean, square: Boolean, src: MutableIntera
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth().shine(hot)) {
|
||||
Art(t.art, t.iconRes, t.title, Modifier.fillMaxWidth(), wide)
|
||||
// Qualified: the enclosing Column would otherwise pick its scoped overload.
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = hot, modifier = Modifier.align(Alignment.Center),
|
||||
enter = scaleIn(Motion.sp(0.5f), initialScale = 0.5f) + fadeIn(Motion.tw(200)),
|
||||
@@ -875,7 +821,7 @@ private fun GameTile(t: Tile, wide: Boolean, square: Boolean, src: MutableIntera
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(if (square) 26.dp else 32.dp).graphicsLayer { shadowElevation = 10.dp.toPx(); shape = CircleShape; clip = false; spotShadowColor = pal.signal }.clip(CircleShape).background(pal.signal),
|
||||
) { Text("▶", fontSize = if (square) 9.sp else 11.sp, color = Color.White, modifier = Modifier.padding(start = 2.dp)) }
|
||||
) { Text("›", fontSize = if (square) 16.sp else 20.sp, color = Color.White) }
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.padding(horizontal = 6.dp, vertical = 5.dp)) {
|
||||
@@ -888,7 +834,6 @@ private fun GameTile(t: Tile, wide: Boolean, square: Boolean, src: MutableIntera
|
||||
@Composable
|
||||
private fun Art(art: File?, iconRes: Int?, label: String, modifier: Modifier, wide: Boolean = false) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
// Game art is a poster (2:3) or a screenshot (16:9); an emulator's icon is a square.
|
||||
val ratio = if (art == null && iconRes != null) 1f else if (wide) 16f / 9f else 2f / 3f
|
||||
Box(modifier = modifier.aspectRatio(ratio).background(if (art == null && iconRes == null) artBrush(hueOf(label)) else Brush.linearGradient(listOf(colors.surfaceVariant, colors.surface)))) {
|
||||
when {
|
||||
|
||||
@@ -41,7 +41,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.steamdeck.launcher.R
|
||||
|
||||
/** Everything the main screen shows; the activity owns the values and the work behind them. */
|
||||
class MainUiState(
|
||||
val installed: String?,
|
||||
val ready: Boolean,
|
||||
@@ -53,29 +52,13 @@ class MainUiState(
|
||||
val failed: Boolean,
|
||||
val frameGenLabel: String,
|
||||
val desktopInstalled: Boolean,
|
||||
/** The account the client would sign in as offline, or null if it has never signed in. */
|
||||
val offlineAccount: String?,
|
||||
val offline: Boolean,
|
||||
/** The folder every session shows at /root/ROMs, or null if none is chosen. */
|
||||
val romsDir: String?,
|
||||
val logsEnabled: Boolean,
|
||||
/** Installed emulators that can run under gamescope: label to path inside the runtime. */
|
||||
val emulators: List<Pair<String, String>> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* The whole app outside a session, in one column that is sized to the window it has.
|
||||
*
|
||||
* Panels and densities vary too much for fixed dp: a 7" 1080p handheld in landscape has barely
|
||||
* 400dp of height, a foldable's inner screen has 900. So the column is designed at one size and
|
||||
* scaled by the height available — everything on it, text included, shrinks together until it
|
||||
* fits — with scrolling left as the last resort, and a width cap so the buttons never span a
|
||||
* wide panel. The system bars are kept clear of.
|
||||
*
|
||||
* Play and Desktop span the column; everything else is a tile in two columns, a title with the
|
||||
* setting's current value under it, so the menu is read at a glance and fits without scrolling
|
||||
* on a handheld.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
@@ -104,7 +87,6 @@ fun MainScreen(
|
||||
.systemBarsPadding(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
// The column as designed needs about this much height; below it, scale everything.
|
||||
val designHeight = 480.dp
|
||||
val k = (maxHeight / designHeight).coerceIn(0.55f, 1f)
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -124,10 +106,6 @@ fun MainScreen(
|
||||
)
|
||||
Spacer(Modifier.height(6.dp * k))
|
||||
Text("SteamDeck", fontSize = 22.sp * k, color = colors.onBackground)
|
||||
Text(
|
||||
"Valve's native ARM64 Steam client, under gamescope",
|
||||
fontSize = 12.sp * k, color = colors.onSurfaceVariant, textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(14.dp * k))
|
||||
|
||||
val status = when {
|
||||
@@ -138,14 +116,14 @@ fun MainScreen(
|
||||
Text(status, fontSize = 15.sp * k, color = colors.onBackground)
|
||||
val detail = when {
|
||||
state.busy -> if (state.percent >= 0) "${state.stage} ${state.percent}%" else state.stage
|
||||
state.failed -> "Install failed — nothing was changed"
|
||||
state.failed -> "Install failed. Nothing changed."
|
||||
!state.ready -> state.available?.let { "$it · ${state.availableSize} download, one time" }
|
||||
?: "Could not reach the runtime catalog"
|
||||
state.available != null && state.available != state.installed ->
|
||||
"Runtime ${state.installed ?: "?"} installed · ${state.available} available"
|
||||
else -> "Runtime ${state.installed ?: "?"} installed"
|
||||
else -> null
|
||||
}
|
||||
Text(
|
||||
if (detail != null) Text(
|
||||
detail, fontSize = 12.sp * k, color = colors.onSurfaceVariant, textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 3.dp * k),
|
||||
)
|
||||
@@ -160,7 +138,6 @@ fun MainScreen(
|
||||
Spacer(Modifier.height(14.dp * k))
|
||||
|
||||
val buttonHeight = 40.dp * k
|
||||
// Each launch button has its own cog: what only matters for that mode lives there.
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp * k), modifier = Modifier.fillMaxWidth()) {
|
||||
Button(
|
||||
onClick = onPlay, enabled = state.ready && !state.busy,
|
||||
@@ -172,11 +149,9 @@ fun MainScreen(
|
||||
OutlinedButton(
|
||||
onClick = onDesktop, enabled = state.ready && !state.busy && state.desktopInstalled,
|
||||
modifier = Modifier.weight(1f).height(buttonHeight),
|
||||
) { Text(if (state.desktopInstalled) "Desktop" else "Desktop — install it under Desktop & apps", fontSize = 13.sp * k, maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
) { Text("Desktop", fontSize = 13.sp * k, maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
CogButton(k, onDesktopSettings)
|
||||
}
|
||||
// The ROMs folder, then every installed emulator as a session of its own under
|
||||
// gamescope (where the GPU is); the ? says why they are here and not on the desktop.
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp * k),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp * k),
|
||||
@@ -197,7 +172,7 @@ fun MainScreen(
|
||||
onClick = { onLaunchEmulator(path) }, enabled = state.ready && !state.busy,
|
||||
contentPadding = PaddingValues(horizontal = 10.dp * k, vertical = 0.dp),
|
||||
modifier = Modifier.height(30.dp * k),
|
||||
) { Text("▶ $label", fontSize = 11.sp * k, maxLines = 1) }
|
||||
) { Text(label, fontSize = 11.sp * k, maxLines = 1) }
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onEmulatorHelp,
|
||||
@@ -205,15 +180,13 @@ fun MainScreen(
|
||||
modifier = Modifier.width(30.dp * k).height(30.dp * k),
|
||||
) { Text("?", fontSize = 12.sp * k) }
|
||||
}
|
||||
// Offline is read while the client starts, so it is decided here rather than in the
|
||||
// session's drawer, and it needs credentials from an earlier sign-in to be possible.
|
||||
OutlinedButton(
|
||||
onClick = onOffline, enabled = state.offlineAccount != null,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp * k).height(buttonHeight),
|
||||
) {
|
||||
Text(
|
||||
when {
|
||||
state.offlineAccount == null -> "Start offline — sign in once first"
|
||||
state.offlineAccount == null -> "Start offline"
|
||||
state.offline -> "Start offline: on · ${state.offlineAccount}"
|
||||
else -> "Start offline: off"
|
||||
},
|
||||
@@ -260,7 +233,6 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/** The cog beside a launch button. */
|
||||
@Composable
|
||||
private fun CogButton(k: Float, onClick: () -> Unit) {
|
||||
OutlinedButton(
|
||||
@@ -270,7 +242,6 @@ private fun CogButton(k: Float, onClick: () -> Unit) {
|
||||
) { Icon(Icons.Filled.Settings, contentDescription = "Settings", modifier = Modifier.size(18.dp * k)) }
|
||||
}
|
||||
|
||||
/** One tile of the two-column menu: what it is, and under it what it is set to. */
|
||||
@Composable
|
||||
private fun MenuTile(
|
||||
title: String,
|
||||
@@ -297,40 +268,12 @@ private fun MenuTile(
|
||||
}
|
||||
}
|
||||
|
||||
/** The ? beside the emulator row: what the ▶ buttons are, how to use them, and why. */
|
||||
@Composable
|
||||
fun EmulatorHelpDialog(onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Emulators") },
|
||||
text = {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("What the ▶ buttons are", style = MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
"One per emulator installed under Desktop & apps. Each starts that emulator fullscreen, as a " +
|
||||
"session of its own under gamescope — the same way the Steam client runs — with the Steam " +
|
||||
"cog's display, driver and HDR settings.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("How to use them", style = MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
"Put your games in the folder the ROMs chip beside these buttons points at. In the emulator, open root › ROMs " +
|
||||
"(/root/ROMs); the whole phone is at root › Storage. Firmware and BIOS files go in the " +
|
||||
"same way (RPCS3: File › Install Firmware). Back opens the drawer; Stop session ends it.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Why not from the desktop", style = MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
"The desktop draws in software (labwc on pixman) and cannot hand a program the GPU: a Vulkan " +
|
||||
"emulator started there dies at its first frame (\"Surface lost\"). gamescope can, so an " +
|
||||
"emulator that renders with Vulkan or OpenGL belongs here. The desktop stays for files, " +
|
||||
"Firefox and anything that draws in software.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
text = { Text("Place games in the ROMs folder. In RPCS3, install firmware from File > Install Firmware.") },
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text("OK") } },
|
||||
)
|
||||
}
|
||||
@@ -353,11 +296,11 @@ fun CreditsDialog(onDismiss: () -> Unit) {
|
||||
title = { Text("Credits") },
|
||||
text = {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("The412Banner — the app, the Wayland compositor, the runtime and the Steam session.", style = MaterialTheme.typography.bodyMedium)
|
||||
Text("The412Banner - the app, the Wayland compositor, the runtime and the Steam session.", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text("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.", style = MaterialTheme.typography.bodyMedium)
|
||||
Text("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.", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text("Winlator (brunodev85) — the audio stack, the gamepad model and the shape of a session's host-side components; WinNative and Bannerlator are both Winlator lineage.", style = MaterialTheme.typography.bodyMedium)
|
||||
Text("Winlator (brunodev85) - the audio stack, the gamepad model and the shape of a session's host-side components; WinNative and Bannerlator are both Winlator lineage.", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(
|
||||
"GPL-3.0. Valve, Steam, Steam Deck and Proton are trademarks of Valve Corporation; this project is not affiliated with Valve. gamescope, Mesa, Turnip, Xwayland, PulseAudio, proot and Arch Linux ARM are their authors' own.",
|
||||
|
||||
@@ -15,32 +15,25 @@ import com.steamdeck.launcher.core.FexPreset
|
||||
import com.steamdeck.launcher.session.SessionPrefs
|
||||
import com.steamdeck.launcher.session.SessionService
|
||||
|
||||
/** One driver as the page shows it. [removable] is false for the runtime's own and the bundled builds. */
|
||||
class DriverRow(val id: String, val name: String, val detail: String, val removable: Boolean)
|
||||
|
||||
/** Everything one mode's settings page shows; the activity owns the values. */
|
||||
class ModeSettings(
|
||||
val mode: String,
|
||||
val resolutionCap: Int,
|
||||
val shapeMode: String,
|
||||
val hdr: Boolean,
|
||||
/** Why HDR cannot be offered on this display, or null when it can. */
|
||||
val hdrReason: String?,
|
||||
val linuxRows: List<DriverRow>,
|
||||
val linuxSelected: String,
|
||||
val androidRows: List<DriverRow>,
|
||||
val androidSelected: String,
|
||||
val touchMode: String,
|
||||
/** Steam only. */
|
||||
val oscMode: String?,
|
||||
val directAudio: Boolean?,
|
||||
val mic: Boolean?,
|
||||
/** Desktop only. */
|
||||
val renderer: String?,
|
||||
/** Steam only: the second library's root ("" = internal only) and what this device offers. */
|
||||
val gameStorage: String? = null,
|
||||
val storageOptions: List<Pair<String, String>> = emptyList(),
|
||||
/** Steam only: the FEXCore preset for the games the client launches. */
|
||||
val fexPreset: String? = null,
|
||||
)
|
||||
|
||||
@@ -65,12 +58,6 @@ class ModeSettingsActions(
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
||||
/**
|
||||
* The cog beside Play / Desktop: a page in the front end's pane, one row per setting with its
|
||||
* value in a chip, and a small menu under the chip to change it. What only matters for that one
|
||||
* mode lives here - the display the session is sized to, HDR, the driver inside the runtime,
|
||||
* the display driver, and the input and audio choices - so the rail keeps what applies to both.
|
||||
*/
|
||||
@Composable
|
||||
fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
val steam = s.mode == SessionService.MODE_STEAM
|
||||
@@ -88,7 +75,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
ChoiceRow(
|
||||
host, "res", "Resolution", "Takes effect at the next session: gamescope sizes its display once, when it starts.",
|
||||
listOf(720 to "Up to 720p", 900 to "Up to 900p", 1080 to "Up to 1080p", 0 to "The panel's own")
|
||||
.map { (cap, label) -> cap to (if (cap == default) "$label — the default" else label) },
|
||||
.map { (cap, label) -> cap to (if (cap == default) "$label - the default" else label) },
|
||||
s.resolutionCap, note = "720p keeps the client's menus responsive; above 1080p costs frames for nothing a handheld can show.",
|
||||
onPick = a.onResolution,
|
||||
)
|
||||
@@ -151,7 +138,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
if (steam && s.gameStorage != null) SettingsGroup("Game storage") {
|
||||
val custom = s.gameStorage.isNotEmpty() && s.gameStorage != "off" && s.storageOptions.none { it.second == s.gameStorage }
|
||||
val options = buildList {
|
||||
add("" to ("Automatic — the SD card when one is in" + (if (s.storageOptions.isEmpty()) " (none right now)" else "")))
|
||||
add("" to ("Automatic - the SD card when one is in" + (if (s.storageOptions.isEmpty()) " (none right now)" else "")))
|
||||
add("off" to "Internal only")
|
||||
for ((label, path) in s.storageOptions) add(path to label)
|
||||
if (custom) add(s.gameStorage to "Folder: ${s.gameStorage}")
|
||||
@@ -163,7 +150,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
highlighted = open,
|
||||
) {
|
||||
androidx.compose.foundation.layout.Box {
|
||||
ValueChip(options.firstOrNull { it.first == s.gameStorage }?.second?.substringBefore(" —") ?: "—", open) { host.open = if (open) null else "storage" }
|
||||
ValueChip(options.firstOrNull { it.first == s.gameStorage }?.second?.substringBefore(" -") ?: "-", open) { host.open = if (open) null else "storage" }
|
||||
AnchoredMenu(
|
||||
open, onDismiss = { if (host.open == "storage") host.open = null }, title = "Second library",
|
||||
note = "An SD card and shared storage go through Android's file layer: a game that streams big assets from there can stall. Keep such games internal.",
|
||||
@@ -180,7 +167,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
if (!steam && s.renderer != null) SettingsGroup("Renderer") {
|
||||
ChoiceRow(
|
||||
host, "renderer", "Desktop renderer", "How labwc composites the desktop. pixman is software and works everywhere.",
|
||||
listOf("pixman" to "pixman — software", "gles2" to "gles2", "vulkan" to "vulkan"), s.renderer,
|
||||
listOf("pixman" to "pixman - software", "gles2" to "gles2", "vulkan" to "vulkan"), s.renderer,
|
||||
note = "gles2 and vulkan need a real DRM render node, which the Adreno stand-in is not on most devices.", onPick = a.onRenderer,
|
||||
)
|
||||
}
|
||||
@@ -189,7 +176,6 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
}
|
||||
}
|
||||
|
||||
/** A driver list as a menu: each build a line, imported ones with a remove control, an import at the end. */
|
||||
@Composable
|
||||
private fun DriverRowMenu(
|
||||
host: MenuHost, key: String, label: String, hint: String, rows: List<DriverRow>, selected: String, importLabel: String,
|
||||
@@ -199,7 +185,7 @@ private fun DriverRowMenu(
|
||||
val colors = MaterialTheme.colorScheme
|
||||
SettingsRow(label, hint, highlighted = open) {
|
||||
androidx.compose.foundation.layout.Box {
|
||||
ValueChip(rows.firstOrNull { it.id == selected }?.name ?: rows.firstOrNull()?.name ?: "—", open) { host.open = if (open) null else key }
|
||||
ValueChip(rows.firstOrNull { it.id == selected }?.name ?: rows.firstOrNull()?.name ?: "-", open) { host.open = if (open) null else key }
|
||||
AnchoredMenu(open, onDismiss = { if (host.open == key) host.open = null }, title = label) {
|
||||
for (row in rows) MenuItem(
|
||||
row.name, checked = row.id == selected, detail = row.detail.ifEmpty { null },
|
||||
|
||||
@@ -17,15 +17,8 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/** One core as the page labels it: its number and, where known, its ceiling. */
|
||||
class CoreRow(val core: Int, val label: String)
|
||||
|
||||
/**
|
||||
* The two core masks a Steam session carries, kept separate because they are wanted at the same
|
||||
* time and suit different things: the client's menus and a game each get their own. Both apply
|
||||
* at the next session start; the game's is applied by the Proton wrapper at each launch. Below
|
||||
* them, the session fixes for a device the runtime does not sit well on.
|
||||
*/
|
||||
@Composable
|
||||
fun PerformancePage(
|
||||
cores: List<CoreRow>,
|
||||
@@ -63,7 +56,7 @@ fun PerformancePage(
|
||||
SettingsGroup("Steam client cores") {
|
||||
ToggleRow(
|
||||
host, "override", "Override Steam's own core choice",
|
||||
"Steam pins its interface to a subset of cores it picks — on one device 5 of 8, leaving out the fastest — which suits a running game and makes the menus sluggish when the client is all there is. On: the client, its UI helper and gamescope are pinned to the cores below instead, re-applied every few seconds.",
|
||||
"Steam pins its interface to a subset of cores it picks - on one device 5 of 8, leaving out the fastest - which suits a running game and makes the menus sluggish when the client is all there is. On: the client, its UI helper and gamescope are pinned to the cores below instead, re-applied every few seconds.",
|
||||
clientOverride, onChange = onClientOverride,
|
||||
)
|
||||
MultiRow(
|
||||
@@ -74,7 +67,7 @@ fun PerformancePage(
|
||||
SettingsGroup("Game cores") {
|
||||
MultiRow(
|
||||
host, "gameCores", "Game cores",
|
||||
"Applied by exec'ing the game through taskset, so every thread inherits the mask. Every core ticked sends nothing — that is what the scheduler does unaided. Untick the small cores to keep a heavy game off them.",
|
||||
"Applied by exec'ing the game through taskset, so every thread inherits the mask. Every core ticked sends nothing - that is what the scheduler does unaided. Untick the small cores to keep a heavy game off them.",
|
||||
coreItems, gameCores, note = "Tick or untick as many as you like; the menu stays open.", onToggle = onGameCore,
|
||||
)
|
||||
}
|
||||
@@ -103,7 +96,7 @@ fun PerformancePage(
|
||||
SettingsGroup("Session fixes") {
|
||||
ToggleRow(
|
||||
host, "sysmem", "Turnip: sysmem rendering",
|
||||
"Renders without the GPU's tile memory (TU_DEBUG=sysmem). A fix for an Adreno 8xx that shows corruption, and required on a 710/720/722 (set on its own there). Not a speed setting: elsewhere it COSTS frames — leave it off unless the picture is wrong.",
|
||||
"Renders without the GPU's tile memory (TU_DEBUG=sysmem). A fix for an Adreno 8xx that shows corruption, and required on a 710/720/722 (set on its own there). Not a speed setting: elsewhere it COSTS frames - leave it off unless the picture is wrong.",
|
||||
tuSysmem, onChange = onTuSysmem,
|
||||
)
|
||||
ToggleRow(
|
||||
|
||||
@@ -15,14 +15,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/** One third-party Proton as the dialog shows it. */
|
||||
class ProtonRow(val id: String, val name: String, val installed: String?, val queued: Boolean)
|
||||
|
||||
/**
|
||||
* GE-Proton and proton-cachyos: installed, queued for the next session, or neither. Installing
|
||||
* happens inside the runtime when a session starts (a download of several hundred MB), so the
|
||||
* dialog only ever queues or cancels a request, and removes an installed build.
|
||||
*/
|
||||
@Composable
|
||||
fun ProtonDialog(
|
||||
rows: List<ProtonRow>,
|
||||
@@ -37,7 +31,7 @@ fun ProtonDialog(
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"Native ARM64 Proton builds beside the ARM64 Proton Valve ships. Installed when a session starts — a large download — and then chosen per game in Steam under Properties → Compatibility.",
|
||||
"Native ARM64 Proton builds beside the ARM64 Proton Valve ships. Installed when a session starts - a large download - and then chosen per game in Steam under Properties → Compatibility.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/** The ROMs folder: where it is, and where the emulators will find it. */
|
||||
@Composable
|
||||
fun RomsDialog(path: String?, onChoose: () -> Unit, onClear: () -> Unit, onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
@@ -21,7 +20,7 @@ fun RomsDialog(path: String?, onChoose: () -> Unit, onClear: () -> Unit, onDismi
|
||||
Column {
|
||||
Text(
|
||||
"A folder on this device that every session shows as ROMs in the home folder " +
|
||||
"(/root/ROMs) — open it from any emulator's file dialog. All of internal " +
|
||||
"(/root/ROMs) - open it from any emulator's file dialog. All of internal " +
|
||||
"storage is there too, as Storage.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
@@ -66,7 +66,6 @@ import com.steamdeck.launcher.gpu.FrameGen
|
||||
import com.steamdeck.launcher.session.SessionPrefs
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
/** A line of numbers in the top-right corner. It takes no touches: everything goes to the game. */
|
||||
@Composable
|
||||
fun HudText(text: String) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopEnd) {
|
||||
@@ -83,11 +82,6 @@ fun HudText(text: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What the user sees until the client draws its first frame: the runtime's milestone, the
|
||||
* client's download progress lifted out of its log, a clock, and a hint. Opaque, so a stale
|
||||
* frame from an earlier session never shows through, and it swallows touches.
|
||||
*/
|
||||
@Composable
|
||||
fun LoadingOverlay(step: String, percent: Int, elapsed: String, hint: String, ended: Boolean) {
|
||||
Column(
|
||||
@@ -116,7 +110,6 @@ fun LoadingOverlay(step: String, percent: Int, elapsed: String, hint: String, en
|
||||
}
|
||||
}
|
||||
|
||||
/** Everything the drawer shows and does. */
|
||||
class DrawerActions(
|
||||
val steam: Boolean,
|
||||
val hudOn: Boolean,
|
||||
@@ -125,16 +118,13 @@ class DrawerActions(
|
||||
val lsfgReady: Boolean,
|
||||
val oscMode: String,
|
||||
val touchMode: String,
|
||||
/** What "auto" resolves to right now: "touchpad" or "direct". */
|
||||
val touchAuto: String,
|
||||
val shapeMode: String,
|
||||
val fexPreset: String,
|
||||
val onHud: (Boolean) -> Unit,
|
||||
val onFrameGenPick: (engine: String, multiplier: Int) -> Unit,
|
||||
val onKeyboard: () -> Unit,
|
||||
/** Sends the Guide button (the client's menu); null on the desktop, where there is none. */
|
||||
val onSteamMenu: (() -> Unit)?,
|
||||
/** Sends Guide + A to open Steam's Quick Access Menu; null on the desktop. */
|
||||
val onQam: (() -> Unit)?,
|
||||
val onProtons: () -> Unit,
|
||||
val onOsc: (String) -> Unit,
|
||||
@@ -146,13 +136,6 @@ class DrawerActions(
|
||||
val onClose: () -> Unit,
|
||||
)
|
||||
|
||||
/**
|
||||
* The drawer Back opens over a running session, in the front end's own dress: a panel that
|
||||
* slides in from the right with the switches a player reaches for mid-game as rows whose values
|
||||
* open in place, what only applies at the next session under its own heading, and the two ways
|
||||
* out at the end. Steam and the desktop share the rows that mean the same on both; the client's
|
||||
* own (its menu, the virtual pad, the FEX preset, the compatibility tools) show only there.
|
||||
*/
|
||||
@Composable
|
||||
fun SessionDrawer(open: Boolean, a: DrawerActions) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -174,7 +157,6 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
|
||||
.fillMaxHeight()
|
||||
.width(340.dp)
|
||||
.background(pal.background.copy(alpha = 0.97f))
|
||||
// The panel swallows its own touches so they do not close it.
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) {}
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 14.dp, vertical = 16.dp),
|
||||
@@ -182,10 +164,7 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 4.dp, bottom = 6.dp)) {
|
||||
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(Brush.linearGradient(listOf(colors.primary, pal.primary2))))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Column {
|
||||
Text("SteamDeck", fontSize = 17.sp, fontWeight = FontWeight.Bold, color = colors.onBackground)
|
||||
Text(if (a.steam) "Steam session" else "Desktop session", fontSize = 11.sp, color = colors.onSurfaceVariant)
|
||||
}
|
||||
Text(if (a.steam) "Steam" else "Desktop", fontSize = 17.sp, fontWeight = FontWeight.Bold, color = colors.onBackground)
|
||||
}
|
||||
if (a.onSteamMenu != null && a.onQam != null) {
|
||||
val qamInteraction = remember { MutableInteractionSource() }
|
||||
@@ -226,14 +205,14 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
|
||||
}
|
||||
|
||||
SettingsGroup("Now") {
|
||||
ToggleRow(host, "hud", "Performance HUD", "The frame counter in the corner.", a.hudOn, onChange = a.onHud)
|
||||
ToggleRow(host, "hud", "Performance HUD", null, a.hudOn, onChange = a.onHud)
|
||||
val fgOpen = host.open == "fg"
|
||||
val fgLabel = when (a.frameGenEngine) {
|
||||
FrameGen.ENGINE_WINFG -> "Win-FG ${a.frameGenMultiplier}×"
|
||||
FrameGen.ENGINE_LSFG -> "LSFG ${a.frameGenMultiplier}×"
|
||||
else -> "Off"
|
||||
}
|
||||
SettingsRow("Frame generation", "Extra frames between the real ones; takes effect at once, mid-game included.", highlighted = fgOpen) {
|
||||
SettingsRow("Frame generation", null, highlighted = fgOpen) {
|
||||
Box {
|
||||
ValueChip(fgLabel, fgOpen) { host.open = if (fgOpen) null else "fg" }
|
||||
AnchoredMenu(fgOpen, onDismiss = { if (host.open == "fg") host.open = null }, title = "Frame generation") {
|
||||
@@ -245,28 +224,28 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
|
||||
}
|
||||
}
|
||||
ChoiceRow(
|
||||
host, "touch", "Touch", "How a finger drives the pointer.",
|
||||
host, "touch", "Touch", null,
|
||||
listOf(SessionPrefs.TOUCH_AUTO to "Auto (${a.touchAuto})", SessionPrefs.TOUCH_PAD to "Touchpad", SessionPrefs.TOUCH_DIRECT to "Direct"), a.touchMode,
|
||||
note = "Touchpad: drag moves, tap clicks. Direct: the pointer jumps under the finger.", onPick = a.onTouch,
|
||||
onPick = a.onTouch,
|
||||
)
|
||||
if (a.steam) ChoiceRow(
|
||||
host, "osc", "On-screen controls", "The virtual pad drawn over a game.",
|
||||
host, "osc", "On-screen controls", null,
|
||||
listOf(SessionPrefs.OSC_AUTO to "Auto", SessionPrefs.OSC_ALWAYS to "Always", SessionPrefs.OSC_STEAM_QAM to "Steam + QAM", SessionPrefs.OSC_NEVER to "Never"), a.oscMode,
|
||||
note = "Auto shows the full pad when no controller is attached. Steam + QAM shows only those two buttons.", onPick = a.onOsc,
|
||||
onPick = a.onOsc,
|
||||
)
|
||||
ActionRow("Keyboard", "The on-screen keyboard, for a field the client or a program is waiting on.", "Show") { host.open = null; a.onKeyboard() }
|
||||
ActionRow("Keyboard", null, "Show") { host.open = null; a.onKeyboard() }
|
||||
}
|
||||
|
||||
SettingsGroup("Next session") {
|
||||
ChoiceRow(
|
||||
host, "shape", "Display shape", "gamescope sizes its display once, when a session starts.",
|
||||
host, "shape", "Display shape", null,
|
||||
listOf(SessionPrefs.SHAPE_AUTO to "The panel's shape", SessionPrefs.SHAPE_WIDE to "16:9 with bars"), a.shapeMode, onPick = a.onShape,
|
||||
)
|
||||
if (a.steam) ChoiceRow(
|
||||
host, "fex", "FEX preset", "How FEX translates the x86 games the client launches. Applies to the next game launch.",
|
||||
host, "fex", "FEX preset", null,
|
||||
FexPreset.all.map { it.id to it.label }, a.fexPreset, note = FexPreset.byId(a.fexPreset).detail, onPick = a.onFexPreset,
|
||||
)
|
||||
if (a.steam) ActionRow("Compatibility tools", "The Proton builds the client can run games with.", "Manage") { host.open = null; a.onProtons() }
|
||||
if (a.steam) ActionRow("Compatibility tools", null, "Manage") { host.open = null; a.onProtons() }
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(18.dp))
|
||||
@@ -280,7 +259,6 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The one button that ends things: outlined in the error colour, filled on focus. */
|
||||
@Composable
|
||||
private fun DangerButton(text: String, onClick: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
|
||||
@@ -64,16 +64,10 @@ import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
|
||||
/*
|
||||
* Settings without a pop-up: a page of rows, each with its value in a chip on the right, and a
|
||||
* small faded menu that opens right under the chip when it is tapped. One menu at a time per
|
||||
* page; the rest of the page dims a little while it is open.
|
||||
*/
|
||||
|
||||
private val RowShape = RoundedCornerShape(12.dp)
|
||||
private val GroupShape = RoundedCornerShape(14.dp)
|
||||
|
||||
/** Which row's menu is open on a page, so the page can dim and only one menu shows. */
|
||||
class MenuHost {
|
||||
var open by mutableStateOf<String?>(null)
|
||||
}
|
||||
@@ -81,7 +75,6 @@ class MenuHost {
|
||||
@Composable
|
||||
fun rememberMenuHost(): MenuHost = remember { MenuHost() }
|
||||
|
||||
/** Under the anchor, right edges aligned; above it when the bottom of the window is too close. */
|
||||
private class BelowEndProvider(private val gap: Int) : PopupPositionProvider {
|
||||
override fun calculatePosition(anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize): IntOffset {
|
||||
var x = anchorBounds.right - popupContentSize.width
|
||||
@@ -93,11 +86,6 @@ private class BelowEndProvider(private val gap: Int) : PopupPositionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The menu itself, anchored to whatever composable it is placed inside (put it in the same Box as
|
||||
* the chip). It fades and scales in from its top-right corner and out again on dismiss; the popup
|
||||
* is only removed once the exit has finished.
|
||||
*/
|
||||
@Composable
|
||||
fun AnchoredMenu(open: Boolean, onDismiss: () -> Unit, title: String? = null, note: String? = null, content: @Composable ColumnScope.() -> Unit) {
|
||||
val state = remember { MutableTransitionState(false) }
|
||||
@@ -138,7 +126,6 @@ fun AnchoredMenu(open: Boolean, onDismiss: () -> Unit, title: String? = null, no
|
||||
}
|
||||
}
|
||||
|
||||
/** One line of a menu: a check mark when it is the current value, a label, an optional second line. */
|
||||
@Composable
|
||||
fun MenuItem(
|
||||
label: String, checked: Boolean, enabled: Boolean = true, detail: String? = null,
|
||||
@@ -168,7 +155,6 @@ fun MenuItem(
|
||||
}
|
||||
}
|
||||
|
||||
/** The value on the right of a row: the current choice and a caret that turns while the menu is open. */
|
||||
@Composable
|
||||
fun ValueChip(text: String, open: Boolean, enabled: Boolean = true, onClick: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -198,7 +184,6 @@ fun ValueChip(text: String, open: Boolean, enabled: Boolean = true, onClick: ()
|
||||
}
|
||||
}
|
||||
|
||||
/** A group of rows on one surface, with a section label above it. */
|
||||
@Composable
|
||||
fun SettingsGroup(title: String, content: @Composable ColumnScope.() -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -210,7 +195,6 @@ fun SettingsGroup(title: String, content: @Composable ColumnScope.() -> Unit) {
|
||||
Column(modifier = Modifier.fillMaxWidth().clip(GroupShape).background(colors.surface).border(1.dp, pal.line, GroupShape)) { content() }
|
||||
}
|
||||
|
||||
/** A row: label and hint on the left, whatever control on the right. */
|
||||
@Composable
|
||||
fun SettingsRow(label: String, hint: String?, highlighted: Boolean = false, control: @Composable () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -229,7 +213,6 @@ fun SettingsRow(label: String, hint: String?, highlighted: Boolean = false, cont
|
||||
Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line))
|
||||
}
|
||||
|
||||
/** A row whose value is one of a list: the chip opens the menu, a pick closes it. */
|
||||
@Composable
|
||||
fun <T> ChoiceRow(
|
||||
host: MenuHost, key: String, label: String, hint: String?,
|
||||
@@ -239,7 +222,7 @@ fun <T> ChoiceRow(
|
||||
val open = host.open == key
|
||||
SettingsRow(label, hint, highlighted = open) {
|
||||
Box {
|
||||
ValueChip(options.firstOrNull { it.first == selected }?.second ?: "—", open, enabled) { host.open = if (open) null else key }
|
||||
ValueChip(options.firstOrNull { it.first == selected }?.second ?: "-", open, enabled) { host.open = if (open) null else key }
|
||||
AnchoredMenu(open, onDismiss = { if (host.open == key) host.open = null }, title = label, note = note) {
|
||||
for ((value, text) in options) MenuItem(text, checked = value == selected) { onPick(value); host.open = null }
|
||||
}
|
||||
@@ -247,12 +230,10 @@ fun <T> ChoiceRow(
|
||||
}
|
||||
}
|
||||
|
||||
/** On or off, as a two-line menu, so every row on the page behaves the same way under a pad. */
|
||||
@Composable
|
||||
fun ToggleRow(host: MenuHost, key: String, label: String, hint: String?, checked: Boolean, enabled: Boolean = true, onChange: (Boolean) -> Unit) =
|
||||
ChoiceRow(host, key, label, hint, listOf(true to "On", false to "Off"), checked, enabled, onPick = onChange)
|
||||
|
||||
/** Several of a list at once (the core masks): the menu stays open while boxes are ticked. */
|
||||
@Composable
|
||||
fun MultiRow(
|
||||
host: MenuHost, key: String, label: String, hint: String?,
|
||||
@@ -278,13 +259,11 @@ fun MultiRow(
|
||||
}
|
||||
}
|
||||
|
||||
/** A row that does something (import, choose a folder) instead of holding a value. */
|
||||
@Composable
|
||||
fun ActionRow(label: String, hint: String?, button: String, onClick: () -> Unit) {
|
||||
SettingsRow(label, hint) { SecondaryButton(button, onClick = onClick) }
|
||||
}
|
||||
|
||||
/** A page: eyebrow with a way back, title, lede, and the rows below it, dimmed while a menu is open. */
|
||||
@Composable
|
||||
fun SettingsPage(host: MenuHost, eyebrow: String, title: String, lede: String, onBack: () -> Unit, content: @Composable ColumnScope.() -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
|
||||
@@ -9,12 +9,6 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.steamdeck.launcher.session.SessionPrefs
|
||||
|
||||
/**
|
||||
* The app's colours, in the icon's three: black, white and its blue. Three ways to deal them
|
||||
* out, chosen under Setup › Theme. [primary] is what the selection pill and the Play button are
|
||||
* made of; [signal] is the small accent for rules, rings, dots and the eyebrow - the same colour
|
||||
* as [primary] in the blue themes, the blue itself in Paper, where the primary is white.
|
||||
*/
|
||||
class Palette(
|
||||
val id: String, val label: String, val detail: String,
|
||||
val background: Color, val surface: Color, val surfaceVariant: Color, val line: Color, val line2: Color,
|
||||
@@ -57,7 +51,6 @@ object Themes {
|
||||
|
||||
val LocalPalette = staticCompositionLocalOf { Themes.byId(Themes.PAPER) }
|
||||
|
||||
/** The app's theme; with no [theme] given, the one saved in the preferences. The app is never light. */
|
||||
@Composable
|
||||
fun SteamDeckTheme(theme: String? = null, content: @Composable () -> Unit) {
|
||||
val id = theme ?: SessionPrefs.theme(LocalContext.current)
|
||||
|
||||
@@ -76,8 +76,8 @@ object CompositorHost {
|
||||
/**
|
||||
* The window the guest presents through has changed (the Steam menu opening or closing over a
|
||||
* fullscreen game swaps gamescope between forwarding the game's own buffers and its composited
|
||||
* output). The engine's ring and history straddle that switch and come back wrong-sized — a
|
||||
* game shown as a strip in a corner — so it is disarmed and re-armed against the new window.
|
||||
* output). The engine's ring and history straddle that switch and come back wrong-sized - a
|
||||
* game shown as a strip in a corner - so it is disarmed and re-armed against the new window.
|
||||
*/
|
||||
fun rearmFrameGen(rearm: () -> Unit) {
|
||||
if (!started) return
|
||||
@@ -87,7 +87,7 @@ object CompositorHost {
|
||||
}
|
||||
|
||||
/**
|
||||
* The activity is going away. The compositor keeps running with nothing to present into —
|
||||
* The activity is going away. The compositor keeps running with nothing to present into -
|
||||
* the guest carries on, and its next frames land on the Surface the next activity brings.
|
||||
*/
|
||||
/** The Surface the compositor presents on; only its own owner may take it away. */
|
||||
|
||||
@@ -22,7 +22,7 @@ public final class WaylandCompositor {
|
||||
|
||||
/** Register a callback fired once, when the compositor presents the first client
|
||||
* frame to the output Surface. Used to dismiss the launch overlay in wayland mode
|
||||
* (there is no XServer window-content hook). Runs on the compositor thread — the
|
||||
* (there is no XServer window-content hook). Runs on the compositor thread - the
|
||||
* listener must marshal to the UI thread itself. */
|
||||
public static void setFirstFrameListener(Runnable r) { firstFrameListener = r; }
|
||||
|
||||
@@ -39,12 +39,12 @@ public final class WaylandCompositor {
|
||||
/** A window started presenting GPU frames ({@code window} describes it; {@code gpuName} is the
|
||||
* compositor's GPU), or {@code window == null} when that window closed. Compositor thread. */
|
||||
void onGameSurface(String window, String gpuName);
|
||||
/** One GPU frame from that window. Compositor thread — keep it cheap. */
|
||||
/** One GPU frame from that window. Compositor thread - keep it cheap. */
|
||||
void onGameFrame();
|
||||
/** The program behind that window, right after {@link #onGameSurface}: its Linux pid (the
|
||||
* Wayland client's credentials) and executable name ({@code ""} when unknown). The app arms its
|
||||
* launch-time CPU affinity on it — on X11 that comes from window events, which a Wayland session
|
||||
* has none of. Compositor thread — marshal to the UI thread. */
|
||||
* launch-time CPU affinity on it - on X11 that comes from window events, which a Wayland session
|
||||
* has none of. Compositor thread - marshal to the UI thread. */
|
||||
default void onGameProgram(int pid, String program) {}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ public final class WaylandCompositor {
|
||||
private static native void nativeTextInputPreedit(byte[] utf8, int cursorBegin, int cursorEnd);
|
||||
private static native void nativeTextInputDelete(int before, int after);
|
||||
|
||||
/** Start the compositor headless (no output window) — bring-up tests only. */
|
||||
/** Start the compositor headless (no output window) - bring-up tests only. */
|
||||
public static native void nativeStart(String xdgRuntimeDir);
|
||||
|
||||
/** Start the compositor rendering to {@code surface}. XDG_RUNTIME_DIR = an
|
||||
@@ -201,7 +201,7 @@ public final class WaylandCompositor {
|
||||
|
||||
/** Zero-copy frames the compositor presented in its LAST completed 10 s stats window (the
|
||||
* "| N zero-copy frames" figure of its session-log stats line); 0 while zero-copy is off or
|
||||
* before the first window closes. Read-only, any thread — the in-game drawer polls it. */
|
||||
* before the first window closes. Read-only, any thread - the in-game drawer polls it. */
|
||||
public static native int nativeZeroCopyFrames();
|
||||
|
||||
/** Milliseconds since the compositor last put a game frame on the display layer without a copy;
|
||||
@@ -244,7 +244,7 @@ public final class WaylandCompositor {
|
||||
}
|
||||
|
||||
/** Write one line into the compositor's session log (Download/Wayland-logs) under the "display"
|
||||
* area, from Java. Used for facts the platform knows and the native side does not — the panel's
|
||||
* area, from Java. Used for facts the platform knows and the native side does not - the panel's
|
||||
* HDR capability, which lives behind android.view.Display. Safe before the compositor thread is
|
||||
* up (the line then only reaches logcat) and safe after it has gone. */
|
||||
public static native void nativeLogDisplay(String message);
|
||||
@@ -382,7 +382,7 @@ public final class WaylandCompositor {
|
||||
|
||||
/** Colour grade in the drawer's slider units (brightness/contrast -100..100, gamma 0.5..3.0,
|
||||
* saturation 0..200 percent; 0/0/1/100 = neutral, pass off) plus the FXAA / Toon / CRT / NTSC
|
||||
* toggles — the same signature as {@code VulkanRenderer.setScreenEffects}. */
|
||||
* toggles - the same signature as {@code VulkanRenderer.setScreenEffects}. */
|
||||
public static native void nativeSetScreenEffects(float brightness, float contrast, float gamma, float saturation,
|
||||
boolean fxaa, boolean toon, boolean crt, boolean ntsc);
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
<string name="runtime_update">Runtime %1$s installed · %2$s available</string>
|
||||
<string name="runtime_download_size">%1$s · %2$s download, one time</string>
|
||||
<string name="catalog_unreachable">Could not reach the runtime catalog</string>
|
||||
<string name="install_failed">Install failed — nothing was changed</string>
|
||||
<string name="install_failed">Install failed - nothing was changed</string>
|
||||
<string name="session_starting">Starting the session…</string>
|
||||
<string name="loading_title">Steam is loading</string>
|
||||
<string name="loading_elapsed">%1$s elapsed · still working</string>
|
||||
<string name="loading_download">Downloading the Steam client update · %1$d%%</string>
|
||||
<string name="loading_client_download">Downloading the Steam client · %1$s</string>
|
||||
<string-array name="loading_hints">
|
||||
<item>The first run downloads the Steam client from Valve and then its update — a few minutes on a good connection.</item>
|
||||
<item>The first run downloads the Steam client from Valve and then its update - a few minutes on a good connection.</item>
|
||||
<item>The screen stays black until the client draws its first frame; nothing is stuck while this clock is moving.</item>
|
||||
<item>You can leave. Steam keeps running in the background and the notification brings you back.</item>
|
||||
<item>A controller shows up in Steam as an Xbox 360 pad. Without one, the on-screen pad appears.</item>
|
||||
@@ -31,11 +31,11 @@
|
||||
<string name="session_ended">The session ended (%1$d)\n%2$s</string>
|
||||
<string name="session_channel">Session</string>
|
||||
<string name="session_channel_description">Keeps Steam running while the app is in the background</string>
|
||||
<string name="session_notification">Steam is running — leave this notification alone</string>
|
||||
<string name="session_notification">Steam is running - leave this notification alone</string>
|
||||
<string name="stop_session">Stop session</string>
|
||||
<string name="frame_gen">Frame generation: %1$s</string>
|
||||
<string name="frame_gen_title">Frame generation</string>
|
||||
<string name="frame_gen_help">Extra frames are generated between the real ones on the way to the screen, so a game running at 30 fps looks like 60. Win-FG is built in. LSFG uses the shaders from your own copy of Lossless Scaling — install it from the Steam client in this app first. Takes effect immediately, even mid-game.</string>
|
||||
<string name="frame_gen_help">Extra frames are generated between the real ones on the way to the screen, so a game running at 30 fps looks like 60. Win-FG is built in. LSFG uses the shaders from your own copy of Lossless Scaling - install it from the Steam client in this app first. Takes effect immediately, even mid-game.</string>
|
||||
<string name="drawer_hud">Performance HUD</string>
|
||||
<string name="drawer_osc">On-screen controls: %1$s</string>
|
||||
<string name="drawer_shape">Display shape: %1$s · next session</string>
|
||||
@@ -47,5 +47,5 @@
|
||||
<string name="osc_never">never</string>
|
||||
<string name="credits_line">by The412Banner and maxjivi05 · credits</string>
|
||||
<string name="credits_title">Credits</string>
|
||||
<string name="credits_body">SteamDeck\n\nThe412Banner — the app, the Wayland compositor, the runtime and the Steam session.\n\nmaxjivi05 (Max) — the gamescope runtime this is built on: the proot session, the session shim, the fake-evdev interposer and the controller work, from WinNative.\n\nGPL-3.0. Valve, Steam, Steam Deck and Proton are trademarks of Valve Corporation; this project is not affiliated with Valve. gamescope, Mesa, Turnip, Xwayland, PulseAudio, proot and Arch Linux ARM are their authors\' own.</string>
|
||||
<string name="credits_body">SteamDeck\n\nThe412Banner - the app, the Wayland compositor, the runtime and the Steam session.\n\nmaxjivi05 (Max) - the gamescope runtime this is built on: the proot session, the session shim, the fake-evdev interposer and the controller work, from WinNative.\n\nGPL-3.0. Valve, Steam, Steam Deck and Proton are trademarks of Valve Corporation; this project is not affiliated with Valve. gamescope, Mesa, Turnip, Xwayland, PulseAudio, proot and Arch Linux ARM are their authors\' own.</string>
|
||||
</resources>
|
||||
@@ -1,99 +0,0 @@
|
||||
# Lineage audit — what in this repo is Winlator's, and what it would take to leave the lineage
|
||||
|
||||
Audited 2026-09-23 against Winlator upstream (brunodev85's commits in the Bannerlator history) with
|
||||
a text-similarity measure, not by trusting the comments. Everything below is verifiable with
|
||||
`git grep -i winlator` (54 lines in 26 files) and a `difflib` ratio against Bruno's originals.
|
||||
|
||||
## 0. The right cut: inherited from Winlator, or written fresh inside its forks
|
||||
|
||||
WinNative and Bannerlator are Winlator forks, so "from Bannerlator" does not by itself mean
|
||||
"Winlator's". What matters is whether a piece came down from Bruno's tree or was written new in
|
||||
the forks. Measured against Bruno's originals:
|
||||
|
||||
**Inherited from Winlator (through the forks):**
|
||||
- the five small app files in §1 (228 lines);
|
||||
- the **PulseAudio AAudio sink** in the runtime's audio bundle: Bannerlator's adaptive
|
||||
`module-aaudio-sink` is a fork of Bruno's and Tom Yan's (brunodev85/pulseaudio-android,
|
||||
LGPL-2.1) with adaptive buffer sizing added. It lives in `pulseaudio.tzst` and the `libpulse*`
|
||||
prebuilts, not in app code. This is the one inherited piece that does real work.
|
||||
|
||||
**Written fresh in the forks, not Winlator's:**
|
||||
- the Wayland compositor and its Vulkan present path (Bannerlator);
|
||||
- DirectAudio, both the Wine driver and the relay (Bannerlator);
|
||||
- Win-FG (Bannerlator, FSR3-derived) and LSFG Native (WinNative's port of lsfg-vk);
|
||||
- the gamescope runtime, the session shim and the fake-evdev layer (WinNative); the proot launch
|
||||
here measures 1.5 % against Winlator's launcher — proot is the shared idea, none of the code is;
|
||||
- the file manager, the front end, the settings, the session service (Bannerlator / this app).
|
||||
|
||||
## 1. App code that is measurably Winlator's
|
||||
|
||||
| File | Bruno's original | Shared text | Lines | What it is |
|
||||
|---|---|---|---|---|
|
||||
| `core/Callback.java` | `core/Callback.java` | 55 % | 6 | a one-method callback interface |
|
||||
| `core/EnvironmentComponent.java` | `xenvironment/EnvironmentComponent.java` | 51 % | 17 | start/stop base for the audio components |
|
||||
| `input/GamepadState.java` | `inputcontrols/GamepadState.java` | 33 % | 58 | six axis floats, a d-pad array, a button mask |
|
||||
| `core/EnvVars.java` | `core/EnvVars.java` | 21 % | 31 | KEY=VALUE list helper |
|
||||
| `core/ProcessHelper.java` | `core/ProcessHelper.java` | 21 % | 116 | spawn a process, drain its output |
|
||||
|
||||
Five files, 228 lines, none above 55 %. Everything else with a Winlator name on it measures under
|
||||
12 % against Bruno's text — i.e. the file name and a few field names survived, the code did not:
|
||||
|
||||
| File | Compared with | Shared text |
|
||||
|---|---|---|
|
||||
| `audio/PulseAudioComponent.java` | Bruno's PulseAudioComponent | 11 % |
|
||||
| `core/FileUtils.java` | Bruno's FileUtils | 9 % |
|
||||
| `core/TarZst.java` | Bruno's TarCompressorUtils | 8 % |
|
||||
| `input/PadBridge.java` | Bruno's ExternalController | 7 % |
|
||||
| `files/FileOps.kt` | Bruno's FileUtils | 4 % |
|
||||
| `core/DeviceReport.kt` | Bruno's GPUInformation | 2 % |
|
||||
| `input/EvdevKeys.java` | Bruno's XKeycode | 2 % |
|
||||
| `gpu/TurnipDriver.java` | AdrenotoolsManager (pipetto-crypto's, not Bruno's) | 7 % |
|
||||
|
||||
## 2. Things named after Winlator that are not Winlator's code
|
||||
|
||||
- **`app/src/main/cpp/winlator/`** — the directory name only. Inside: `lsfg/` (WinNative's port of
|
||||
lsfg-vk, credited to Eden / PancakeTAS), `winfg/` (Bannerlator's Win-FG, FSR3-derived, with its own
|
||||
LICENSE and FidelityFX notice), `VulkanRendererContext.h` (a Vulkan dispatch table struct from
|
||||
Bannerlator's renderer, first written by yaywoohoo in June 2026). Referenced by 6 lines in
|
||||
`cpp/CMakeLists.txt` and 8 in `waylandcomp/CMakeLists.txt`, plus 9 comment lines in the compositor.
|
||||
- **`scanout/ScanoutContext.h`** — Bannerlator's (The412Banner, June 2026). Log tag `Winlator_Scanout`.
|
||||
- **`VulkanRendererContext.h`** log tags `Winlator_Renderer`, `Winlator_Scanout`.
|
||||
- **`fakeinput_steam.cpp`** — Max's, from WinNative. Connects to an abstract socket named
|
||||
`winlator_vibration` for rumble. **Nothing in this app serves that socket** (the server was
|
||||
Winlator's WinHandler); the connect fails silently and rumble is dead code here.
|
||||
- **`winlator-contents`** — the name of the public catalog repo, in two URLs (`linuxfs.json`,
|
||||
`desktop.json`) and the README. A repo name, not code.
|
||||
- **File manager comments** mention a `/storage/emulated/0/Winlator/…` path as an example.
|
||||
- **`WAYLAND_RUNTIME.md`, `HDR_RECON.md`** — design notes carried from Bannerlator that describe
|
||||
Bannerlator's X11 renderer (`libwinlator.so`), which this app does not have.
|
||||
|
||||
## 3. Credits and prose
|
||||
|
||||
- `README.md` — "Lineage and licence" paragraph and the credits list name Winlator.
|
||||
- `ui/MainScreen.kt` — the credits dialog text (the rest of that file is dead code).
|
||||
- `docs/releases/0.1.0.md` … `0.1.5.md` — a Winlator credit line each (published release notes;
|
||||
history, leave as is).
|
||||
- `PROGRESS_LOG.md` — five mentions, all of the `winlator-contents` repo.
|
||||
|
||||
## 4. What it would take
|
||||
|
||||
**To stop using Winlator code at all** — rewrite the five files in §1 (228 lines, an afternoon):
|
||||
a callback interface, a start/stop base class, a pad state holder, an env list, a process spawner.
|
||||
None of them does anything Winlator-specific; they are the shapes any such app has. Then the one
|
||||
real piece: a **new AAudio sink module** for PulseAudio in place of the forked one — a few hundred
|
||||
lines of C against the PulseAudio 13 module API (a sink that pulls from PA's render loop into an
|
||||
AAudio stream, with the adaptive buffer logic Bannerlator added kept), rebuilt into the audio
|
||||
bundle by the existing build-pulseaudio workflow. A day or two, and the part most worth testing.
|
||||
|
||||
**To stop being named after it** — rename `cpp/winlator/` (to `framegen/`), fix the two CMake
|
||||
files, the log tags, the abstract-socket name (and remove the dead rumble client while at it), and
|
||||
the comments; drop the Bannerlator design notes that describe the X11 path this app never had.
|
||||
|
||||
**To take it out of the credits** — the README paragraph, the credits dialog, and the credit line
|
||||
in future release notes. Past notes stay as published.
|
||||
|
||||
**What does not change** — the licence. Everything here is GPL-3.0 and stays so: the runtime and the
|
||||
fake-evdev layer are Max's (WinNative, GPL-3.0), the compositor, frame generation, file manager
|
||||
and the session are Bannerlator's (GPL-3.0), lsfg-vk and FidelityFX carry their own notices. Leaving
|
||||
Winlator's lineage does not leave the GPL, and does not touch the runtime image, which is shared
|
||||
with Bannerlator by design.
|
||||
@@ -1,41 +1,41 @@
|
||||
# SteamDeck — Progress Log
|
||||
# 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*.
|
||||
`docs/releases/` (what each version said) - this is *how it got here and where it stands*.
|
||||
|
||||
---
|
||||
|
||||
## 🔖 0.1.5 RELEASED 2026-09-23 03:17 — known-good point
|
||||
## 0.1.5 RELEASED 2026-09-23 03:17 - known-good point
|
||||
|
||||
- **Tag `0.1.5` = `d2f4a84`** (bump commit, versionCode 6), notes `docs/releases/0.1.5.md` on main after it. Private release [0.1.5](https://github.com/The412Banner/SteamDeck/releases/tag/0.1.5) and public [`SteamDeck-0.1.5`](https://github.com/The412Banner/winlator-contents/releases/tag/SteamDeck-0.1.5) on winlator-contents, both Latest, asset `SteamDeck-0.1.5.apk` 21,093,959 bytes, sha256 `85b42954…`, run 35813465422. Staged as `/sdcard/Download/SteamDeck-0.1.5.apk`.
|
||||
- What went in since 0.1.4 (58 commits): the motion front end, settings as pages with anchored menus, the session drawer, three themes (Paper default) and the new icon, run-as-a-game + libpci fix (117 fps Big Picture on a Fold), client-interface switches, FEX presets, 720p default, TZ, non-Adreno gate, 16 KB app libs, two on-screen sticks + bar-aware layout, folded categories at launch, Kurt's #3 (Back key) / #4 (icon) / #5 (local build helpers), pull-request CI + contributions ledger.
|
||||
- Untested at release: sticks/bar layout, FEX presets, Deck mode, desktop → Steam hand-off, the `perf:` line. Rollback: `git reset --hard 8e58e8e` (0.1.4) + `SteamDeck-0.1.4.apk`.
|
||||
- After the cut: the public release on winlator-contents got the private title, and that repo's README a SteamDeck section (latest release, `linuxfs.json`, `desktop.json`) — refresh both at every cut. Verified on the FIT's client binary: there is no `-steampal` flag; the Deck flags are `-steamdeck` / `-steamos3` (Deck mode on the Performance page), `-gamepadui`, `-steamos`.
|
||||
- **Next, in order:** (1) device pass of 0.1.5 — sticks in a game and the bar layout on the Fold, the `perf:` line and 720p in a session log, a FEX preset, the desktop → Steam hand-off; (2) decide the stick click gesture (double tap or long press); (3) Kurt's draft PR #1 (Decky installer) builds when he marks it ready — it compiles against main, minus two stray `.kotlin` files; (4) rebuild the six PulseAudio prebuilts 16 KB-aligned; (5) optional FIFO/mailbox switch on the compositor's Vulkan present path; (6) the rename away from Valve marks is still open (icon is now the "A").
|
||||
- After the cut: the public release on winlator-contents got the private title, and that repo's README a SteamDeck section (latest release, `linuxfs.json`, `desktop.json`) - refresh both at every cut. Verified on the FIT's client binary: there is no `-steampal` flag; the Deck flags are `-steamdeck` / `-steamos3` (Deck mode on the Performance page), `-gamepadui`, `-steamos`.
|
||||
- **Next, in order:** (1) device pass of 0.1.5 - sticks in a game and the bar layout on the Fold, the `perf:` line and 720p in a session log, a FEX preset, the desktop → Steam hand-off; (2) decide the stick click gesture (double tap or long press); (3) Kurt's draft PR #1 (Decky installer) builds when he marks it ready - it compiles against main, minus two stray `.kotlin` files; (4) rebuild the six PulseAudio prebuilts 16 KB-aligned; (5) optional FIFO/mailbox switch on the compositor's Vulkan present path; (6) the rename away from Valve marks is still open (icon is now the "A").
|
||||
|
||||
## 2026-09-22 (late) — motion front end + run-as-a-game, branch `feat/frontend-motion` (unmerged)
|
||||
## 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.
|
||||
- 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.
|
||||
- CI for pull requests (2026-09-23 early): `Build APK` runs on `pull_request_target` against main (a conflicting pull request never fires `pull_request`), checks the pull request out at its head with full history, merges main itself, and leaves one comment kept current: mergeable + built (apk on the run), or the conflicting files and hunks, or the compiler's errors from the kept Gradle log. Drafts wait. Fork pull requests may run without approval (repo setting). Proven with a throwaway pull request (#2, closed) both ways. `Contributions ledger` rewrites the README's contributors table on open/merge/close via `.github/scripts/contributions.sh`, committed to main by the Actions bot.
|
||||
|
||||
## 🔖 Checkpoint 2026-09-22 22:xx — known-good point for the front end
|
||||
## 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
|
||||
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
|
||||
- **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:
|
||||
@@ -49,9 +49,9 @@ the timeline, then lessons and backlog. Companion to the README (what the app *d
|
||||
## 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
|
||||
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
|
||||
- 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,
|
||||
@@ -59,7 +59,7 @@ the timeline, then lessons and backlog. Companion to the README (what the app *d
|
||||
|
||||
## State at 0.1.4 (2026-09-22)
|
||||
|
||||
- **Latest release: 0.1.4** — private: https://github.com/The412Banner/SteamDeck/releases/tag/0.1.4
|
||||
- **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
|
||||
@@ -82,64 +82,64 @@ the timeline, then lessons and backlog. Companion to the README (what the app *d
|
||||
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
|
||||
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
|
||||
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.
|
||||
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
|
||||
- **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
|
||||
- **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),
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **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;
|
||||
- `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
|
||||
- `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
|
||||
- `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
|
||||
- `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
|
||||
- `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).
|
||||
- `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).
|
||||
@@ -147,18 +147,18 @@ the timeline, then lessons and backlog. Companion to the README (what the app *d
|
||||
- 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
|
||||
- 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.
|
||||
- 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
|
||||
- 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.
|
||||
- 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.
|
||||
+42
-42
@@ -1,6 +1,6 @@
|
||||
# SteamDeck 0.1
|
||||
|
||||
**Valve's native ARM64 Linux Steam client, on an Android phone or handheld — and now a Linux
|
||||
**Valve's native ARM64 Linux Steam client, on an Android phone or handheld - and now a Linux
|
||||
desktop beside it.** No Wine, and no x86 translation for the client itself: it is ARM code running
|
||||
as ARM code, under gamescope, presenting through an in-app Vulkan compositor onto your screen.
|
||||
Windows games it launches go through Valve's own ARM64 Proton, which is where FEX comes in.
|
||||
@@ -8,7 +8,7 @@ Windows games it launches go through Valve's own ARM64 Proton, which is where FE
|
||||
This is the first release. One screen, one button: install the runtime, press **Play**, Big
|
||||
Picture comes up.
|
||||
|
||||
> ⚠️ **Early software.** The Steam client and the desktop have both been run end to end on real
|
||||
> **Early software.** The Steam client and the desktop have both been run end to end on real
|
||||
> hardware, but this is version 0.1 and a great deal of it has been exercised on one or two
|
||||
> devices only. Where something has not been tested, this page says so rather than implying it
|
||||
> works.
|
||||
@@ -17,16 +17,16 @@ Picture comes up.
|
||||
|
||||
# What it does
|
||||
|
||||
## 🎮 The Steam client, natively
|
||||
## The Steam client, natively
|
||||
|
||||
Sign in, browse your library, install and play. The client is fetched from Valve's own manifest on
|
||||
the device at first use — **no Valve software is redistributed here**. Windows games run through
|
||||
the device at first use - **no Valve software is redistributed here**. Windows games run through
|
||||
the ARM64 Proton build Valve ships, downloaded by Steam itself.
|
||||
|
||||
✅ **Proven on device:** sign-in, Proton ARM64 fetched automatically, a game installed from the
|
||||
store and launched — *FlatOut* rendering at 144 Hz.
|
||||
**Proven on device:** sign-in, Proton ARM64 fetched automatically, a game installed from the
|
||||
store and launched - *FlatOut* rendering at 144 Hz.
|
||||
|
||||
## 🖥️ A Linux desktop
|
||||
## A Linux desktop
|
||||
|
||||
A second thing the app can start: **LXQt on labwc**, in the same session and the same compositor,
|
||||
with Xwayland for X11 programs. Everything on it is downloaded on request from a catalog hosted
|
||||
@@ -36,56 +36,56 @@ beside the runtime, so the app stays small.
|
||||
|---|---|
|
||||
| **Desktop** (406 MB) | labwc, LXQt panel, file manager, terminal, text and image viewers, Firefox, mpv |
|
||||
| **Emulators** (694 MB) | PPSSPP and RetroArch with 28 cores, plus ScummVM, DOSBox, Mednafen, Snes9x |
|
||||
| **RPCS3 · DuckStation · melonDS** | PS3, PS1, DS — the projects' own ARM64 builds |
|
||||
| **PCSX2 · Dolphin · Cemu** | PS2, GameCube/Wii, Wii U — **no ARM64 Linux build exists upstream for any of these three**, so they are compiled from source against the same glibc the runtime uses |
|
||||
| **RPCS3 · DuckStation · melonDS** | PS3, PS1, DS - the projects' own ARM64 builds |
|
||||
| **PCSX2 · Dolphin · Cemu** | PS2, GameCube/Wii, Wii U - **no ARM64 Linux build exists upstream for any of these three**, so they are compiled from source against the same glibc the runtime uses |
|
||||
|
||||
Between them: PS1, PS2, PS3, PSP, GameCube, Wii, Wii U, N64, SNES, NES, Game Boy/Color/Advance,
|
||||
DS, Genesis, Dreamcast, Saturn, PC Engine, arcade and DOS.
|
||||
|
||||
✅ **Proven on device:** the desktop starts, panel and wallpaper and file manager all come up, at
|
||||
**Proven on device:** the desktop starts, panel and wallpaper and file manager all come up, at
|
||||
the panel's native resolution, and Firefox browses the live internet at 93 fps.
|
||||
⚠️ **Not yet run on device:** the emulators themselves. They build, publish and install; none has
|
||||
**Not yet run on device:** the emulators themselves. They build, publish and install; none has
|
||||
been launched with a game yet.
|
||||
|
||||
## 🎞️ Frame generation
|
||||
## Frame generation
|
||||
|
||||
Extra frames generated between the real ones **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.
|
||||
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` out of 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.
|
||||
screen - it applies immediately, mid-game included.
|
||||
|
||||
✅ **Proven on device:** LSFG 2× generating in *FlatOut* — 30 → 61, 60 → 118 and 61 → 123 fps
|
||||
**Proven on device:** LSFG 2× generating in *FlatOut* - 30 → 61, 60 → 118 and 61 → 123 fps
|
||||
observed, with the on-screen readout showing `base → generated`.
|
||||
|
||||
## 🎛️ Controls, pointer and keyboard
|
||||
## Controls, pointer and keyboard
|
||||
|
||||
A physical controller is republished into the session as a synthetic **Xbox 360 pad** — the
|
||||
A physical controller is republished into the session as a synthetic **Xbox 360 pad** - the
|
||||
identity SDL and Steam have a mapping for. With nothing attached an **on-screen pad** appears and
|
||||
writes 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 returns.
|
||||
|
||||
The app draws the pointer. On the desktop, touch is a **touchpad**: 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-then-drag drags. In Steam, touch is **direct** — the
|
||||
tap) right-clicks, two fingers scroll, hold-then-drag drags. In Steam, touch is **direct** - the
|
||||
pointer jumps under your finger. A USB or Bluetooth mouse works as itself.
|
||||
|
||||
**Keyboard** in the drawer puts the soft keyboard over the session, turned into the key presses
|
||||
that would have typed it — Shift included, so capitals and the symbol row arrive as themselves.
|
||||
that would have typed it - Shift included, so capitals and the symbol row arrive as themselves.
|
||||
|
||||
⚠️ **Not yet tested on device:** the soft keyboard.
|
||||
**Not yet tested on device:** the soft keyboard.
|
||||
|
||||
## 🌐 Networking
|
||||
## Networking
|
||||
|
||||
The session uses the phone's network directly — proot creates no network namespace, so **IPv4 and
|
||||
IPv6 both pass straight through** — and the app writes the runtime's `/etc/resolv.conf` from the
|
||||
The session uses the phone's network directly - proot creates no network namespace, so **IPv4 and
|
||||
IPv6 both pass straight through** - and the app writes the runtime's `/etc/resolv.conf` from the
|
||||
active network's own DNS servers every time the link changes.
|
||||
|
||||
✅ **Proven on device:** name resolution and an HTTPS fetch over both IPv4 and IPv6 from inside
|
||||
**Proven on device:** name resolution and an HTTPS fetch over both IPv4 and IPv6 from inside
|
||||
the session, and Firefox loading live pages.
|
||||
|
||||
## 🔌 Starting Steam offline
|
||||
## Starting Steam offline
|
||||
|
||||
**Start offline** on the main screen brings the client up without signing in to Valve. Installed
|
||||
games launch; Proton and the frame generation are local anyway. It lives on the main screen and
|
||||
@@ -93,19 +93,19 @@ not in the drawer for a reason: the client reads the keys that control this *onc
|
||||
starts, so changing it mid-session would do nothing until the next launch. Without an earlier
|
||||
successful sign-in the button says so and stays disabled.
|
||||
|
||||
⚠️ **Not yet tested on device.** Games with always-online DRM will not start offline, and nothing
|
||||
**Not yet tested on device.** Games with always-online DRM will not start offline, and nothing
|
||||
can be installed or updated while offline.
|
||||
|
||||
## 🧰 Compatibility tools
|
||||
## Compatibility tools
|
||||
|
||||
**Compatibility tools** installs **GE-Proton** or **proton-cachyos** — native ARM64 builds —
|
||||
**Compatibility tools** installs **GE-Proton** or **proton-cachyos** - native ARM64 builds -
|
||||
beside the ARM64 Proton Valve ships. The runtime's registrar fetches a requested build at the next
|
||||
session start, patches it past pressure-vessel, and it appears in Steam under Properties →
|
||||
Compatibility for any game.
|
||||
|
||||
⚠️ **Not yet tested in this app.**
|
||||
**Not yet tested in this app.**
|
||||
|
||||
## 📱 Background, foreground and the drawer
|
||||
## Background, foreground and the drawer
|
||||
|
||||
A session belongs to a **foreground service**, not to the activity, so leaving Big Picture does
|
||||
not end it: perceptible priority, a partial wake lock so the guest's threads keep running, and a
|
||||
@@ -116,13 +116,13 @@ notification is silent and brings the session back.
|
||||
(auto/always/never), touch mode, display shape, *Send to background* and *Stop session*. A session
|
||||
ends only when you say so.
|
||||
|
||||
## 📐 Foldables and odd panels
|
||||
## Foldables and odd panels
|
||||
|
||||
The main screen scales to whatever panel and density it is given. In a session the display shape
|
||||
is either the panel's own or a fixed **16:9** — the default on a foldable, so opening or closing
|
||||
is either the panel's own or a fixed **16:9** - the default on a foldable, so opening or closing
|
||||
the device mid-session changes only the bars, never the picture.
|
||||
|
||||
✅ **Proven on device:** unfolding and folding a Fold mid-session.
|
||||
**Proven on device:** unfolding and folding a Fold mid-session.
|
||||
|
||||
---
|
||||
|
||||
@@ -131,7 +131,7 @@ the device mid-session changes only the bars, never the picture.
|
||||
Download the APK below and install it. On first launch it downloads the Linux runtime (~790 MB,
|
||||
once). Then press **Play**.
|
||||
|
||||
**If Bannerlator is already installed**, the runtime image is the same one — the README shows how
|
||||
**If Bannerlator is already installed**, the runtime image is the same one - the README shows how
|
||||
to copy it across with root and skip the download entirely. The two apps stay independent after
|
||||
that: separate Steam installs, separate logins, separate games.
|
||||
|
||||
@@ -144,11 +144,11 @@ driver), and roughly 3 GB free for the runtime before any games.
|
||||
|
||||
- **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. Programs still reach the GPU
|
||||
themselves through Vulkan and Turnip — it is the desktop's own compositing that is on the CPU.
|
||||
themselves through Vulkan and Turnip - it is the desktop's own compositing that is on the CPU.
|
||||
- **Firefox's child-process sandboxes are turned off**, because seccomp and namespace sandboxes
|
||||
cannot be set up under proot and its tabs crash otherwise. This is a real reduction in
|
||||
isolation: treat browsing here as less protected than on a normal desktop.
|
||||
- **Changing the display shape takes effect at the next session**, not immediately — gamescope
|
||||
- **Changing the display shape takes effect 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 and there is nothing to
|
||||
mirror or build from.
|
||||
@@ -157,18 +157,18 @@ driver), and roughly 3 GB free for the runtime before any games.
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary><b>🙏 Credits</b> (tap to expand)</summary>
|
||||
<summary><b> Credits</b> (tap to expand)</summary>
|
||||
|
||||
- **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
|
||||
- **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.
|
||||
- **Winlator (brunodev85)** — the PulseAudio-on-AAudio audio stack, the gamepad state model and
|
||||
- **Winlator (brunodev85)** - the PulseAudio-on-AAudio audio stack, the gamepad state model and
|
||||
the shape of a session's host-side components. WinNative and Bannerlator are both Winlator
|
||||
lineage.
|
||||
|
||||
GPL-3.0. gamescope, Mesa, Turnip, Xwayland, labwc, LXQt, PulseAudio, proot and Arch Linux ARM are
|
||||
their own authors'. Valve, Steam, Steam Deck and Proton are trademarks of Valve Corporation; this
|
||||
project is not affiliated with Valve, and no Valve software is redistributed — the client is
|
||||
project is not affiliated with Valve, and no Valve software is redistributed - the client is
|
||||
fetched from Valve's own manifest on your device.
|
||||
|
||||
</details>
|
||||
@@ -177,6 +177,6 @@ fetched from Valve's own manifest on your device.
|
||||
|
||||
---
|
||||
|
||||
**APK** `SteamDeck-0.1.apk` — 15832230 bytes
|
||||
**APK** `SteamDeck-0.1.apk` - 15832230 bytes
|
||||
`sha256 e181dfcc20957c3f824799ed3ef5ec73ddbf9957b7dbb2224c08b7e34ef8db11`
|
||||
Built from `05fc566` by [run 35503326574](https://github.com/The412Banner/SteamDeck/actions/runs/35503326574). Signed with the AOSP test key (v1 + v2 + v3), so it installs over earlier test builds in place.
|
||||
+35
-35
@@ -1,93 +1,93 @@
|
||||
# SteamDeck 0.1.1
|
||||
|
||||
**A patch release, and mostly about the two things 0.1 could not do: change a driver, and tell you
|
||||
what went wrong.** Install it over 0.1 — same key, same runtime, nothing to re-download.
|
||||
what went wrong.** Install it over 0.1 - same key, same runtime, nothing to re-download.
|
||||
|
||||
Everything here came out of one day of reports from three devices, which is why so much of it is
|
||||
about being able to *see* a fault rather than a new feature.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Graphics drivers — a new button on the main screen
|
||||
## Graphics drivers - a new button on the main screen
|
||||
|
||||
Two lists, because a zip for one cannot serve the other:
|
||||
|
||||
- **Linux runtime driver** — the glibc Turnip everything *inside* the runtime renders on: the Steam
|
||||
- **Linux runtime driver** - the glibc Turnip everything *inside* the runtime renders on: the Steam
|
||||
client's interface, every game it launches, and in desktop mode every program on the desktop.
|
||||
Import a `-Linux` Turnip zip; an Android or `-Wayland` zip is refused with the reason rather than
|
||||
becoming a black screen. The choice is **per mode**, so Steam and the desktop can run different
|
||||
builds. Nothing inside the runtime is modified — the session is pointed at the imported driver
|
||||
builds. Nothing inside the runtime is modified - the session is pointed at the imported driver
|
||||
and the runtime's own Turnip is what a removed or unreadable import falls back to.
|
||||
- **Display driver (Android)** — the bionic Turnip the app's compositor puts the frame on the panel
|
||||
- **Display driver (Android)** - the bionic Turnip the app's compositor puts the frame on the panel
|
||||
with. *Auto* keeps the GPU pick between the two bundled builds; an imported AdrenoTools zip can
|
||||
be chosen instead. It applies after the app is fully closed and reopened, because the compositor
|
||||
loads its driver once per process.
|
||||
|
||||
This is how a driver fix now reaches an installed runtime without a ~790 MB re-download.
|
||||
|
||||
✅ **Proven on device:** an imported Mesa 26.3.0 (Vulkan 1.4.363) glibc Turnip drawing a whole Steam
|
||||
session — gamescope, the client, its UI helpers and Xwayland all on it — with an imported Android
|
||||
**Proven on device:** an imported Mesa 26.3.0 (Vulkan 1.4.363) glibc Turnip drawing a whole Steam
|
||||
session - gamescope, the client, its UI helpers and Xwayland all on it - with an imported Android
|
||||
Turnip in the compositor in place of the bundled build.
|
||||
|
||||
### 📱 Adreno 710 / 720 / 722
|
||||
### Adreno 710 / 720 / 722
|
||||
|
||||
0.1 cannot start on these at all: Mesa has no entry for them and the session dies at
|
||||
`device (chip_id = 7010000, gpu_id = 710) is unsupported`. There is now a path — import
|
||||
`device (chip_id = 7010000, gpu_id = 710) is unsupported`. There is now a path - import
|
||||
`Turnip-…-710-720-Test.zip` as the **Display driver** and `…-710-720-Test-Linux.zip` as the **Linux
|
||||
runtime driver**, from Banners-Turnip's `v26.3.0-20260921-r3` or newer. Those GPUs also want
|
||||
sysmem rendering, which the app sets on its own when it sees that driver chosen.
|
||||
|
||||
⚠️ **Unverified on real hardware.** Nobody has run this runtime on a 710 yet. If you have one,
|
||||
**Unverified on real hardware.** Nobody has run this runtime on a 710 yet. If you have one,
|
||||
the session folder is the thing to send.
|
||||
|
||||
---
|
||||
|
||||
## 🔊 Sound, and the microphone
|
||||
## Sound, and the microphone
|
||||
|
||||
**DirectAudio for games** replaces Wine's audio driver *inside the games the client launches* with
|
||||
one that talks straight to Android over a socket — no PulseAudio in between, and no resampling.
|
||||
one that talks straight to Android over a socket - no PulseAudio in between, and no resampling.
|
||||
The client itself keeps PulseAudio either way; it is a native Linux program with no other way to
|
||||
make a sound. It pairs only with a Wine 11 Proton (Valve's ARM64 builds, GE-Proton, proton-cachyos)
|
||||
and stays off on anything else rather than play silence, saying so in the log.
|
||||
|
||||
**Microphone** is its own switch: 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
|
||||
uid - which is what the recording permission is checked against - and PulseAudio exposes it to the
|
||||
client as **DirectAudioMic**, for voice chat. With it off, nothing in the session can record and
|
||||
Android's indicator stays off.
|
||||
|
||||
Both are under **Audio** on the main screen, both off by default, both applying at the next session
|
||||
start. A game's first launch after enabling DirectAudio still uses Proton's own audio — the driver
|
||||
is named in that game's prefix, which does not exist until then — and the second uses DirectAudio.
|
||||
start. A game's first launch after enabling DirectAudio still uses Proton's own audio - the driver
|
||||
is named in that game's prefix, which does not exist until then - and the second uses DirectAudio.
|
||||
|
||||
⚠️ Built and wired; the microphone reached `DirectAudioMic` only after a fix to the bundled
|
||||
Built and wired; the microphone reached `DirectAudioMic` only after a fix to the bundled
|
||||
PulseAudio (0.1 shipped a 17.0 build of the module beside a 13.0 daemon, so it could never load).
|
||||
Treat voice as ready to test rather than proven.
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Steam's overlay, and the app that takes your login
|
||||
## Steam's overlay, and the app that takes your login
|
||||
|
||||
Two deviations from a normal Steam launch, both removed rather than worked around.
|
||||
|
||||
The overlay used to be stripped from every game, because it sat in front of the controller reader.
|
||||
That was an ordering problem — the interposer now goes first and the overlay loads as Valve intends.
|
||||
That was an ordering problem - the interposer now goes first and the overlay loads as Valve intends.
|
||||
Every difference between the launch Valve ships and what runs here is a difference this runtime
|
||||
would otherwise have to account for.
|
||||
|
||||
And another Steam client on the same phone signs this one out seconds after every login. The app
|
||||
now asks Android to stop it when a session starts and says so. ⚠️ Where that app restarts itself
|
||||
through boot receivers, a background stop is a race it can win — if the client keeps signing out,
|
||||
now asks Android to stop it when a session starts and says so. Where that app restarts itself
|
||||
through boot receivers, a background stop is a race it can win - if the client keeps signing out,
|
||||
uninstall the other one.
|
||||
|
||||
---
|
||||
|
||||
## 🧮 Performance and session fixes
|
||||
## Performance and session fixes
|
||||
|
||||
**Performance** on the main screen, all off by default, all applying at the next session start:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Steam client cores** | Steam pins its own interface to a subset of cores it chooses — on one device five of eight, leaving out the fastest — which suits a running game and makes the menus sluggish when the client is all there is. Overriding it 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 |
|
||||
| **Steam client cores** | Steam pins its own interface to a subset of cores it chooses - on one device five of eight, leaving out the fastest - which suits a running game and makes the menus sluggish when the client is all there is. Overriding it 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 |
|
||||
| **Game cores** | the cores a game may run on, applied by exec'ing it through `taskset` so every thread inherits the mask from its first instruction |
|
||||
| **Skip Steam's xalia helper** | `xalia` is an x86 Windows program Proton starts for gamepad navigation. On some devices its system calls are refused in a way it cannot cope with and the session dies seconds after Big Picture appears. Turn this on if a session will not stay up |
|
||||
| **Run proot without seccomp** | proot normally lets most system calls run untraced, which is most of its speed. Some kernels handle that badly and refuse calls that plainly exist. This traces everything instead: slower, but correct |
|
||||
@@ -99,7 +99,7 @@ an experiment that would otherwise need a build per attempt.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Logs worth sending
|
||||
## Logs worth sending
|
||||
|
||||
This is the change that will save the most time. Each session now gets **its own folder**:
|
||||
|
||||
@@ -111,7 +111,7 @@ Download/SteamDeck/session-20260921-161256/
|
||||
crash.log Android's crash buffer, as it stood when the session ended
|
||||
audio.log the PulseAudio daemon and the DirectAudio relay helper
|
||||
session.log the guest session: proot, gamescope, the client's own output
|
||||
wayland.log the compositor — what it presented, and how fast
|
||||
wayland.log the compositor - what it presented, and how fast
|
||||
steam.log + steam/ the client's own logs (Steam mode)
|
||||
desktop.log labwc, the panel and the programs on it (desktop mode)
|
||||
```
|
||||
@@ -122,7 +122,7 @@ names it, the panel, **which driver each half of the session chose**, and every
|
||||
`crash.log` is where a session the *system* killed leaves its only trace.
|
||||
|
||||
**Credentials are scrubbed and nothing identifying is collected.** Steam's logs are filtered line by
|
||||
line on the way in — session tokens, machine-auth GUIDs, WebAPI keys, Guard codes and e-mail
|
||||
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 still correlate.
|
||||
`loginusers.vdf`, `config.vdf` and the `ssfn` files are never copied. No serial number, no device or
|
||||
advertising id, no account name, no network name. **A session folder is meant to be attached to a
|
||||
@@ -130,14 +130,14 @@ report exactly as it is.**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ If Steam dies with nothing in the log
|
||||
## 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,
|
||||
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.
|
||||
Some phones expose it in Developer options as a **"restrict child processes"** switch - turn it off.
|
||||
Otherwise, once per device:
|
||||
|
||||
```sh
|
||||
@@ -151,7 +151,7 @@ disabled.
|
||||
|
||||
## Still true from 0.1
|
||||
|
||||
VAC-protected games report insecure. Games that ship only a Linux build will not start — install
|
||||
VAC-protected games report insecure. Games that ship only a Linux build will not start - install
|
||||
the Windows version instead. The desktop composites in software, and Firefox's child-process
|
||||
sandboxes are off there. No Switch emulator. The emulator packages install but none has been run
|
||||
with a game.
|
||||
@@ -164,24 +164,24 @@ so plainly when it has not.
|
||||
|
||||
## Credits
|
||||
|
||||
- **maxjivi05 (Max)** — the gamescope runtime this is built on: the proot session, the session shim,
|
||||
- **maxjivi05 (Max)** - the gamescope runtime this is built on: the proot session, the session shim,
|
||||
the input interposer and the controller work, from WinNative. The `PROOT_NO_SECCOMP` fallback and
|
||||
the phantom-process finding above are his too.
|
||||
- **The412Banner** — the app, the Wayland compositor, the runtime plumbing and the Steam session.
|
||||
- **Winlator (brunodev85)** — the PulseAudio-on-AAudio audio stack, the gamepad state model and the
|
||||
- **The412Banner** - the app, the Wayland compositor, the runtime plumbing and the Steam session.
|
||||
- **Winlator (brunodev85)** - the PulseAudio-on-AAudio audio stack, the gamepad state model and the
|
||||
shape of a session's host-side components.
|
||||
- **Vauzi (Vauzi-17/710)** — the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
- **Vauzi (Vauzi-17/710)** - the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
|
||||
Frame generation: **Win-FG** is built in; **LSFG** uses the shader chain from *your own copy* of
|
||||
Lossless Scaling, read on device and never redistributed, on groundwork from **lsfg-vk (PancakeTAS)**.
|
||||
|
||||
GPL-3.0. gamescope, Mesa, Turnip, Xwayland, labwc, LXQt, PulseAudio, proot and Arch Linux ARM are
|
||||
their own authors'. **Not affiliated with Valve** — Steam, Proton and Steam Deck are Valve's, and no
|
||||
their own authors'. **Not affiliated with Valve** - Steam, Proton and Steam Deck are Valve's, and no
|
||||
Valve software is redistributed here: the client is fetched from Valve's own manifest on your device.
|
||||
|
||||
---
|
||||
|
||||
**APK** `SteamDeck-0.1.1.apk` — 15927028 bytes
|
||||
**APK** `SteamDeck-0.1.1.apk` - 15927028 bytes
|
||||
`sha256 db4b0604ca53c4441bb66d0764ca7ff98b2a9f7bdfc48e3d52c6024b3a4bf78c`
|
||||
versionCode 2, built from `2570562` by [run 35634795441](https://github.com/The412Banner/SteamDeck/actions/runs/35634795441).
|
||||
Signed with the AOSP test key (v1 + v2 + v3), so it installs over 0.1 and the r24–r34 test builds in place.
|
||||
|
||||
+17
-17
@@ -1,14 +1,14 @@
|
||||
# SteamDeck 0.1.2
|
||||
|
||||
**A fix release. 0.1.1 could hang the app when you left a session — that is fixed, and the two log
|
||||
**A fix release. 0.1.1 could hang the app when you left a session - that is fixed, and the two log
|
||||
lines that would have explained it in the first place are in.** Install it over 0.1.1; same key,
|
||||
same runtime, nothing to re-download.
|
||||
|
||||
---
|
||||
|
||||
## 🩹 Leaving a session could hang the app
|
||||
## Leaving a session could hang the app
|
||||
|
||||
0.1.1 collected a session's logs on the main thread as the session stopped — copying the
|
||||
0.1.1 collected a session's logs on the main thread as the session stopped - copying the
|
||||
compositor's log, scrubbing every one of the client's logs line by line, then waiting on Android's
|
||||
crash buffer. On a desktop session that was around four and a half seconds of frozen app, and
|
||||
Android ended it as not responding. The collecting now happens on its own thread.
|
||||
@@ -17,26 +17,26 @@ This was introduced by 0.1.1's own logging, and it is the whole reason for this
|
||||
|
||||
`crash.log` was wrong twice over in 0.1.1 as well: it waited for output that arrives on another
|
||||
thread, so it declared the buffer empty every time, and it printed a process id where an exit
|
||||
status belonged. It now waits for the dump to finish and says which of three things happened —
|
||||
status belonged. It now waits for the dump to finish and says which of three things happened -
|
||||
could not start, did not finish, or genuinely nothing has crashed.
|
||||
|
||||
## 🔍 Two log lines that were missing
|
||||
## Two log lines that were missing
|
||||
|
||||
Both came out of chasing a real report: a game that came back from the Steam menu as a small box in
|
||||
the corner of the screen.
|
||||
|
||||
- **`changed its frame size: 1920x1080 -> …`** — a window that commits frames at a new size has
|
||||
- **`changed its frame size: 1920x1080 -> …`** - a window that commits frames at a new size has
|
||||
rebuilt its swapchain, and that is exactly what a game shrinking looks like from the compositor's
|
||||
side. The size was previously nowhere in the logs and had to be guessed from a screenshot.
|
||||
- **`renamed "Steam Big Picture Mode" -> "Flat-Out"`** — gamescope keeps one window and renames it
|
||||
- **`renamed "Steam Big Picture Mode" -> "Flat-Out"`** - gamescope keeps one window and renames it
|
||||
as the game takes the screen. That was silent, so the log showed a window *closed* under a name it
|
||||
had never been logged as opening, which looked like a missing event and was really a rename.
|
||||
|
||||
## 📖 A README that matches the app
|
||||
## A README that matches the app
|
||||
|
||||
The README had grown a section per feature in the order they were written, and the parts describing
|
||||
the whole had fallen behind. The feature ledger now covers the drivers, audio, cores, session fixes
|
||||
and logs, with an honest state against each — including, in plain words, the several that set what
|
||||
and logs, with an honest state against each - including, in plain words, the several that set what
|
||||
they claim but have never been measured on hardware. The architecture diagram shows the audio relay
|
||||
and both Turnip slots. The session-log folder has a section of its own. And **Installing** now
|
||||
mentions the one Android setting that decides whether a session stays up at all.
|
||||
@@ -54,11 +54,11 @@ identifying collected · a path for Adreno 710 / 720 / 722 through imported driv
|
||||
See the [0.1.1 notes](https://github.com/The412Banner/SteamDeck/releases/tag/0.1.1) for what each
|
||||
of those does, and the README's ledger for what is proven versus merely wired.
|
||||
|
||||
## ⚠️ If the client dies with nothing in its log
|
||||
## If the client dies with nothing in its log
|
||||
|
||||
That is usually not the client. **Android 12 and later kill the extra processes an app starts for
|
||||
itself**, and a session is made of dozens. Some phones have a **"restrict child processes"** switch
|
||||
in Developer options — turn it off. Otherwise, once per device:
|
||||
in Developer options - turn it off. Otherwise, once per device:
|
||||
|
||||
```sh
|
||||
adb shell settings put global settings_enable_monitor_phantom_procs false
|
||||
@@ -69,21 +69,21 @@ says so when it is not disabled.
|
||||
|
||||
## Credits
|
||||
|
||||
- **maxjivi05 (Max)** — the gamescope runtime this is built on, from WinNative.
|
||||
- **The412Banner** — the app, the Wayland compositor, the runtime plumbing and the Steam session.
|
||||
- **Winlator (brunodev85)** — the audio stack, the gamepad model and the shape of a session.
|
||||
- **Vauzi (Vauzi-17/710)** — the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
- **maxjivi05 (Max)** - the gamescope runtime this is built on, from WinNative.
|
||||
- **The412Banner** - the app, the Wayland compositor, the runtime plumbing and the Steam session.
|
||||
- **Winlator (brunodev85)** - the audio stack, the gamepad model and the shape of a session.
|
||||
- **Vauzi (Vauzi-17/710)** - the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
|
||||
Frame generation: **Win-FG** is built in; **LSFG** uses the shader chain from *your own copy* of
|
||||
Lossless Scaling, read on device and never redistributed, on groundwork from **lsfg-vk (PancakeTAS)**.
|
||||
|
||||
GPL-3.0. gamescope, Mesa, Turnip, Xwayland, labwc, LXQt, PulseAudio, proot and Arch Linux ARM are
|
||||
their own authors'. **Not affiliated with Valve** — Steam, Proton and Steam Deck are Valve's, and no
|
||||
their own authors'. **Not affiliated with Valve** - Steam, Proton and Steam Deck are Valve's, and no
|
||||
Valve software is redistributed here: the client is fetched from Valve's own manifest on your device.
|
||||
|
||||
---
|
||||
|
||||
**APK** `SteamDeck-0.1.2.apk` — 15927028 bytes
|
||||
**APK** `SteamDeck-0.1.2.apk` - 15927028 bytes
|
||||
`sha256 a761ba112fe97f4631d598f2a5d0b1898f9b81f4575771ded39c17b743f80825`
|
||||
versionCode 3, built from `1f0292c` by [run 35674235857](https://github.com/The412Banner/SteamDeck/actions/runs/35674235857).
|
||||
Signed with the AOSP test key (v1 + v2 + v3), so it installs over 0.1.1 in place.
|
||||
|
||||
+12
-12
@@ -5,7 +5,7 @@ the truth.** Install it over 0.1.2; same key, same runtime, nothing to re-downlo
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Settings → Internet works, and the header shows a connection
|
||||
## Settings → Internet works, and the header shows a connection
|
||||
|
||||
The Steam client builds its network page and its connection indicator on NetworkManager, and looks
|
||||
for one on the system bus as it starts. This runtime had neither, so the page stopped with
|
||||
@@ -13,7 +13,7 @@ for one on the system bus as it starts. This runtime had neither, so the page st
|
||||
downloads ran. A user on an Odin 2 reported exactly that in the first day.
|
||||
|
||||
The session now starts a small system bus and a **stand-in that answers for NetworkManager**. It
|
||||
reports what is true and nothing more — one connection that is up while Android has a route out —
|
||||
reports what is true and nothing more - one connection that is up while Android has a route out -
|
||||
and refuses 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.
|
||||
|
||||
@@ -24,10 +24,10 @@ runtime gets this without a re-download; `dbus-daemon` and `python-gi` were alre
|
||||
the only one of the three without it. Credit where it is due, and an apology to the Odin 2 reporter,
|
||||
who was told it was cosmetic and unfixed when it had been fixed upstream for days.
|
||||
|
||||
## 🔍 Where the runtime stands against its sources
|
||||
## Where the runtime stands against its sources
|
||||
|
||||
With this release the standalone's runtime scripts are level with Bannerlator's, and its sandbox
|
||||
shims are byte-for-byte the same behaviour as Max's current branch — checked file by file today;
|
||||
shims are byte-for-byte the same behaviour as Max's current branch - checked file by file today;
|
||||
every difference is a deliberate rename or an attribution header. What remains on his side and not
|
||||
here is a feature, not a fix: launching Epic games from the native client.
|
||||
|
||||
@@ -42,31 +42,31 @@ session fixes · `Download/steamdeck-env` · a folder of logs per session with c
|
||||
a path for Adreno 710 / 720 / 722 · a README with an honest ledger of what is proven and what is
|
||||
merely wired.
|
||||
|
||||
## ⚠️ If the client dies with nothing in its log
|
||||
## 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. Some phones have a **"restrict child processes"** switch in Developer options — turn it off.
|
||||
dozens. Some phones have a **"restrict child processes"** switch in Developer options - turn it off.
|
||||
Otherwise, once per device: `adb shell settings put global settings_enable_monitor_phantom_procs
|
||||
false`. `device.txt` in every session folder records what your phone reports.
|
||||
|
||||
## Credits
|
||||
|
||||
- **maxjivi05 (Max)** — the gamescope runtime this is built on, the sandbox shims, and the
|
||||
- **maxjivi05 (Max)** - the gamescope runtime this is built on, the sandbox shims, and the
|
||||
NetworkManager stand-in in this release, from WinNative.
|
||||
- **The412Banner** — the app, the Wayland compositor, the runtime plumbing and the Steam session.
|
||||
- **Winlator (brunodev85)** — the audio stack, the gamepad model and the shape of a session.
|
||||
- **Vauzi (Vauzi-17/710)** — the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
- **The412Banner** - the app, the Wayland compositor, the runtime plumbing and the Steam session.
|
||||
- **Winlator (brunodev85)** - the audio stack, the gamepad model and the shape of a session.
|
||||
- **Vauzi (Vauzi-17/710)** - the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
|
||||
Frame generation: **Win-FG** is built in; **LSFG** uses the shader chain from *your own copy* of
|
||||
Lossless Scaling, read on device and never redistributed, on groundwork from **lsfg-vk (PancakeTAS)**.
|
||||
|
||||
GPL-3.0. gamescope, Mesa, Turnip, Xwayland, labwc, LXQt, PulseAudio, proot and Arch Linux ARM are
|
||||
their own authors'. **Not affiliated with Valve** — Steam, Proton and Steam Deck are Valve's, and no
|
||||
their own authors'. **Not affiliated with Valve** - Steam, Proton and Steam Deck are Valve's, and no
|
||||
Valve software is redistributed here: the client is fetched from Valve's own manifest on your device.
|
||||
|
||||
---
|
||||
|
||||
**APK** `SteamDeck-0.1.3.apk` — 15931221 bytes
|
||||
**APK** `SteamDeck-0.1.3.apk` - 15931221 bytes
|
||||
`sha256 3f85118755234c5d8b1fb59640f817485d8360db7c63c142072c6b1025606542`
|
||||
versionCode 4, built from `3aa0162` by [run 35676349317](https://github.com/The412Banner/SteamDeck/actions/runs/35676349317).
|
||||
Signed with the AOSP test key (v1 + v2 + v3), so it installs over 0.1.2 in place.
|
||||
|
||||
+19
-19
@@ -6,19 +6,19 @@ re-download.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ The desktop can see your files — and a ROMs folder
|
||||
## The desktop can see your files - and a ROMs folder
|
||||
|
||||
A file dialog inside the desktop (RPCS3's "Select Game Folder", say) showed only the runtime's own
|
||||
tree. The phone's storage *was* mounted the whole time, at `/storage/emulated/0`; the dialogs
|
||||
just could not show it — they list "Computer" from the mount table, where a proot bind never
|
||||
just could not show it - they list "Computer" from the mount table, where a proot bind never
|
||||
appears, and open in the home folder, where nothing pointed at the phone.
|
||||
|
||||
The home folder now carries two entries every session: **`Storage`**, all of internal storage, and
|
||||
**`ROMs`**, the folder chosen with the new **ROMs folder** tile — anywhere on the device, an SD
|
||||
**`ROMs`**, the folder chosen with the new **ROMs folder** tile - anywhere on the device, an SD
|
||||
card included (it is a bind, not a link). Point an emulator at `/root/ROMs` once. Both are the
|
||||
live folders: a save written next to a ROM lands on the phone.
|
||||
|
||||
## 📁 Files — Bannerlator's File Manager, whole
|
||||
## Files - Bannerlator's File Manager, whole
|
||||
|
||||
The **Files** tile is Bannerlator's File Manager carried over as it is: grid and list, sort,
|
||||
search, multi-select, copy / cut / paste with conflict handling and a cancellable progress bar,
|
||||
@@ -28,9 +28,9 @@ the ROMs folder and imports a driver, in place of Android's document picker. Lef
|
||||
they have no meaning without a Wine container: run, add to shortcuts, the C: / Z: drives and the
|
||||
archive unpacker.
|
||||
|
||||
## ⚙️ A cog beside Play and beside Desktop
|
||||
## A cog beside Play and beside Desktop
|
||||
|
||||
Everything that only matters for one mode moved behind that mode's cog — a settings window with
|
||||
Everything that only matters for one mode moved behind that mode's cog - a settings window with
|
||||
a rail of sections: **Display** (resolution up to 1080p by default, 900p, 720p or the panel's own;
|
||||
the shape), **HDR**, the **runtime driver** for that mode, the **display driver**, **touch**, and
|
||||
for Steam the on-screen controls and audio, for the desktop its renderer. The Graphics drivers and
|
||||
@@ -42,12 +42,12 @@ Performance · ROMs folder · Files · Session logs.
|
||||
HDR10. On, the compositor offers `wp_color_manager_v1` with BT.2020 + PQ and 10-bit buffers,
|
||||
gamescope starts with `--hdr-enabled` and games get `DXVK_HDR=1`. The compositor decides this once,
|
||||
when it starts, so like the display driver it applies after the app is fully closed and opened
|
||||
again. ⚠️ Wired end to end; not yet seen on a device from this app.
|
||||
again. Wired end to end; not yet seen on a device from this app.
|
||||
|
||||
## 🩹 Logs that survive a crash, and a session that always starts
|
||||
## Logs that survive a crash, and a session that always starts
|
||||
|
||||
A session's folder used to be finished only at a clean stop; an app crash or an Android kill left
|
||||
it half-written — a user saw the toast name a log that was not there. The folder is now finished
|
||||
it half-written - a user saw the toast name a log that was not there. The folder is now finished
|
||||
three ways: the ordinary stop; the app's own crash handler, which writes `crash-app.txt` and
|
||||
finishes the folder before the process dies; and a sweep at the next app start for any folder
|
||||
without its `.complete` marker (a native crash, a kill), which gets `ended-without-teardown.txt`
|
||||
@@ -58,9 +58,9 @@ The same user had to force-stop the app before every launch or nothing started:
|
||||
proot, gamescope and Steam outlive it, holding the rootfs, the GPU and Steam's lock. Every session
|
||||
start now kills whatever of ours is still running from before, and `app.log` lists it.
|
||||
|
||||
## 🎮 Steam menu from the drawer
|
||||
## Steam menu from the drawer
|
||||
|
||||
The in-session drawer has **Steam menu ◉** in Steam mode — the Guide button, for a device whose
|
||||
The in-session drawer has **Steam menu ◉** in Steam mode - the Guide button, for a device whose
|
||||
own home button belongs to Android and a pad the client hides the on-screen controls for. (There is
|
||||
no Quick Access chord on a non-Deck controller that I could confirm, so none is offered.)
|
||||
|
||||
@@ -73,32 +73,32 @@ restored and a competing client asked to stop · DirectAudio and the microphone
|
||||
core masks · four session fixes · a folder of logs per session with credentials scrubbed · a path
|
||||
for Adreno 710 / 720 / 722.
|
||||
|
||||
## ⚠️ If the client dies with nothing in its log
|
||||
## 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. Some phones have a **"restrict child processes"** switch in Developer options — turn it off.
|
||||
dozens. Some phones have a **"restrict child processes"** switch in Developer options - turn it off.
|
||||
Otherwise, once per device: `adb shell settings put global settings_enable_monitor_phantom_procs
|
||||
false`. `device.txt` in every session folder records what your phone reports.
|
||||
|
||||
## Credits
|
||||
|
||||
- **maxjivi05 (Max)** — the gamescope runtime this is built on, the sandbox shims, the controller
|
||||
- **maxjivi05 (Max)** - the gamescope runtime this is built on, the sandbox shims, the controller
|
||||
work and the NetworkManager stand-in, from WinNative.
|
||||
- **The412Banner** — the app, the Wayland compositor, the runtime plumbing, the Steam session and
|
||||
- **The412Banner** - the app, the Wayland compositor, the runtime plumbing, the Steam session and
|
||||
the File Manager (from Bannerlator).
|
||||
- **Winlator (brunodev85)** — the audio stack, the gamepad model and the shape of a session.
|
||||
- **Vauzi (Vauzi-17/710)** — the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
- **Winlator (brunodev85)** - the audio stack, the gamepad model and the shape of a session.
|
||||
- **Vauzi (Vauzi-17/710)** - the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
|
||||
Frame generation: **Win-FG** is built in; **LSFG** uses the shader chain from *your own copy* of
|
||||
Lossless Scaling, read on device and never redistributed, on groundwork from **lsfg-vk (PancakeTAS)**.
|
||||
|
||||
GPL-3.0. gamescope, Mesa, Turnip, Xwayland, labwc, LXQt, PulseAudio, proot and Arch Linux ARM are
|
||||
their own authors'. **Not affiliated with Valve** — Steam, Proton and Steam Deck are Valve's, and no
|
||||
their own authors'. **Not affiliated with Valve** - Steam, Proton and Steam Deck are Valve's, and no
|
||||
Valve software is redistributed here: the client is fetched from Valve's own manifest on your device.
|
||||
|
||||
---
|
||||
|
||||
**APK** `SteamDeck-0.1.4.apk` — 20957823 bytes
|
||||
**APK** `SteamDeck-0.1.4.apk` - 20957823 bytes
|
||||
`sha256 4b53ce6a937f2b51a5b9d2639ea32b816993e4f7b2dc33dd0f3198b81f21e9d2`
|
||||
versionCode 5, built from `8e58e8e` by [run 35682844106](https://github.com/The412Banner/SteamDeck/actions/runs/35682844106).
|
||||
Signed with the AOSP test key (v1 + v2 + v3), so it installs over 0.1.3 in place.
|
||||
+28
-28
@@ -6,10 +6,10 @@ hands.** Install it over 0.1.4; same key, same runtime, nothing to re-download.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 The front end
|
||||
## The front end
|
||||
|
||||
The main screen is a rail and a pane. The rail lists what can be launched — **Steam** and its
|
||||
installed games, **Desktop** and its emulators and *their* games — and **Setup** under them, every
|
||||
The main screen is a rail and a pane. The rail lists what can be launched - **Steam** and its
|
||||
installed games, **Desktop** and its emulators and *their* games - and **Setup** under them, every
|
||||
category folded when the app opens so the rail is three headers and their counts. The pane shows
|
||||
the chosen thing with its launch button, its art, and a shelf of the rest. A session left in the
|
||||
background is the first thing on the rail, with a pulse, and tapping it goes back in.
|
||||
@@ -18,16 +18,16 @@ It moves: the selection is one pill that glides between rows, lists unfold with
|
||||
staggering in, a page change cascades the new page in row by row over a blurred wash of its own
|
||||
art, tiles lift and shine under the pad, the launch button sweeps a sheen and squashes on press,
|
||||
and the session rises over the front end and sinks back. Every duration follows the system's
|
||||
animator scale, so "no animations" in developer options snaps all of it. ✅ Seen on a Pocket FIT
|
||||
animator scale, so "no animations" in developer options snaps all of it. Seen on a Pocket FIT
|
||||
and a Fold.
|
||||
|
||||
**Emulators launch from the rail, under gamescope, with their games.** The ROMs folder is scanned
|
||||
per system (one level of subfolders, PS3 game folders, RPCS3's own installed games by their
|
||||
PARAM.SFO with their icons), and each game launches straight in. ✅ God of War II HD booted in RPCS3
|
||||
from the rail, ~40 fps on the FIT — the first emulator game from the app. The desktop's own Steam
|
||||
launchers hand a game to the app's Steam session (⚠️ the hand-off itself is not yet proven).
|
||||
PARAM.SFO with their icons), and each game launches straight in. God of War II HD booted in RPCS3
|
||||
from the rail, ~40 fps on the FIT - the first emulator game from the app. The desktop's own Steam
|
||||
launchers hand a game to the app's Steam session ( the hand-off itself is not yet proven).
|
||||
|
||||
## 🎛️ Settings without a pop-up
|
||||
## Settings without a pop-up
|
||||
|
||||
The cog's window and the Performance dialog are pages in the pane: one row per setting, its value
|
||||
in a chip, and a small faded menu that opens right under the chip. One menu at a time; the page
|
||||
@@ -39,17 +39,17 @@ The **session drawer** is the same rows and menus, sliding in from the right: No
|
||||
generation, touch, the virtual pad, keyboard, the client's menu) and Next session (display shape,
|
||||
FEX preset, compatibility tools), with Send to background and Stop at the end.
|
||||
|
||||
## 🎨 The icon, and themes in its colours
|
||||
## The icon, and themes in its colours
|
||||
|
||||
A new launcher icon (Kurt's), and the same mark in the rail and on the loading screen. Three
|
||||
themes under Setup › Theme, in the icon's black, white and blue: **Paper on black** (the default —
|
||||
themes under Setup › Theme, in the icon's black, white and blue: **Paper on black** (the default -
|
||||
white is the primary, the blue is the small signal for rules, rings and dots), **Icon blue**, and
|
||||
**Electric navy**. A change applies at once.
|
||||
|
||||
## ⚡ Run as a game, and the client's frames back
|
||||
## Run as a game, and the client's frames back
|
||||
|
||||
- The app is declared a **game** to Android, with a game-mode config that welcomes Performance
|
||||
mode and refuses the OS-side frame-rate cap and resolution downscaling — the interventions that
|
||||
mode and refuses the OS-side frame-rate cap and resolution downscaling - the interventions that
|
||||
make a regular flagship's Big Picture crawl while a gaming phone's does not. The session window
|
||||
asks for **sustained performance mode**, the panel's **fastest display mode** at its size, tells
|
||||
GameManager it is in gameplay, and opens an **ADPF hint session** over the compositor thread fed
|
||||
@@ -58,7 +58,7 @@ white is the primary, the blue is the small signal for rules, rings and dots), *
|
||||
the app can read `/proc/bus/pci`, then dies on the devices file inside it, which Android denies;
|
||||
Chromium loads libpci in its GPU process, so after a few tries CEF drew the whole interface on
|
||||
the CPU. An empty file bound over that path at session start answers the scan truthfully.
|
||||
(WinNative, maxjivi05: 44 → 85 fps in the Big Picture library on a OnePlus 15.) ✅ Big Picture at
|
||||
(WinNative, maxjivi05: 44 → 85 fps in the Big Picture library on a OnePlus 15.) Big Picture at
|
||||
117–118 fps on both panels of a 120 Hz Fold, Adreno 830.
|
||||
- **Client interface** switches on the Performance page: threaded GL, lazy descriptors and no GL
|
||||
error checks, all on by default; **Steam Deck mode** (`-steamdeck -steamos3`) off by default,
|
||||
@@ -70,20 +70,20 @@ white is the primary, the blue is the small signal for rules, rings and dots), *
|
||||
- The guest gets the device's **time zone** (the client's clock and logs were UTC).
|
||||
- A device without an Adreno GPU is told **before** the runtime download that a session would be
|
||||
sound over a black screen.
|
||||
- The app's own libraries are **16 KB-aligned** (Android 15 devices). ⚠️ Six prebuilt PulseAudio
|
||||
- The app's own libraries are **16 KB-aligned** (Android 15 devices). Six prebuilt PulseAudio
|
||||
libraries are still 4 KB-only and need a rebuild.
|
||||
|
||||
## 🕹️ On-screen controls: sticks, and the bars
|
||||
## On-screen controls: sticks, and the bars
|
||||
|
||||
A **left and a right stick** beside the d-pad, a base and a knob that follows the finger, writing
|
||||
the pad's thumb axes the way a physical stick does. The controls **take the bars** beside or under
|
||||
the picture when the panel letterboxes it — a Fold's side bars, a square panel's bottom band —
|
||||
and only overlay the picture when there are none wide enough. ⚠️ Not yet tried in a game.
|
||||
the picture when the panel letterboxes it - a Fold's side bars, a square panel's bottom band -
|
||||
and only overlay the picture when there are none wide enough. Not yet tried in a game.
|
||||
|
||||
## 🤝 From Kurt (xXJSONDeruloXx)
|
||||
## From Kurt (xXJSONDeruloXx)
|
||||
|
||||
- The **hardware Back button** reaches the app and opens the drawer instead of going into Steam
|
||||
as Select. ✅ Verified on an AYN Thor.
|
||||
as Select. Verified on an AYN Thor.
|
||||
- The **launcher icon**.
|
||||
- **Local build and deploy helpers**: a Docker build for the preload libraries, a local release
|
||||
build with signing, an ADB install helper.
|
||||
@@ -92,7 +92,7 @@ Every pull request against main now builds by itself and gets one comment kept c
|
||||
or the conflicting hunks, and a failed build's compiler errors. The README lists the contributors
|
||||
and a ledger of their pull requests that a workflow keeps.
|
||||
|
||||
## 🔧 Smaller
|
||||
## Smaller
|
||||
|
||||
The file manager and the picker stay in landscape · a session's log folder goes with it when it
|
||||
ends and the next session gets its own · a replaced session's exit no longer ends the one that
|
||||
@@ -101,32 +101,32 @@ files are ignored.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Carried over — still open
|
||||
## Carried over - still open
|
||||
|
||||
The desktop → Steam hand-off and the sticks are untested on a device. A stick click is not yet
|
||||
offered. VAC-protected games report insecure. Games that ship only a Linux build won't start.
|
||||
|
||||
## Credits
|
||||
|
||||
- **maxjivi05 (Max)** — the gamescope runtime this is built on, the sandbox shims, the controller
|
||||
- **maxjivi05 (Max)** - the gamescope runtime this is built on, the sandbox shims, the controller
|
||||
work, the NetworkManager stand-in and the libpci fix, from WinNative.
|
||||
- **Kurt Himebauch (xXJSONDeruloXx)** — the launcher icon, the hardware Back fix, the local build
|
||||
- **Kurt Himebauch (xXJSONDeruloXx)** - the launcher icon, the hardware Back fix, the local build
|
||||
helpers.
|
||||
- **The412Banner** — the app, the Wayland compositor, the runtime plumbing, the Steam session, the
|
||||
- **The412Banner** - the app, the Wayland compositor, the runtime plumbing, the Steam session, the
|
||||
front end and the File Manager (from Bannerlator).
|
||||
- **Winlator (brunodev85)** — the audio stack, the gamepad model and the shape of a session.
|
||||
- **Vauzi (Vauzi-17/710)** — the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
- **Winlator (brunodev85)** - the audio stack, the gamepad model and the shape of a session.
|
||||
- **Vauzi (Vauzi-17/710)** - the Adreno 710/720/722 device entries the experimental driver carries.
|
||||
|
||||
Frame generation: **Win-FG** is built in; **LSFG** uses the shader chain from *your own copy* of
|
||||
Lossless Scaling, read on device and never redistributed, on groundwork from **lsfg-vk (PancakeTAS)**.
|
||||
|
||||
GPL-3.0. gamescope, Mesa, Turnip, Xwayland, labwc, LXQt, PulseAudio, proot and Arch Linux ARM are
|
||||
their own authors'. **Not affiliated with Valve** — Steam, Proton and Steam Deck are Valve's, and no
|
||||
their own authors'. **Not affiliated with Valve** - Steam, Proton and Steam Deck are Valve's, and no
|
||||
Valve software is redistributed here: the client is fetched from Valve's own manifest on your device.
|
||||
|
||||
---
|
||||
|
||||
**APK** `SteamDeck-0.1.5.apk` — 21093959 bytes
|
||||
**APK** `SteamDeck-0.1.5.apk` - 21093959 bytes
|
||||
`sha256 85b4295431ddea6f35025959864f3fbb47ad238693e146405ac0fe6ae87504f1`
|
||||
versionCode 6, built from `d2f4a84` by [run 35813465422](https://github.com/The412Banner/SteamDeck/actions/runs/35813465422).
|
||||
Signed with the AOSP test key (v1 + v2 + v3), so it installs over 0.1.4 in place.
|
||||
Reference in new issue
Block a user