mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
279
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76825f3d05 | ||
|
|
b89d87cbde | ||
|
|
06e06b9673 | ||
|
|
b896e7a3b9 | ||
|
|
c91daa9147 | ||
|
|
31ea194f3d | ||
|
|
472bb28c88 | ||
|
|
eaa4a7a047 | ||
|
|
4fa467c800 | ||
|
|
beb96e6f0c | ||
|
|
4628ae4c92 | ||
|
|
df9734f0ca | ||
|
|
26cb9e2f03 | ||
|
|
6dc74523c3 | ||
|
|
3858a69867 | ||
|
|
49a5f4aa94 | ||
|
|
4cc9f2a3f3 | ||
|
|
b0d7fc01d8 | ||
|
|
9cc40246a3 | ||
|
|
423cf11919 | ||
|
|
33f7213599 | ||
|
|
6f1fb462ed | ||
|
|
336b8851f7 | ||
|
|
5da3cf673c | ||
|
|
9e54e094fb | ||
|
|
eb9ca4ab6b | ||
|
|
e7b6876877 | ||
|
|
c535f6ef6e | ||
|
|
db2e1d7da2 | ||
|
|
f5e204eb1d | ||
|
|
705a5e7b2a | ||
|
|
e60eae3646 | ||
|
|
b04900b1b2 | ||
|
|
6357543b44 | ||
|
|
67fb338e5d | ||
|
|
ba140c4ab5 | ||
|
|
f5fdbc7941 | ||
|
|
90ae85da4d | ||
|
|
f3600fa618 | ||
|
|
2d8befe1cb | ||
|
|
f982ffcaf7 | ||
|
|
255c64551e | ||
|
|
746f28c1b2 | ||
|
|
2b481ba250 | ||
|
|
49622623de | ||
|
|
69f99ade42 | ||
|
|
c236e9711f | ||
|
|
4c2efb6688 | ||
|
|
7eb2d1cf1a | ||
|
|
b4ff83cf9f | ||
|
|
7b20b70c6b | ||
|
|
86207f9d0e | ||
|
|
eda5ece382 | ||
|
|
3404bbc128 | ||
|
|
b5a431aa3c | ||
|
|
49249f6541 | ||
|
|
5614621d6e | ||
|
|
0816562065 | ||
|
|
39618e6862 | ||
|
|
c66e4ba4b3 | ||
|
|
e4b033861f | ||
|
|
85a07a556c | ||
|
|
8dfc48d58b | ||
|
|
82ebdaa0b5 | ||
|
|
ec016f85bc | ||
|
|
c2c812deaa | ||
|
|
eec3428f78 | ||
|
|
4a5e4b76e3 | ||
|
|
43f952b2fb | ||
|
|
0d86bec34d | ||
|
|
47a6f29ad7 | ||
|
|
84834f180e | ||
|
|
bfd445618a | ||
|
|
eddfd37b93 | ||
|
|
d7341863bf | ||
|
|
2f51be7b5a | ||
|
|
fbc0014d14 | ||
|
|
46df6171fc | ||
|
|
761b62914f | ||
|
|
91b0299f77 | ||
|
|
6d08e59d10 | ||
|
|
83cc0fe585 | ||
|
|
c711c627b8 | ||
|
|
0b34690721 | ||
|
|
b640734db5 | ||
|
|
f8feb63dde | ||
|
|
25a5d3fe5d | ||
|
|
41dd93fd25 | ||
|
|
adf3c9405d | ||
|
|
abf4f57b47 | ||
|
|
c1e36975d4 | ||
|
|
0bfa0915d4 | ||
|
|
4da87672a2 | ||
|
|
3ba0d287e9 | ||
|
|
b4a86cb5bb | ||
|
|
0fa1f2f868 | ||
|
|
71610ddfdb | ||
|
|
5c9e683769 | ||
|
|
32a5e83b88 | ||
|
|
88904c08f7 | ||
|
|
9928bfe780 | ||
|
|
a491dec5c4 | ||
|
|
17ef33ee31 | ||
|
|
06caaee343 | ||
|
|
1c161a76dd | ||
|
|
aa05ab03fa | ||
|
|
43ea3f3181 | ||
|
|
3966b27948 | ||
|
|
7e24327621 | ||
|
|
d025aa3c6d | ||
|
|
325f6893c5 | ||
|
|
466419407d | ||
|
|
cbd4ce6636 | ||
|
|
d4130181e7 | ||
|
|
16a9d0af2a | ||
|
|
e51698e01f | ||
|
|
519091dd86 | ||
|
|
972088f1b3 | ||
|
|
542d31daf0 | ||
|
|
8f9a849c48 | ||
|
|
e8bfe6e520 | ||
|
|
15618f357b | ||
|
|
b74010435a | ||
|
|
0a58a7faee | ||
|
|
0d21e2f913 | ||
|
|
ebb82d2dc9 | ||
|
|
557e346810 | ||
|
|
08b2cf0894 | ||
|
|
4e90e03536 | ||
|
|
11d997dc65 | ||
|
|
454193da9b | ||
|
|
cdd5c99bc7 | ||
|
|
fb1da1c39a | ||
|
|
b29ece82c8 | ||
|
|
1330fe4d6c | ||
|
|
0e1f6f15a1 | ||
|
|
4df0e012fd | ||
|
|
48054a86ab | ||
|
|
38d658abf5 | ||
|
|
5547391cfc | ||
|
|
6782e5a717 | ||
|
|
e1a80c1751 | ||
|
|
05933a5ee3 | ||
|
|
1f29cd92a5 | ||
|
|
4f30356c9b | ||
|
|
baebf50197 | ||
|
|
01674b7c6f | ||
|
|
728bfa2043 | ||
|
|
c4de482b6a | ||
|
|
55697535a1 | ||
|
|
65733c23a1 | ||
|
|
fd51b86060 | ||
|
|
d6e4ab1824 | ||
|
|
566c7d703a | ||
|
|
d7eb3256a5 | ||
|
|
c96f2d1070 | ||
|
|
cc8b743ecb | ||
|
|
45422b739b | ||
|
|
f58bf9b23c | ||
|
|
f0e8ef1274 | ||
|
|
bb6d6fee97 | ||
|
|
d59734e375 | ||
|
|
14db12290e | ||
|
|
1e476539df | ||
|
|
3c8e370744 | ||
|
|
3fcd01f536 | ||
|
|
0e2ee35b49 | ||
|
|
27920711f0 | ||
|
|
d4c16b5b59 | ||
|
|
e392364e30 | ||
|
|
3cdb77fd65 | ||
|
|
a91be68992 | ||
|
|
e16f351540 | ||
|
|
efdab60b22 | ||
|
|
8dc24f244c | ||
|
|
b565c4a6e5 | ||
|
|
a999fbdb38 | ||
|
|
52eee568f5 | ||
|
|
05cebd4a4f | ||
|
|
8cfd54a739 | ||
|
|
8565acf12b | ||
|
|
941a6c3ef9 | ||
|
|
fe84c050ad | ||
|
|
f13c885fec | ||
|
|
8ee1cbe3eb | ||
|
|
6c5d6cda73 | ||
|
|
2725ee4512 | ||
|
|
7cc1c93775 | ||
|
|
38c9b733b5 | ||
|
|
27e5f86793 | ||
|
|
a6331e44ae | ||
|
|
f53b844add | ||
|
|
fb34098006 | ||
|
|
c729f4c8e7 | ||
|
|
9f343278c3 | ||
|
|
6beeb29486 | ||
|
|
ade2e982b6 | ||
|
|
8a61a1898b | ||
|
|
dfa3b12f48 | ||
|
|
93382ff241 | ||
|
|
24b74d8346 | ||
|
|
9ed25d8ae5 | ||
|
|
b4b31d5a08 | ||
|
|
f394906726 | ||
|
|
b50dfa76d1 | ||
|
|
3f81ddff9b | ||
|
|
d1dec8d7a4 | ||
|
|
e60097e5cb | ||
|
|
7c7bf2dcbe | ||
|
|
75593cfd29 | ||
|
|
00d0f8cd2b | ||
|
|
faf4b4e8b5 | ||
|
|
5a61d37c5d | ||
|
|
25079ab819 | ||
|
|
736b7bed5e | ||
|
|
a3f4c911e5 | ||
|
|
caa7682c6f | ||
|
|
e217cbe66d | ||
|
|
4914b2cc69 | ||
|
|
051b9a320e | ||
|
|
6ecd6fa1d0 | ||
|
|
0f4e83eefd | ||
|
|
d62525f5af | ||
|
|
71057fefe1 | ||
|
|
f4d2c37fbd | ||
|
|
ffd9f92d7b | ||
|
|
213d110766 | ||
|
|
1a8c91b49b | ||
|
|
9644a4cc7f | ||
|
|
24c250fdb4 | ||
|
|
d4636afa97 | ||
|
|
32930e704e | ||
|
|
a8b18947a1 | ||
|
|
7c422577b6 | ||
|
|
1cc1ee994d | ||
|
|
d8d375cb9a | ||
|
|
49934ce933 | ||
|
|
221644e8be | ||
|
|
5179b54745 | ||
|
|
448feb30bc | ||
|
|
9d891d4db1 | ||
|
|
240f1194c0 | ||
|
|
7b74eebce2 | ||
|
|
7592684b6f | ||
|
|
7b78e1629b | ||
|
|
30741302fb | ||
|
|
645f7d0bf1 | ||
|
|
124f162ee8 | ||
|
|
3c577fd9dd | ||
|
|
659ff4817f | ||
|
|
626adb62af | ||
|
|
71cb100a40 | ||
|
|
68002f3fa7 | ||
|
|
dae7592788 | ||
|
|
e10f5b4430 | ||
|
|
3e32b75a63 | ||
|
|
435f7a1b03 | ||
|
|
1fd8c50225 | ||
|
|
e797bc5f01 | ||
|
|
0cc3fe6bca | ||
|
|
ab8542e4e5 | ||
|
|
c2bcf20172 | ||
|
|
2341659cea | ||
|
|
b2eaf380dc | ||
|
|
197fc3f163 | ||
|
|
9530e7b394 | ||
|
|
0e0f133496 | ||
|
|
d98c211168 | ||
|
|
614b4d4de4 | ||
|
|
43a19a831a | ||
|
|
0d308af024 | ||
|
|
128f73da8f | ||
|
|
67cb005e6c | ||
|
|
73f76f6799 | ||
|
|
961a060461 | ||
|
|
dadbbf841a | ||
|
|
1ce8280371 | ||
|
|
78bdc0e3b5 | ||
|
|
ce5f2f5513 |
No files matched your search
@@ -42,4 +42,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release create "${{ inputs.tag }}" --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber
|
||||
gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber
|
||||
# A component the apk fetches by tag, never something users install: it must not
|
||||
# become the repo's Latest release or appear as a pre-release, also when re-uploaded.
|
||||
gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false
|
||||
@@ -43,4 +43,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release create "${{ inputs.tag }}" --title "${{ inputs.tag }}" --notes "wlroots 0.20.2 with tools/wlroots/patches (Vulkan DMA-BUF allocator for non-DRM GPU nodes); staged by the apk at /usr/local/lib/droiddeck-wlroots for the desktop's GPU renderers. sha256 $(cut -d' ' -f1 wlroots.tzst.sha256)" wlroots.tzst wlroots.tzst.sha256 || gh release upload "${{ inputs.tag }}" wlroots.tzst wlroots.tzst.sha256 --clobber
|
||||
gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "wlroots 0.20.2 with tools/wlroots/patches (Vulkan DMA-BUF allocator for non-DRM GPU nodes); staged by the apk at /usr/local/lib/droiddeck-wlroots for the desktop's GPU renderers. sha256 $(cut -d' ' -f1 wlroots.tzst.sha256)" wlroots.tzst wlroots.tzst.sha256 || gh release upload "${{ inputs.tag }}" wlroots.tzst wlroots.tzst.sha256 --clobber
|
||||
# A component the apk fetches by tag, never something users install: it must not
|
||||
# become the repo's Latest release or appear as a pre-release, also when re-uploaded.
|
||||
gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false
|
||||
+232
-11
@@ -22,11 +22,13 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
# Replace older builds only for the same PR. Each push/dispatch gets its own group so one main
|
||||
# build cannot cancel another before it signs and publishes.
|
||||
group: build-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
PRELOAD_CACHE_VERSION: v2
|
||||
PRELOAD_CACHE_VERSION: v3
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -34,12 +36,18 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
variants: ${{ steps.variants.outputs.list }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: The packages a signed build ships as
|
||||
id: variants
|
||||
run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: No private key in the tree
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -140,6 +148,8 @@ jobs:
|
||||
label="${BUILD_REF} at ${BUILD_SHA:0:7}"
|
||||
fi
|
||||
echo "label=$label" >> "$GITHUB_OUTPUT"
|
||||
echo "DROIDDECK_SOURCE_COMMIT=${PR_HEAD_SHA:-$BUILD_SHA}" >> "$GITHUB_ENV"
|
||||
echo "DROIDDECK_PR_NUMBER=${PR_NUMBER:-0}" >> "$GITHUB_ENV"
|
||||
echo "Build: \`$label\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Cache the preloads
|
||||
@@ -147,7 +157,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: app/src/main/assets/linuxfs
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/mangoapp/*') }}
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*') }}
|
||||
|
||||
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
@@ -166,7 +176,13 @@ jobs:
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libblsession.so" tools/linuxfs/preload/*.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libblsession.so"
|
||||
for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libblfastpath.so"
|
||||
mkdir -p "$D/usr/local/bin"
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard"
|
||||
for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do
|
||||
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
|
||||
done
|
||||
for f in tools/linuxfs/overlay/usr/bin/* tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/*; do
|
||||
@@ -188,6 +204,12 @@ jobs:
|
||||
test -f "$D/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so"
|
||||
rm wlroots.tzst
|
||||
fi
|
||||
. tools/linuxfs/uruntime.env
|
||||
curl -fsSL --retry 3 -o uruntime "https://github.com/VHSgunzo/uruntime/releases/download/${URUNTIME_VERSION}/${URUNTIME_ASSET}"
|
||||
echo "${URUNTIME_SHA256} uruntime" | sha256sum -c -
|
||||
install -Dm644 uruntime "$D/usr/local/lib/droiddeck/uruntime"
|
||||
install -Dm644 tools/linuxfs/licenses/uruntime-LICENSE "$D/usr/local/share/licenses/uruntime/LICENSE"
|
||||
rm uruntime
|
||||
M="$D/usr/local/lib/mangoapp"
|
||||
mkdir -p "$M" mango-pkgs
|
||||
grep -v '^#' tools/mangoapp/packages.txt | while read -r sum url; do
|
||||
@@ -198,9 +220,13 @@ jobs:
|
||||
rm mango-pkgs/pkg.tar.zst
|
||||
done
|
||||
install -m644 mango-pkgs/usr/bin/mangoapp "$M/mangoapp"
|
||||
for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1 libtracefs.so.1; do
|
||||
for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do
|
||||
cp -L "mango-pkgs/usr/lib/$so" "$M/$so"
|
||||
done
|
||||
# Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c).
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \
|
||||
-o "$M/libtracefs.so.1" tools/mangoapp/libtracefs-shim.c
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$M/libtracefs.so.1"
|
||||
install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp"
|
||||
rm -rf mango-pkgs
|
||||
ls -l "$M"
|
||||
@@ -213,6 +239,46 @@ jobs:
|
||||
install -Dm644 tools/linuxfs/desktop/firefox-droiddeck.js "$D/usr/lib/firefox/defaults/pref/droiddeck.js"
|
||||
ls -lR "$D"
|
||||
|
||||
- name: Build the x86 preloads FEX guests load (glibc 2.31)
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm -v "$PWD:/src" -w /src debian:bullseye bash -c '
|
||||
set -euo pipefail
|
||||
printf "deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian-security bullseye-security main\n" > /etc/apt/sources.list
|
||||
apt-get -o Acquire::Check-Valid-Until=false update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends gcc g++ gcc-multilib g++-multilib binutils >/dev/null
|
||||
tools/linuxfs/build-x86-preloads.sh app/src/main/assets/linuxfs
|
||||
'
|
||||
sudo chown -R "$(id -u):$(id -g)" app/src/main/assets/linuxfs
|
||||
ls -l app/src/main/assets/linuxfs/x86_64 app/src/main/assets/linuxfs/i386
|
||||
|
||||
- name: Bundle the droiddeck-esync packs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -f tools/droiddeck-esync/release.env ]; then
|
||||
. tools/droiddeck-esync/release.env
|
||||
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p "$SYNC_BUNDLE_ASSET" -O sync-bundle.tzst
|
||||
echo "$SYNC_BUNDLE_SHA256 sync-bundle.tzst" | sha256sum -c -
|
||||
mkdir -p app/src/main/assets/droiddeck-esync
|
||||
tar --use-compress-program=unzstd -xf sync-bundle.tzst -C app/src/main/assets/droiddeck-esync
|
||||
rm sync-bundle.tzst
|
||||
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p index.json -O sync-index.json
|
||||
revoked=$(python3 -c 'import json, sys; print(" ".join(p["id"] for p in json.load(open(sys.argv[1]))["packs"] if p.get("revoked") is True))' sync-index.json)
|
||||
for id in $revoked; do
|
||||
if [ -e "app/src/main/assets/droiddeck-esync/packs/$id.tzst" ]; then
|
||||
echo "::warning::$SYNC_BUNDLE_ASSET carries revoked pack $id; it is left out of the APK (point tools/droiddeck-esync/release.env at a newer bundle)"
|
||||
rm -f "app/src/main/assets/droiddeck-esync/packs/$id.tzst"
|
||||
fi
|
||||
done
|
||||
rm sync-index.json
|
||||
test -f app/src/main/assets/droiddeck-esync/index.json
|
||||
test -f app/src/main/assets/droiddeck-esync/index.json.sig
|
||||
ls -lR app/src/main/assets/droiddeck-esync
|
||||
fi
|
||||
|
||||
- name: Cache PulseAudio 13.0 source
|
||||
id: pa-src
|
||||
uses: actions/cache@v4
|
||||
@@ -277,11 +343,11 @@ jobs:
|
||||
echo "::error::libfakeinput.so exports the C++ runtime"; exit 1
|
||||
fi
|
||||
SESSION=$(syms "$D/libblsession.so")
|
||||
for sym in socket bind getsockname setsockopt statfs statvfs; do
|
||||
for sym in socket bind getsockname setsockopt statfs statvfs syscall shm_open shm_unlink mmap mmap64 munmap; do
|
||||
grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; }
|
||||
done
|
||||
test -f "$D/usr/local/bin/bannerlator-session"
|
||||
test -f "$D/usr/local/bin/bannerlator-proton-extra"
|
||||
test -f "$D/usr/local/bin/droiddeck-session"
|
||||
test -f "$D/usr/local/bin/droiddeck-proton-extra"
|
||||
|
||||
- name: Cache the CMake build tree
|
||||
uses: actions/cache@v4
|
||||
@@ -300,6 +366,10 @@ jobs:
|
||||
run: |
|
||||
set -o pipefail
|
||||
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
|
||||
python3 tools/release/check_session_assets.py app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
- name: Steam import and game environment tests
|
||||
run: python3 -m unittest discover -s tools/tests -p 'test_*.py'
|
||||
|
||||
- name: Unit tests
|
||||
run: ./gradlew testReleaseUnitTest --console=plain -PndkVersion="$NDK_VERSION"
|
||||
@@ -361,6 +431,7 @@ jobs:
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
BUILD_OUTCOME: ${{ steps.build.outcome }}
|
||||
SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
MARK="<!-- droiddeck-pr-build -->"
|
||||
@@ -374,7 +445,15 @@ jobs:
|
||||
success)
|
||||
echo "### Build passed"
|
||||
echo
|
||||
echo "The \`droiddeck-apk\` artifact is [on this run]($RUN_URL), signed with the public test key. After approval, a maintainer can run **PR Release Publish** from \`main\` to create an APK that installs over the release."
|
||||
echo "The \`droiddeck-apk\` artifact is [on this run]($RUN_URL), signed with the public test key."
|
||||
echo
|
||||
if [ "$SAME_REPO" = true ]; then
|
||||
echo "A release-signed tester build that installs over Stable or Preview is being published now and will appear in DroidDeck's **Test builds** list. Label the PR \`no-test-build\` to skip that."
|
||||
else
|
||||
echo "**Maintainer:** add the \`publish-test\` label to publish a release-signed tester build that installs over Stable or Preview and appears in DroidDeck's **Test builds** list. The label is removed automatically after publishing."
|
||||
fi
|
||||
echo
|
||||
echo "Fallback: [open **PR Release Publish**]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/workflows/pr-release-publish.yml) on \`main\` and enter PR \`$PR\`."
|
||||
;;
|
||||
failure)
|
||||
echo "### ERROR: Build failed"
|
||||
@@ -427,5 +506,147 @@ jobs:
|
||||
uses: ./.github/workflows/sign.yml
|
||||
with:
|
||||
environment: signing-main
|
||||
variants: '["standard"]'
|
||||
variants: ${{ needs.build.outputs.variants }}
|
||||
secrets: inherit
|
||||
|
||||
# Each signed main build as a prerelease of the public Droid-Deck/DroidDeck-CI (main-<sha>, the
|
||||
# last KEEP kept), where anyone can download it without a GitHub account, then a post in the
|
||||
# Discord's builds channel linking to it. The release is made with a token of the org's
|
||||
# "DroidDeck CI Publisher" app (installed on DroidDeck-CI only, Contents: write): its ID in the
|
||||
# DROIDDECK_CI_APP_ID variable, its private key in the DROIDDECK_CI_APP_KEY secret.
|
||||
# DISCORD_BUILDS_WEBHOOK is the channel's webhook URL.
|
||||
announce:
|
||||
needs: [build, sign]
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
env:
|
||||
CI_REPO: Droid-Deck/DroidDeck-CI
|
||||
KEEP: 10
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: droiddeck-signed-*
|
||||
merge-multiple: true
|
||||
path: signed
|
||||
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
|
||||
# What this build is, for the top of the release notes: the merged PR's title and the first
|
||||
# paragraph of its description, or the commit's subject for a push made without a PR.
|
||||
- name: Describe the change
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pr=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \
|
||||
--jq '[.[] | select(.merged_at != null and .base.ref == "main")] | first // empty')
|
||||
if [ -n "$pr" ]; then
|
||||
number=$(jq -r .number <<<"$pr")
|
||||
title=$(jq -r .title <<<"$pr")
|
||||
merged=$(jq -r '.merged_at[:10]' <<<"$pr")
|
||||
summary=$(jq -r '.body // ""' <<<"$pr" | tr -d '\r' | { grep -v '^🤖' || true; } |
|
||||
awk '!p && /^#/ { next } NF { p = 1; print; next } p { exit }' | head -n 6 | cut -c1-400)
|
||||
{
|
||||
echo "**Merged to main** - [PR #$number]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$number): $title (merged $merged)"
|
||||
if [ -n "$summary" ]; then
|
||||
echo
|
||||
echo "$summary"
|
||||
fi
|
||||
} > notes-head.md
|
||||
else
|
||||
subject=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA" --jq '.commit.message | split("\n")[0]')
|
||||
echo "**Pushed to main** without a PR - $subject" > notes-head.md
|
||||
fi
|
||||
cat notes-head.md
|
||||
|
||||
- name: Publish to DroidDeck-CI
|
||||
id: publish
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag=main-${GITHUB_SHA::7}
|
||||
cat signed/SHA256SUMS-*.txt > signed/SHA256SUMS.txt
|
||||
rm signed/SHA256SUMS-*.txt
|
||||
notes="$(cat notes-head.md)
|
||||
|
||||
Signed build of $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA - in main, not yet in a DroidDeck release; for testing.
|
||||
Commit: $GITHUB_SHA
|
||||
VersionCode: $(sed -n 's/^ *versionCode \([0-9][0-9]*\)$/\1/p' app/build.gradle)
|
||||
Each apk installs over a DroidDeck release and keeps its data. \`standard\` is the normal app; the others are the same app under another package name, installed beside it.
|
||||
|
||||
SHA-256:
|
||||
\`\`\`
|
||||
$(cat signed/SHA256SUMS.txt)
|
||||
\`\`\`"
|
||||
gh release delete "$tag" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null || true
|
||||
gh release create "$tag" -R "$CI_REPO" --target main --prerelease \
|
||||
--title "main ${GITHUB_SHA::7}" --notes "$notes" signed/*
|
||||
# Keep the newest main builds by publication time. GitHub App releases can share createdAt.
|
||||
gh release list -R "$CI_REPO" -L 200 --json tagName,publishedAt \
|
||||
--jq '[.[] | select(.tagName | startswith("main-"))] | sort_by(.publishedAt) | reverse | .['"$KEEP"':][] | .tagName' |
|
||||
while IFS= read -r old; do
|
||||
# Never remove this run's release, even if the API returns an unexpected ordering.
|
||||
if [ "$old" = "$tag" ]; then
|
||||
continue
|
||||
fi
|
||||
# Another main publish may have removed the same old release concurrently.
|
||||
if ! gh release delete "$old" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null; then
|
||||
if gh release view "$old" -R "$CI_REPO" >/dev/null 2>&1; then
|
||||
echo "::error::Could not delete old release $old"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
published=$(gh release view "$tag" -R "$CI_REPO" --json tagName,assets)
|
||||
jq -e --arg tag "$tag" '.tagName == $tag and (.assets | length) > 0' <<< "$published" >/dev/null
|
||||
url="$GITHUB_SERVER_URL/$CI_REPO/releases/tag/$tag"
|
||||
apk=$(cd signed && ls DroidDeck-*.apk | awk 'length(a) == 0 || length($0) < length(a) { a = $0 } END { print a }')
|
||||
echo "release=$url" >> "$GITHUB_OUTPUT"
|
||||
echo "apk=$GITHUB_SERVER_URL/$CI_REPO/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
|
||||
echo "Published $url"
|
||||
|
||||
- name: Post to Discord
|
||||
env:
|
||||
WEBHOOK: ${{ secrets.DISCORD_BUILDS_WEBHOOK }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE: ${{ steps.publish.outputs.release }}
|
||||
APK: ${{ steps.publish.outputs.apk }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$WEBHOOK" ]; then
|
||||
echo "::notice::No DISCORD_BUILDS_WEBHOOK secret; nothing posted"
|
||||
exit 0
|
||||
fi
|
||||
commit=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA")
|
||||
subject=$(jq -r '.commit.message | split("\n")[0]' <<<"$commit")
|
||||
# A merge commit's subject is "Merge pull request #N from ..."; its body's first line is
|
||||
# the PR's title, which says more.
|
||||
title=$(jq -r '.commit.message | split("\n") | map(select(length > 0)) | .[1] // ""' <<<"$commit")
|
||||
pr=$(sed -n 's/^Merge pull request #\([0-9]*\).*/\1/p' <<<"$subject")
|
||||
if [ -n "$pr" ]; then
|
||||
what="[#$pr]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$pr): ${title:-$subject}"
|
||||
else
|
||||
what="[\`${GITHUB_SHA::7}\`]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA): $subject"
|
||||
fi
|
||||
get=$(printf 'Download: [standard APK](<%s>) · [all variants](<%s>)' "$APK" "$RELEASE")
|
||||
content=$(printf '**New signed build** `main-%s`\n%s\n%s' "${GITHUB_SHA::7}" "$what" "$get")
|
||||
jq -n --arg content "$content" '{content: $content, allowed_mentions: {parse: []}}' |
|
||||
curl -fsS -H 'Content-Type: application/json' -d @- "$WEBHOOK" -o /dev/null
|
||||
echo "Posted: $content"
|
||||
|
||||
catalog:
|
||||
needs: announce
|
||||
if: ${{ always() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && needs.announce.result != 'skipped' }}
|
||||
uses: ./.github/workflows/update-catalog.yml
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,49 @@
|
||||
name: PR Release Cleanup
|
||||
run-name: remove PR #${{ github.event.pull_request.number }}'s test build
|
||||
|
||||
# A merged or closed PR's test build leaves DroidDeck-CI, so the app's Updates page stops offering it
|
||||
# (a merged fix is in the next Preview). pull_request_target for the publisher app's secrets; nothing
|
||||
# from the PR is checked out or run.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ closed ]
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
- name: Delete the test build
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
found=0
|
||||
for tag in "pr-$PR_NUMBER" "pr-$PR_NUMBER.0"; do
|
||||
if gh release view "$tag" -R Droid-Deck/DroidDeck-CI >/dev/null 2>&1; then
|
||||
gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
|
||||
echo "Deleted $tag"
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
[ "$found" = 1 ] || echo "No test build for PR #$PR_NUMBER"
|
||||
|
||||
catalog:
|
||||
needs: cleanup
|
||||
if: ${{ always() }}
|
||||
uses: ./.github/workflows/update-catalog.yml
|
||||
secrets: inherit
|
||||
@@ -1,99 +1,159 @@
|
||||
name: PR Release Publish
|
||||
run-name: sign PR #${{ inputs.pr_number }} with the release key
|
||||
run-name: "sign PR #${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }} with the release key"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: Open, approved pull request
|
||||
description: Open pull request number
|
||||
required: true
|
||||
type: number
|
||||
type: string
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
branches: [main]
|
||||
# A PR from a branch of this repo is signed on its own after each green build: only people who
|
||||
# can push here can change it. A fork's PR waits for a maintainer's publish-test label.
|
||||
workflow_run:
|
||||
workflows: [Build APK]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: pr-release-${{ inputs.pr_number }}
|
||||
group: pr-release-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
if: >-
|
||||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'pull_request_target' && github.event.label.name == 'publish-test') ||
|
||||
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_repository.full_name == github.repository &&
|
||||
github.event.workflow_run.pull_requests[0] != null)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
pr_number: ${{ steps.gate.outputs.pr_number }}
|
||||
run_id: ${{ steps.gate.outputs.run_id }}
|
||||
head_sha: ${{ steps.gate.outputs.head_sha }}
|
||||
base_sha: ${{ steps.gate.outputs.base_sha }}
|
||||
version_code: ${{ steps.gate.outputs.version_code }}
|
||||
label: ${{ steps.gate.outputs.label }}
|
||||
skip: ${{ steps.gate.outputs.skip }}
|
||||
automatic: ${{ steps.gate.outputs.automatic }}
|
||||
variants: ${{ steps.variants.outputs.list }}
|
||||
steps:
|
||||
- name: Check the PR, review, and build
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Every variant a main build ships, so a PR build can be tried under any of them.
|
||||
- name: The packages a signed build ships as
|
||||
id: variants
|
||||
run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT"
|
||||
- name: Check the requester, PR, and build
|
||||
id: gate
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
REQUESTER: ${{ github.actor }}
|
||||
PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
|
||||
BUILT_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number '$PR_NUMBER'"; exit 1; }
|
||||
# The automatic path stands down quietly, never failing: a draft, a PR labelled
|
||||
# no-test-build, or a build that a later push has already overtaken.
|
||||
skip() { echo "::notice::Not publishing PR #$PR_NUMBER: $1"; echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
|
||||
if [ "$EVENT_NAME" = workflow_run ]; then
|
||||
echo "automatic=true" >> "$GITHUB_OUTPUT"
|
||||
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
||||
[ "$(jq -r .state <<<"$pr")" = open ] || skip "it is not open"
|
||||
[ "$(jq -r .draft <<<"$pr")" = false ] || skip "it is a draft"
|
||||
[ "$(jq -r .base.ref <<<"$pr")" = main ] || skip "it does not target main"
|
||||
[ "$(jq -r .head.repo.full_name <<<"$pr")" = "$GITHUB_REPOSITORY" ] || skip "it comes from a fork"
|
||||
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$pr" >/dev/null && skip "it is labelled no-test-build"
|
||||
[ "$(jq -r .head.sha <<<"$pr")" = "$BUILT_SHA" ] || skip "a newer push is building"
|
||||
fi
|
||||
if [ "$EVENT_NAME" = pull_request_target ]; then
|
||||
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$REQUESTER/permission" --jq .permission)
|
||||
case "$permission" in
|
||||
admin|maintain|write) ;;
|
||||
*) echo "::error::@$REQUESTER does not have permission to publish release-signed test builds"; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
||||
jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null
|
||||
jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null ||
|
||||
{ echo "::error::PR #$PR_NUMBER must be open, ready for review, and target main"; exit 1; }
|
||||
head=$(jq -r '.head.sha' <<<"$pr")
|
||||
base=$(jq -r '.base.sha' <<<"$pr")
|
||||
|
||||
run_id=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \
|
||||
--event pull_request --commit "$head" \
|
||||
--json databaseId,headSha,status,conclusion,createdAt,attempt -L 100 |
|
||||
jq -r --arg head "$head" '[.[] | select(.headSha == $head)] |
|
||||
sort_by([.createdAt, .attempt]) | last |
|
||||
select(.status == "completed" and .conclusion == "success") | .databaseId // empty')
|
||||
[[ -n "$run_id" ]] || { echo '::error::The latest PR build is not green'; exit 1; }
|
||||
# The label is often added while the PR's build is still running, or just after a push
|
||||
# before it has started: wait for the latest build of the head commit to finish (a PR
|
||||
# build takes about 4 minutes) rather than fail and need the label added again.
|
||||
run_id=
|
||||
for attempt in $(seq 1 60); do
|
||||
latest=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \
|
||||
--event pull_request --commit "$head" \
|
||||
--json databaseId,headSha,status,conclusion,createdAt,attempt -L 100 |
|
||||
jq -c --arg head "$head" '[.[] | select(.headSha == $head)] |
|
||||
sort_by([.createdAt, .attempt]) | last // empty')
|
||||
if [[ -n "$latest" && $(jq -r .status <<<"$latest") == completed ]]; then
|
||||
conclusion=$(jq -r .conclusion <<<"$latest")
|
||||
[[ "$conclusion" == success ]] || { echo "::error::The latest PR build is not green ($conclusion)"; exit 1; }
|
||||
run_id=$(jq -r .databaseId <<<"$latest")
|
||||
break
|
||||
fi
|
||||
status=not-started
|
||||
[[ -z "$latest" ]] || status=$(jq -r .status <<<"$latest")
|
||||
echo "Waiting for the PR build of ${head:0:7} to finish ($status)"
|
||||
sleep 30
|
||||
done
|
||||
[[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; }
|
||||
# GitHub leaves a run's pull_requests empty when its head is in a fork, so the build is
|
||||
# tied to this PR by its head repository, branch and commit instead.
|
||||
run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id")
|
||||
jq -e --arg head "$head" --arg base "$base" --argjson number "$PR_NUMBER" '
|
||||
jq -e --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" '
|
||||
.event == "pull_request" and .head_sha == $head and .conclusion == "success" and
|
||||
any(.pull_requests[]?; .number == $number and .head.sha == $head and .base.sha == $base)
|
||||
' <<<"$run" >/dev/null
|
||||
|
||||
approved=$(gh api --paginate --slurp \
|
||||
"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews?per_page=100" |
|
||||
jq -r --arg head "$head" '[.[][] | select(.commit_id == $head)] |
|
||||
group_by(.user.login) | map(max_by(.id)) | .[] |
|
||||
select(.state == "APPROVED") | .user.login')
|
||||
maintainer_approved=false
|
||||
while IFS= read -r reviewer; do
|
||||
[[ -n "$reviewer" ]] || continue
|
||||
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$reviewer/permission" --jq .permission)
|
||||
[[ "$permission" == write || "$permission" == admin ]] && maintainer_approved=true
|
||||
done <<<"$approved"
|
||||
[[ "$maintainer_approved" == true ]] || { echo '::error::A maintainer must approve this commit'; exit 1; }
|
||||
.head_repository.full_name == $repo and .head_branch == $ref
|
||||
' <<<"$run" >/dev/null ||
|
||||
{ echo "::error::Build run $run_id is not a green build of PR #$PR_NUMBER at ${head:0:7}"; exit 1; }
|
||||
|
||||
sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}
|
||||
tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1)
|
||||
stable_tag=$(gh api repos/The412Banner/DroidDeck/releases/latest --jq .tag_name)
|
||||
mkdir -p "$RUNNER_TEMP/stable" "$RUNNER_TEMP/pr"
|
||||
gh release download "$stable_tag" -R The412Banner/DroidDeck \
|
||||
-p "DroidDeck-$stable_tag.apk" -D "$RUNNER_TEMP/stable"
|
||||
# A PR build is made from the PR merged onto main, so it must install over main's own
|
||||
# build. main is what this job checked out; no release has to exist or be marked latest.
|
||||
main_code=$(sed -n 's/^[[:space:]]*versionCode[[:space:]][[:space:]]*\([0-9][0-9]*\)[[:space:]]*$/\1/p' app/build.gradle)
|
||||
[[ "$main_code" =~ ^[0-9]+$ ]] || { echo "::error::No numeric versionCode in main's app/build.gradle"; exit 1; }
|
||||
mkdir -p "$RUNNER_TEMP/pr"
|
||||
gh run download "$run_id" -R "$GITHUB_REPOSITORY" -n droiddeck-apk -D "$RUNNER_TEMP/pr"
|
||||
mapfile -t apks < <(find "$RUNNER_TEMP/pr" -type f -name '*.apk')
|
||||
[[ ${#apks[@]} == 1 ]] || { echo '::error::Expected one PR APK'; exit 1; }
|
||||
package_line() {
|
||||
"$tools/aapt" dump badging "$1" | sed -n "s/^package: name='\\([^']*\\)' versionCode='\\([^']*\\)'.*/\\1 \\2/p"
|
||||
}
|
||||
stable=$(package_line "$RUNNER_TEMP/stable/DroidDeck-$stable_tag.apk")
|
||||
candidate=$(package_line "${apks[0]}")
|
||||
[[ "$stable" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && "$candidate" == "$stable" ]] || {
|
||||
echo "::error::PR package/versionCode '$candidate' differs from stable '$stable'"; exit 1;
|
||||
[[ "$candidate" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && ${candidate##* } -ge $main_code ]] || {
|
||||
echo "::error::PR package/versionCode '$candidate' cannot install over main (versionCode $main_code)"; exit 1;
|
||||
}
|
||||
|
||||
current=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
||||
jq -e --arg head "$head" --arg base "$base" '
|
||||
.state == "open" and .draft == false and .base.ref == "main" and
|
||||
.head.sha == $head and .base.sha == $base
|
||||
' <<<"$current" >/dev/null
|
||||
[[ $(gh pr view "$PR_NUMBER" -R "$GITHUB_REPOSITORY" --json reviewDecision --jq .reviewDecision) == APPROVED ]] || {
|
||||
echo '::error::The current PR revision still needs approval'; exit 1;
|
||||
}
|
||||
' <<<"$current" >/dev/null ||
|
||||
{ echo "::error::PR #$PR_NUMBER moved on while its build was checked; publish again"; exit 1; }
|
||||
{
|
||||
echo "pr_number=$PR_NUMBER"
|
||||
echo "run_id=$run_id"
|
||||
echo "head_sha=$head"
|
||||
echo "base_sha=$base"
|
||||
echo "version_code=${candidate##* }"
|
||||
echo "label=PR.$PR_NUMBER-${head:0:7}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -102,11 +162,12 @@ jobs:
|
||||
uses: ./.github/workflows/sign.yml
|
||||
with:
|
||||
environment: signing-main
|
||||
variants: '["standard"]'
|
||||
variants: ${{ needs.authorize.outputs.variants }}
|
||||
run_id: ${{ needs.authorize.outputs.run_id }}
|
||||
label: ${{ needs.authorize.outputs.label }}
|
||||
artifact: droiddeck-pr-signed
|
||||
secrets: inherit
|
||||
if: ${{ needs.authorize.outputs.skip != 'true' }}
|
||||
|
||||
publish:
|
||||
needs: [authorize, sign]
|
||||
@@ -114,37 +175,174 @@ jobs:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: droiddeck-pr-signed-standard
|
||||
pattern: droiddeck-pr-signed-*
|
||||
merge-multiple: true
|
||||
path: signed
|
||||
- name: Publish to DroidDeck-CI when configured
|
||||
# What this build is, for the top of the release notes: the PR's title and the first
|
||||
# paragraph of its description.
|
||||
- name: Describe the change
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.DROIDDECK_CI_TOKEN }}
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
||||
title=$(jq -r .title <<<"$pr")
|
||||
summary=$(jq -r '.body // ""' <<<"$pr" | tr -d '\r' | { grep -v '^🤖' || true; } |
|
||||
awk '!p && /^#/ { next } NF { p = 1; print; next } p { exit }' | head -n 6 | cut -c1-400)
|
||||
{
|
||||
echo "**Not merged** - a test build of open [PR #$PR_NUMBER]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$PR_NUMBER): $title"
|
||||
if [ -n "$summary" ]; then
|
||||
echo
|
||||
echo "$summary"
|
||||
fi
|
||||
} > notes-head.md
|
||||
cat notes-head.md
|
||||
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
- name: Publish to DroidDeck-CI
|
||||
id: publish
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
|
||||
LABEL: ${{ needs.authorize.outputs.label }}
|
||||
BASE_SHA: ${{ needs.authorize.outputs.base_sha }}
|
||||
VERSION_CODE: ${{ needs.authorize.outputs.version_code }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "$GH_TOKEN" ]]; then
|
||||
echo '::warning::No CI token; download the signed APK from this run'
|
||||
exit 0
|
||||
fi
|
||||
repo=Droid-Deck/DroidDeck-CI
|
||||
tag="pr-$PR_NUMBER"
|
||||
apk="DroidDeck-$LABEL.apk"
|
||||
sha=$(sha256sum "signed/$apk" | cut -d ' ' -f 1)
|
||||
notes="Source: https://github.com/$GITHUB_REPOSITORY/pull/$PR_NUMBER
|
||||
cat signed/SHA256SUMS-*.txt > signed/SHA256SUMS.txt
|
||||
rm signed/SHA256SUMS-*.txt
|
||||
notes="$(cat notes-head.md)
|
||||
|
||||
Source: https://github.com/$GITHUB_REPOSITORY/pull/$PR_NUMBER
|
||||
Commit: $HEAD_SHA
|
||||
APK SHA-256: $sha"
|
||||
Base: $BASE_SHA
|
||||
VersionCode: $VERSION_CODE
|
||||
Each apk installs over a DroidDeck release and keeps its data. \`standard\` is the normal app; the others are the same app under another package name, installed beside it.
|
||||
|
||||
SHA-256:
|
||||
\`\`\`
|
||||
$(cat signed/SHA256SUMS.txt)
|
||||
\`\`\`"
|
||||
first=false
|
||||
if ! gh release view "$tag" -R "$repo" >/dev/null 2>&1; then
|
||||
gh release create "$tag" -R "$repo" --target main --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
|
||||
first=true
|
||||
fi
|
||||
gh release upload "$tag" "signed/$apk" -R "$repo" --clobber
|
||||
echo "first=$first" >> "$GITHUB_OUTPUT"
|
||||
gh release upload "$tag" signed/* -R "$repo" --clobber
|
||||
gh release edit "$tag" -R "$repo" --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
|
||||
# Drop the previous build's files: anything this build did not just upload.
|
||||
keep=$(cd signed && ls | jq -R . | jq -sc .)
|
||||
gh api "repos/$repo/releases/tags/$tag" |
|
||||
jq -r --arg keep "$apk" '.assets[] | select(.name | endswith(".apk")) | select(.name != $keep) | .id' |
|
||||
jq -r --argjson keep "$keep" '.assets[] | select(.name as $n | $keep | index($n) | not) | .id' |
|
||||
while IFS= read -r id; do
|
||||
gh api --method DELETE "repos/$repo/releases/assets/$id" >/dev/null
|
||||
done
|
||||
|
||||
published=$(gh release view "$tag" -R "$repo" --json tagName,assets)
|
||||
jq -e --arg tag "$tag" '.tagName == $tag and (.assets | length) > 0' <<<"$published" >/dev/null
|
||||
release="$GITHUB_SERVER_URL/$repo/releases/tag/$tag"
|
||||
apk=$(cd signed && ls DroidDeck-*.apk | awk 'length(a) == 0 || length($0) < length(a) { a = $0 } END { print a }')
|
||||
echo "release=$release" >> "$GITHUB_OUTPUT"
|
||||
echo "apk=$GITHUB_SERVER_URL/$repo/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Whether the PR's branch is in this repo
|
||||
id: same-repo
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
run: |
|
||||
head_repo=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .head.repo.full_name)
|
||||
echo "same=$([ "$head_repo" = "$GITHUB_REPOSITORY" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Clear the publish request label
|
||||
if: ${{ github.event_name == 'pull_request_target' && steps.publish.outcome == 'success' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
run: |
|
||||
gh api --method DELETE "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels/publish-test"
|
||||
|
||||
- name: Report the signed build on the PR
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
|
||||
RELEASE: ${{ steps.publish.outputs.release }}
|
||||
APK: ${{ steps.publish.outputs.apk }}
|
||||
SAME_REPO: ${{ steps.same-repo.outputs.same }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
short=${HEAD_SHA:0:7}
|
||||
mark='<!-- droiddeck-pr-release -->'
|
||||
if [ "$SAME_REPO" = true ]; then
|
||||
NEXT='Each green build of a new push is signed and published here on its own. Label the PR `no-test-build` to stop that.'
|
||||
else
|
||||
NEXT='Pushes to this PR do not re-sign automatically. After a new green build, a maintainer can add the `publish-test` label again.'
|
||||
fi
|
||||
body=$(cat <<EOF
|
||||
$mark
|
||||
## Signed test build
|
||||
|
||||
A release-signed build of this PR at \`$short\` is ready. It installs over Stable or Preview and keeps app data.
|
||||
|
||||
[Download the standard APK]($APK) · [All variants and checksums]($RELEASE)
|
||||
|
||||
It is also available in **DroidDeck → Updates → Test builds → PR #$PR_NUMBER**.
|
||||
|
||||
$NEXT
|
||||
EOF
|
||||
)
|
||||
id=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate \
|
||||
--jq ".[] | select(.body | startswith(\"$mark\")) | .id" | head -1 || true)
|
||||
if [ -n "$id" ]; then
|
||||
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -f body="$body" >/dev/null
|
||||
else
|
||||
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null
|
||||
fi
|
||||
|
||||
# Asked for (the label, a manual run): always announced. Signed on its own after a push:
|
||||
# only a PR's first test build is, and later ones update its release quietly.
|
||||
- name: Post to Discord
|
||||
if: ${{ always() && steps.publish.outcome == 'success' && (needs.authorize.outputs.automatic != 'true' || steps.publish.outputs.first == 'true') }}
|
||||
env:
|
||||
WEBHOOK: ${{ secrets.DISCORD_BUILDS_WEBHOOK }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
|
||||
RELEASE: ${{ steps.publish.outputs.release }}
|
||||
APK: ${{ steps.publish.outputs.apk }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$WEBHOOK" ]; then
|
||||
echo "::notice::No DISCORD_BUILDS_WEBHOOK secret; nothing posted"
|
||||
exit 0
|
||||
fi
|
||||
short=${HEAD_SHA:0:7}
|
||||
title=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .title)
|
||||
what="[#$PR_NUMBER]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$PR_NUMBER): $title"
|
||||
get=$(printf 'Download: [standard APK](<%s>) · [all variants](<%s>)' "$APK" "$RELEASE")
|
||||
content=$(printf '**New signed PR test build** \`pr-%s\` · \`%s\`\n%s\n_Not merged — for testing._\n%s' "$PR_NUMBER" "$short" "$what" "$get")
|
||||
jq -n --arg content "$content" '{content: $content, allowed_mentions: {parse: []}}' |
|
||||
curl -fsS -H 'Content-Type: application/json' -d @- "$WEBHOOK" -o /dev/null
|
||||
echo "Posted: $content"
|
||||
|
||||
catalog:
|
||||
needs: publish
|
||||
if: ${{ always() && needs.publish.result != 'skipped' }}
|
||||
uses: ./.github/workflows/update-catalog.yml
|
||||
secrets: inherit
|
||||
@@ -35,6 +35,10 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ $VERSION =~ ^[0-9A-Za-z._-]+$ ]] || { echo "::error::version '$VERSION': letters, digits, . _ - only"; exit 1; }
|
||||
name=$(sed -n 's/^ *versionName "\(.*\)"$/\1/p' app/build.gradle)
|
||||
[[ $VERSION == "$name" || $VERSION == "$name"-* ]] || {
|
||||
echo "::error::version '$VERSION' does not match versionName '$name' in app/build.gradle: bump versionName and versionCode first"; exit 1;
|
||||
}
|
||||
runs=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml --branch main --commit "$GITHUB_SHA" \
|
||||
--status success --json databaseId,event,headSha -L 20)
|
||||
id=$(jq -r --arg sha "$GITHUB_SHA" \
|
||||
@@ -58,3 +62,59 @@ jobs:
|
||||
label: ${{ inputs.version }}
|
||||
artifact: droiddeck-release
|
||||
secrets: inherit
|
||||
|
||||
# The release is made here, from the apks this run signed, so it traces back to its Build APK run.
|
||||
publish:
|
||||
name: publish the release
|
||||
needs: [find, sign]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: droiddeck-release-*
|
||||
merge-multiple: true
|
||||
path: out
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
BUILD_RUN: ${{ needs.find.outputs.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
notes="docs/releases/$VERSION.md"
|
||||
[ -f "$notes" ] || { echo "::error::$notes is missing: add the release notes first"; exit 1; }
|
||||
if gh release view "$VERSION" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
echo "::error::release $VERSION already exists"; exit 1
|
||||
fi
|
||||
# The notes' first line is the title; media paths become raw URLs at this release's tag.
|
||||
title=$(sed -n '1s/^# *//p' "$notes")
|
||||
[ -n "$title" ] || title="DroidDeck $VERSION"
|
||||
raw="https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$VERSION/docs/releases/media/"
|
||||
{
|
||||
sed '1{/^# /d;}' "$notes" | sed -E "s#(\]\(|src=\")media/#\1$raw#g"
|
||||
echo
|
||||
echo "---"
|
||||
echo
|
||||
echo "Built by [Build APK run $BUILD_RUN]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN) from \`$GITHUB_SHA\`, signed and published by [Release APKs run $GITHUB_RUN_ID]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
|
||||
} > "$RUNNER_TEMP/body.md"
|
||||
( cd out && cat SHA256SUMS-*.txt | sort -k2 > SHA256SUMS.txt && rm SHA256SUMS-*.txt && cat SHA256SUMS.txt )
|
||||
gh release create "$VERSION" -R "$GITHUB_REPOSITORY" out/*.apk out/SHA256SUMS.txt \
|
||||
--target "$GITHUB_SHA" --title "$title" --notes-file "$RUNNER_TEMP/body.md" --latest
|
||||
echo "Published [$title]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$VERSION)" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# A release made with this run's token fires no release event, so the in-app catalog is refreshed here.
|
||||
catalog:
|
||||
needs: publish
|
||||
uses: ./.github/workflows/update-catalog.yml
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Update app catalog
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published, edited, deleted, unpublished]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: update-app-catalog
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
catalog:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# Always use the current trusted generator, even when a release event points at an older tag.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
|
||||
- name: Regenerate catalog.json
|
||||
env:
|
||||
SOURCE_GITHUB_TOKEN: ${{ github.token }}
|
||||
CI_GITHUB_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 tools/release/update_catalog.py
|
||||
@@ -12,7 +12,9 @@ captures/
|
||||
# copies cannot drift.
|
||||
app/src/main/assets/linuxfs/
|
||||
app/src/main/jniLibs/arm64-v8a/libproot.so
|
||||
app/src/main/jniLibs/arm64-v8a/.proot-inputs
|
||||
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
|
||||
app/src/main/assets/droiddeck-esync/
|
||||
|
||||
# Kotlin compiler session files (build-tool state, never source).
|
||||
.kotlin/
|
||||
@@ -29,3 +31,6 @@ app/src/main/jniLibs/arm64-v8a/libproot-loader.so
|
||||
droiddeck-release-key/
|
||||
password.txt
|
||||
__pycache__/
|
||||
|
||||
# Local signing with the release key (tools/build_local.sh); holds a password.
|
||||
.signing.env
|
||||
@@ -7,13 +7,21 @@
|
||||
|
||||
DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big Picture on your Adreno handheld, with Windows games through Valve's ARM64 Proton.
|
||||
|
||||
<p align="center"><a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a></p>
|
||||
|
||||
<p align="center"><img src="docs/releases/media/0.2.0/launch-into-steam.gif" width="80%" alt="Tapping DroidDeck on the Android home screen and landing in Steam Big Picture"></p>
|
||||
|
||||
> Note: DroidDeck does not have a stand-alone website. Do not click on any download links from websites claiming to be the DroidDeck team.
|
||||
|
||||
## Requirements and install
|
||||
|
||||
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/The412Banner/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators.
|
||||
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/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts.
|
||||
|
||||
On Android 12+, if Steam exits without a log, turn off **Restrict child processes** in Developer options.
|
||||
Before Steam launches, you must turn off **Restrict child processes** in Developer options. If this option is not available in developer settings (Android 12 and 13 devices), first launch of Steam will present a "Fix it for me" button, which will help automate the setup process.
|
||||
|
||||
## Community
|
||||
|
||||
Join the [DroidDeck Discord](https://discord.gg/JRGAvawjsm) for help, Preview builds, and device reports. Bug reports go in its **#bug-reports** forum; attach the session folder from `Download/DroidDeck/` so the logs come with it.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -25,4 +33,4 @@ Compatibility and performance vary by device; hardware validation is limited. De
|
||||
|
||||
## Credits and licence
|
||||
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
+36
-2
@@ -49,6 +49,22 @@ if (!(treeState in ['clean', 'dirty'])) {
|
||||
buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}"
|
||||
def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"')
|
||||
|
||||
// What the in-app updater compares against the published builds: the commit this build is of (a PR
|
||||
// build's head, not the merge CI makes of it), its PR, and whether CI made it at all.
|
||||
def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull()
|
||||
?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: ''
|
||||
def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' }
|
||||
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
|
||||
// The release key's certificate: a build signed with it, from CI or a computer, updates in place.
|
||||
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
|
||||
// When the source was committed, for "built 3 hours ago": fixed per commit, so it costs no rebuilds.
|
||||
def commitTime = (gitValue(['show', '-s', '--format=%ct', 'HEAD']) ?: '0').with { it.isLong() ? it : '0' }
|
||||
// tools/release/variants.txt as "package=suffix;...", so the updater picks the apk of the package it runs as.
|
||||
def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines()
|
||||
.findAll { it.trim() && !it.startsWith('#') }
|
||||
.collect { def f = it.trim().split(/\s+/); "${f[1]}=${f[2] == '-' ? '' : f[2]}" }
|
||||
.join(';')
|
||||
|
||||
android {
|
||||
namespace 'com.droiddeck.launcher'
|
||||
compileSdk 34
|
||||
@@ -63,9 +79,15 @@ android {
|
||||
// file out of its own data directory, and this app's entire job is to exec a rootfs:
|
||||
// proot, gamescope, the Steam client and everything it launches all live under files/.
|
||||
targetSdkVersion 28
|
||||
versionCode 8
|
||||
versionName "0.1.7"
|
||||
versionCode 11
|
||||
versionName "0.3.1"
|
||||
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
|
||||
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
|
||||
buildConfigField "int", "PR_NUMBER", "${prNumber}"
|
||||
buildConfigField "boolean", "CI_BUILD", "${ciBuild}"
|
||||
buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\""
|
||||
buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L"
|
||||
buildConfigField "String", "VARIANT_SUFFIXES", "\"${variantSuffixes}\""
|
||||
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a'
|
||||
@@ -154,6 +176,16 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// Session scripts must follow this checkout even when the native asset bundle is cached.
|
||||
// A stale local bundle can omit a newly required helper on a fresh runtime install.
|
||||
def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
|
||||
from(rootProject.file('tools/linuxfs/overlay')) {
|
||||
include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**'
|
||||
}
|
||||
into(layout.projectDirectory.dir('src/main/assets/linuxfs'))
|
||||
}
|
||||
tasks.named('preBuild').configure { dependsOn(stageSessionScripts) }
|
||||
|
||||
dependencies {
|
||||
// The runtime tarball is a zstd-compressed tar.
|
||||
implementation 'com.github.luben:zstd-jni:1.5.2-3@aar'
|
||||
@@ -168,6 +200,7 @@ dependencies {
|
||||
// copies on. Same versions as Bannerlator so the screen stays a straight copy.
|
||||
implementation 'androidx.compose.material:material-icons-extended'
|
||||
implementation 'io.coil-kt:coil-compose:2.6.0'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
// ADB-over-Wi-Fi pairing and authenticated shell client. Users still approve pairing from
|
||||
// Android's Wireless debugging screen; DroidDeck only runs its one fixed setting command.
|
||||
@@ -176,4 +209,5 @@ dependencies {
|
||||
implementation 'com.termux.termux-app:terminal-view:0.118.0'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||
}
|
||||
@@ -73,6 +73,7 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
.put("steamUi", SessionState.steamUi ?: JSONObject.NULL)
|
||||
.put("steamUrl", SessionState.steamUrl ?: JSONObject.NULL)
|
||||
.put("suspended", SessionState.suspended)
|
||||
.put("pip", SessionState.pipActive)
|
||||
.put("firstFrame", SessionState.firstFrameSeen)
|
||||
.put("output", JSONArray().put(SessionState.outputSize.first).put(SessionState.outputSize.second))
|
||||
.put("refreshHz", SessionState.refreshHz.toDouble())
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
download from being throttled to nothing by the radio's power-save. -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<!-- Optional Wi-Fi discovery: requested only from Steam settings after an explanation. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- Ends background processes of another app that holds the Steam login. GameHub starts
|
||||
itself with the phone via boot receivers and signs this client out seconds after every
|
||||
@@ -22,12 +26,15 @@
|
||||
relay helper opens the input stream under this uid, and PulseAudio exposes it to the
|
||||
Steam client as DirectAudioMic. -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<!-- Installs DroidDeck's own updates (Updates page) over itself, after the user allows it. -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -54,6 +61,16 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="droiddeck" android:host="game" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Android owns the Home key. This small translucent Home handler routes the resulting
|
||||
@@ -109,7 +126,8 @@
|
||||
<activity
|
||||
android:name=".SessionActivity"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTop"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/SessionTheme"
|
||||
android:preferMinimalPostProcessing="true"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
@@ -130,6 +148,9 @@
|
||||
<service
|
||||
android:name=".runtime.RuntimeInstallService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".core.WirelessAdbPairingService"
|
||||
android:exported="false" />
|
||||
<!-- Hands a zipped session folder to the share sheet; nothing else is exposed. -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_alpha.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_beta.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_chain.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_common.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_delta.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// See lsfg_dll.h. PE resource walk + on-device SPIR-V cache for the Lossless
|
||||
// Scaling frame-generation chain.
|
||||
//
|
||||
// Derived from WinNative's lsfg_dll.c (GPL-3.0-or-later), LSFG port credited to
|
||||
// Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk
|
||||
// (PancakeTAS). Bannerlator is GPL-3.0.
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck
|
||||
// by @maxjivi05. GPL-3.0-or-later.
|
||||
|
||||
#include "lsfg_dll.h"
|
||||
#include "lsfg_dxbc.h"
|
||||
@@ -478,20 +478,6 @@ DllStatus validateDll(const std::string& dllPath) {
|
||||
return status;
|
||||
}
|
||||
|
||||
Variant dllVariant(const std::string& dllPath, bool preferFp16) {
|
||||
if (dllPath.empty()) return Variant::None;
|
||||
|
||||
PeImage img;
|
||||
if (!img.open(dllPath)) return Variant::None;
|
||||
|
||||
ResourceTable table;
|
||||
if (parseResources(img, table) != DllStatus::Ok) return Variant::None;
|
||||
|
||||
Variant v = selectVariant(table, preferFp16);
|
||||
if (v == Variant::None && hasBaseChain(table)) v = Variant::DxbcTranslated;
|
||||
return v;
|
||||
}
|
||||
|
||||
DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, bool preferFp16) {
|
||||
if (dllPath.empty() || cachePath.empty()) return DllStatus::NotInstalled;
|
||||
|
||||
@@ -547,22 +533,6 @@ DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, b
|
||||
return DllStatus::Ok;
|
||||
}
|
||||
|
||||
DllStatus cacheMatchesSource(const std::string& cachePath, const std::string& dllPath,
|
||||
bool& outMatches) {
|
||||
outMatches = false;
|
||||
if (cachePath.empty() || dllPath.empty()) return DllStatus::CacheUnusable;
|
||||
|
||||
CacheHeader header{};
|
||||
if (!readCacheHeader(cachePath, header)) return DllStatus::CacheUnusable;
|
||||
|
||||
PeImage img;
|
||||
if (!img.open(dllPath)) return DllStatus::NotInstalled;
|
||||
|
||||
outMatches = header.sourceSize == (uint64_t)img.size()
|
||||
&& header.sourceHash == fnv1a64(img.data(), img.size());
|
||||
return DllStatus::Ok;
|
||||
}
|
||||
|
||||
DllStatus cacheVariant(const std::string& cachePath, Variant& outVariant) {
|
||||
outVariant = Variant::None;
|
||||
CacheHeader header{};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// ship as RCDATA resources inside Lossless.dll from the user's own paid copy
|
||||
// of Lossless Scaling (Steam app 993090). Every implementation - upstream
|
||||
// lsfg-vk, LSFG-Android, eden, WinNative - requires the user to supply their
|
||||
// own DLL and extracts the resources on device. Bannerlator does the same, and
|
||||
// own DLL and extracts the resources on device. DroidDeck does the same, and
|
||||
// bundles nothing.
|
||||
//
|
||||
// The DLL is mmap'd READ-ONLY and parsed as data. It is never loaded as a
|
||||
@@ -26,9 +26,9 @@
|
||||
// Both producers fill the same shader-id -> SPIR-V words map, so everything
|
||||
// downstream of this file is source-agnostic.
|
||||
//
|
||||
// Derived from WinNative's lsfg_dll.c (GPL-3.0-or-later), whose LSFG port is
|
||||
// credited to Camille LaVey / the Eden Emulator Project, and which follows
|
||||
// upstream lsfg-vk (PancakeTAS). Bannerlator is GPL-3.0; see THIRD-PARTY-LICENSES.md.
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck
|
||||
// by @maxjivi05. GPL-3.0-or-later.
|
||||
// ============================================================================
|
||||
|
||||
#include <cstdint>
|
||||
@@ -98,18 +98,11 @@ const char* variantName(Variant v);
|
||||
// reported as valid - the translator handles it.
|
||||
DllStatus validateDll(const std::string& dllPath);
|
||||
|
||||
// Which producer would be used for this DLL.
|
||||
Variant dllVariant(const std::string& dllPath, bool preferFp16);
|
||||
|
||||
// Parse the DLL, produce all 25 SPIR-V modules, and write them to cachePath
|
||||
// (via temp file + rename, so a failed build cannot leave a half-written
|
||||
// cache behind). Records which producer ran in the cache header.
|
||||
DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, bool preferFp16);
|
||||
|
||||
// Is the cache current for this DLL? Compares source size + content hash.
|
||||
DllStatus cacheMatchesSource(const std::string& cachePath, const std::string& dllPath,
|
||||
bool& outMatches);
|
||||
|
||||
// Load a previously built cache.
|
||||
DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// See lsfg_dxbc.h. DXBC -> SPIR-V via DXVK's vendored translator.
|
||||
//
|
||||
// Follows upstream lsfg-vk's src/extract/trans.cpp, including its
|
||||
// encounter-order binding renumber. Derived from WinNative's lsfg_dxbc.cpp
|
||||
// (GPL-3.0-or-later); the translator itself is DXVK's, zlib licensed, vendored
|
||||
// under cpp/thirdparty/dxbc.
|
||||
// encounter-order binding renumber. Ported to WinNative (lsfg_dxbc.cpp) and
|
||||
// DroidDeck by @maxjivi05 from the work of Camille LaVey / the Eden Emulator
|
||||
// Project (GPL-3.0-or-later); the translator itself is DXVK's, zlib licensed,
|
||||
// vendored under cpp/thirdparty/dxbc.
|
||||
|
||||
#include "lsfg_dxbc.h"
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// See lsfg_engine.h.
|
||||
//
|
||||
// Ported from WinNative's vkr_lsfg.cpp (GPL-3.0-or-later), LSFG port credited
|
||||
// to Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk.
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by
|
||||
// @maxjivi05, where it runs on gamescope's output in the compositor.
|
||||
|
||||
#include "lsfg_engine.h"
|
||||
|
||||
@@ -269,7 +270,7 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32
|
||||
// nothing. Copy only when generating, on the one priming frame that
|
||||
// precedes generation, or while the governor is warming up towards a
|
||||
// probe - so the ring is populated by the time it is needed.
|
||||
const bool needHistory = generations > 0 || primeHistory_
|
||||
const bool needHistory = generations > 0 || primeHistory_ || followEveryFrame()
|
||||
|| (governorEnabled_ && governor_.wantsHistory());
|
||||
if (needHistory) {
|
||||
copyPresentedFrame(cmd, source, chain_->Input(count), VkExtent2D{width, height});
|
||||
@@ -284,7 +285,7 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32
|
||||
// regardless of the game's own settings, and it fed straight back into the
|
||||
// governor: the chain made the source rate collapse, the governor saw the
|
||||
// collapse and refused to generate, and refusing did not stop the chain.
|
||||
if (warm_ && generations > 0) chain_->DispatchShared(cmd, count);
|
||||
if (warm_ && (generations > 0 || followEveryFrame())) chain_->DispatchShared(cmd, count);
|
||||
}
|
||||
|
||||
void Engine::generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t targetIndex,
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
// unavoidable cost of interpolating rather than extrapolating; no placement of
|
||||
// this code changes it.
|
||||
//
|
||||
// Ported from WinNative's vkr_lsfg.cpp (GPL-3.0-or-later), LSFG port credited
|
||||
// to Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk.
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by
|
||||
// @maxjivi05, where it runs on gamescope's output in the compositor.
|
||||
// ============================================================================
|
||||
|
||||
#include <cstdint>
|
||||
@@ -109,6 +110,10 @@ private:
|
||||
// generation last stopped.
|
||||
bool historyFresh() const { return haveCopied_ && lastCopiedCount_ + 1 == frameCount_; }
|
||||
|
||||
// With a target rate the pacer plans 0..3 generations frame by frame, so the ring and the
|
||||
// shared chain have to follow every frame it keeps warm, not just the ones that generate.
|
||||
bool followEveryFrame() const { return warm_ && pacer_.Config().target_rate != 0; }
|
||||
|
||||
Device device_{};
|
||||
std::string cachePath_;
|
||||
std::unique_ptr<LsfgShaders> shaders_;
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_gamma.hpp"
|
||||
#include "lsfg_shaders.hpp"
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_generate.hpp"
|
||||
#include "lsfg_dll.h"
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -27,12 +27,6 @@ Java_com_droiddeck_launcher_gpu_LsfgNative_nativeValidateDll(JNIEnv* env, jclass
|
||||
return (jint)lsfg::validateDll(toStdString(env, dllPath));
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeDllVariant(JNIEnv* env, jclass, jstring dllPath,
|
||||
jboolean preferFp16) {
|
||||
return (jint)lsfg::dllVariant(toStdString(env, dllPath), preferFp16 == JNI_TRUE);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeBuildCache(JNIEnv* env, jclass, jstring dllPath,
|
||||
jstring cachePath, jboolean preferFp16) {
|
||||
@@ -40,16 +34,6 @@ Java_com_droiddeck_launcher_gpu_LsfgNative_nativeBuildCache(JNIEnv* env, jclass,
|
||||
preferFp16 == JNI_TRUE);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeCacheMatchesSource(JNIEnv* env, jclass,
|
||||
jstring cachePath,
|
||||
jstring dllPath) {
|
||||
bool matches = false;
|
||||
const lsfg::DllStatus status = lsfg::cacheMatchesSource(toStdString(env, cachePath),
|
||||
toStdString(env, dllPath), matches);
|
||||
return (jboolean)(status == lsfg::DllStatus::Ok && matches);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_gpu_LsfgNative_nativeCacheVariant(JNIEnv* env, jclass, jstring cachePath) {
|
||||
lsfg::Variant variant = lsfg::Variant::None;
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_mipmaps.hpp"
|
||||
#include "lsfg_dll.h"
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_pacer.hpp"
|
||||
|
||||
@@ -22,6 +21,8 @@ constexpr float SOURCE_SMOOTHING = 0.15f;
|
||||
constexpr float SOURCE_STALE_SECONDS = 0.5f;
|
||||
constexpr float DISCONTINUITY_SECONDS = 0.25f;
|
||||
constexpr float HEADROOM_EPSILON = 0.02f;
|
||||
// A target is aimed just under, so rounding never puts output past the panel's refresh rate.
|
||||
constexpr float TARGET_MARGIN = 0.1f;
|
||||
constexpr float CREDIT_EPSILON = 1.0e-4f;
|
||||
constexpr float SOURCE_ACCUM_FLOOR = 0.01f;
|
||||
constexpr uint32_t MIN_RATE_SAMPLES = 12;
|
||||
@@ -117,6 +118,7 @@ LsfgPlan LsfgPacer::Plan(size_t capacity, uint64_t source_frames) {
|
||||
if (target_rate > 0.0f && config.refresh_rate > 0.0f) {
|
||||
target_rate = std::min(target_rate, config.refresh_rate);
|
||||
}
|
||||
if (target_rate > 0.0f) target_rate -= TARGET_MARGIN;
|
||||
|
||||
if (target_rate == 0.0f) {
|
||||
// No explicit target rate: the multiplier is the multiplier, full stop.
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#include "lsfg_shaders.hpp"
|
||||
#include "lsfg_common.hpp"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Ported into Bannerlator's compositor from WinNative (GPL-3.0-or-later),
|
||||
// whose LSFG port is credited to Camille LaVey / the Eden Emulator Project and
|
||||
// follows upstream lsfg-vk. Only the Vulkan dispatch differs: Bannerlator
|
||||
// resolves entry points through the renderer's own table (see lsfg_vkd.h).
|
||||
// LSFG from the work of Camille LaVey / the Eden Emulator Project, following
|
||||
// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the
|
||||
// Vulkan dispatch differs here (see lsfg_vkd.h).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// ============================================================================
|
||||
// lsfg_vkd - the Vulkan entry points the LSFG chain calls, as a single global.
|
||||
//
|
||||
// Two dispatch styles meet here. Bannerlator's compositor resolves Vulkan
|
||||
// Two dispatch styles meet here. DroidDeck's compositor resolves Vulkan
|
||||
// through a per-context VkTable (`vk_`), because an adrenotools-loaded driver
|
||||
// lives in an isolated linker namespace and shares no global symbols with the
|
||||
// system loader. The LSFG chain, ported from eden by way of WinNative, calls
|
||||
// system loader. The LSFG chain, ported from Eden by way of WinNative, calls
|
||||
// through a file-scope `vkd`. Rather than rewrite ~2,300 lines of proven chain
|
||||
// code to thread a table through every call, this shim exposes exactly the 34
|
||||
// entry points the chain uses and is filled from the renderer's own table when
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# generated/ (no host wayland-scanner needed at app build time). Built by the app
|
||||
# (add_subdirectory from ../CMakeLists.txt) and on its own by standalone/ for
|
||||
# build-waylandcomp.yml.
|
||||
add_library(bannerwayland SHARED
|
||||
add_library(droiddeckwayland SHARED
|
||||
src/compositor.c
|
||||
src/vk_present.c
|
||||
src/effects_chain.c
|
||||
@@ -12,13 +12,14 @@ add_library(bannerwayland SHARED
|
||||
src/sc_layer.c
|
||||
src/ahb_swapchain.c
|
||||
src/waylandcomp_jni.c
|
||||
src/banner_ext.c
|
||||
src/droiddeck_ext.c
|
||||
src/wl_dmabuf.c
|
||||
src/wl_clipboard.c
|
||||
src/wl_text_input.c
|
||||
src/wl_toplevel_icon.c
|
||||
src/wl_color_mgmt.c
|
||||
src/hdr_compose.c
|
||||
src/blend_pass.c
|
||||
generated/color-management-v1-protocol.c
|
||||
generated/xdg-shell-protocol.c
|
||||
generated/linux-dmabuf-v1-protocol.c
|
||||
@@ -35,9 +36,9 @@ add_library(bannerwayland SHARED
|
||||
src/framegen_jni.c
|
||||
)
|
||||
# Warnings on for the compositor's own C. They do not fail the build; they are there to be read.
|
||||
target_compile_options(bannerwayland PRIVATE
|
||||
target_compile_options(droiddeckwayland PRIVATE
|
||||
$<$<COMPILE_LANGUAGE:C>:-Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers>)
|
||||
target_include_directories(bannerwayland PRIVATE
|
||||
target_include_directories(droiddeckwayland PRIVATE
|
||||
generated
|
||||
prebuilt/include
|
||||
src
|
||||
@@ -53,56 +54,56 @@ target_include_directories(bannerwayland PRIVATE
|
||||
# framegen tree and the vendored DXBC translator (thirdparty/dxbc, for the LSFG cache format),
|
||||
# which only the app build has: the standalone build (standalone/) gets the stub, which reports
|
||||
# every engine unavailable and generates nothing.
|
||||
set(BANNER_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg)
|
||||
set(BANNER_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/winfg)
|
||||
if (TARGET dxbc AND EXISTS ${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
AND EXISTS ${BANNER_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp)
|
||||
message(STATUS "bannerwayland: frame-generation engines built in (LSFG Native, Win-FG Native)")
|
||||
target_sources(bannerwayland PRIVATE
|
||||
set(DROIDDECK_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg)
|
||||
set(DROIDDECK_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/winfg)
|
||||
if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
AND EXISTS ${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp)
|
||||
message(STATUS "droiddeckwayland: frame-generation engines built in (LSFG Native, Win-FG Native)")
|
||||
target_sources(droiddeckwayland PRIVATE
|
||||
src/framegen_engine.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_probe.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_dll.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_dxbc.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_vkd.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_common.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_shaders.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_mipmaps.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_alpha.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_beta.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_gamma.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_delta.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_generate.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_chain.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_pacer.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_governor.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_probe.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_dll.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_dxbc.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_vkd.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_common.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_shaders.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_mipmaps.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_alpha.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_beta.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_gamma.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_delta.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_generate.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_chain.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_pacer.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_governor.cpp
|
||||
# The Lossless.dll import (validate / extract / cache) for the app's LsfgNative.
|
||||
${BANNER_FRAMEGEN_LSFG_DIR}/lsfg_jni.cpp
|
||||
${BANNER_FRAMEGEN_WINFG_DIR}/framegen.cpp
|
||||
${BANNER_FRAMEGEN_WINFG_DIR}/winfg_vkd.cpp
|
||||
${BANNER_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_jni.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/framegen.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_vkd.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp
|
||||
)
|
||||
target_include_directories(bannerwayland PRIVATE
|
||||
target_include_directories(droiddeckwayland PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/.. # <thirdparty/...> as the frame-generation engines see it
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../framegen # VulkanRendererContext.h (VkTable), lsfg/, winfg/
|
||||
)
|
||||
target_compile_options(bannerwayland PRIVATE
|
||||
target_compile_options(droiddeckwayland PRIVATE
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17 -fvisibility=hidden>)
|
||||
target_compile_definitions(bannerwayland PRIVATE
|
||||
target_compile_definitions(droiddeckwayland PRIVATE
|
||||
WINFG_UPSTREAM="${WINFG_UPSTREAM}"
|
||||
WINFG_CHAIN_HASH="${WINFG_CHAIN_HASH}"
|
||||
)
|
||||
target_link_libraries(bannerwayland dxbc atomic)
|
||||
target_link_libraries(droiddeckwayland dxbc atomic)
|
||||
else()
|
||||
message(STATUS "bannerwayland: frame-generation engines NOT built in (standalone build): stub")
|
||||
target_sources(bannerwayland PRIVATE src/framegen_engine_stub.c)
|
||||
message(STATUS "droiddeckwayland: frame-generation engines NOT built in (standalone build): stub")
|
||||
target_sources(droiddeckwayland PRIVATE src/framegen_engine_stub.c)
|
||||
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
|
||||
# compositor uses Turnip's driver, which supports dmabuf import (drm_format_modifier).
|
||||
target_link_libraries(bannerwayland
|
||||
target_link_libraries(droiddeckwayland
|
||||
wayland-server
|
||||
adrenotools
|
||||
dl
|
||||
|
||||
@@ -29,7 +29,7 @@ the dma-buf fd through `zwp_linux_dmabuf_v1` as today (so the blit path stays as
|
||||
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`,
|
||||
`BANNER_WAYLAND_ZERO_COPY=1`) with a compositor-owned AHB pool and one blit, so the
|
||||
`DROIDDECK_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.
|
||||
|
||||
---
|
||||
@@ -240,13 +240,13 @@ Prerequisite: a build of this branch, container on Wayland, the AIO Graphics Tes
|
||||
D3D12 tabs) as the workload, session log in `Download/Wayland-logs/`.
|
||||
|
||||
0. **UBWC modifier A/B (no layers).** Done on `feat/wayland-ubwc` (advertised when the compositor's
|
||||
driver imports it, `BANNER_WAYLAND_UBWC=0` = old linear-only list; see `WAYLAND_RUNTIME.md`
|
||||
driver imports it, `DROIDDECK_WAYLAND_UBWC=0` = old linear-only list; see `WAYLAND_RUNTIME.md`
|
||||
"Compressed (UBWC) game buffers"). Run the AIO Vulkan/D3D12 with and without. Expect
|
||||
`vulkan: … is presenting GPU frames through Wayland: WxH, format XB24, tiled (zero-copy)` and
|
||||
no `dmabuf import failed`. Records how much of the 20 % is the linear swapchain alone.
|
||||
1. **Layer mode smoke.** Put `BANNER_WAYLAND_ZERO_COPY=1` in the container's environment
|
||||
1. **Layer mode smoke.** Put `DROIDDECK_WAYLAND_ZERO_COPY=1` in the container's environment
|
||||
variables, launch the AIO fullscreen. Expected log lines (tag `layer`):
|
||||
`SurfaceControl "banner_wayland_game" created as a child of the screen surface`,
|
||||
`SurfaceControl "droiddeck_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
|
||||
Turnip accepts gralloc's UBWC pitch), `geometry: buffer 0,0-1920,1080 -> screen …`,
|
||||
@@ -254,7 +254,7 @@ D3D12 tabs) as the workload, session log in `Download/Wayland-logs/`.
|
||||
and the probe line `kernel exports sync_file fences from the game's dma-buf …` or
|
||||
`DMA_BUF_IOCTL_EXPORT_SYNC_FILE … failed (…)`. Picture correct, HUD and pointer on top, fullscreen
|
||||
mode changes move the layer (`geometry:` lines).
|
||||
2. **Composition type.** With the AIO running: `dumpsys SurfaceFlinger | grep -A3 banner_wayland_game`
|
||||
2. **Composition type.** With the AIO running: `dumpsys SurfaceFlinger | grep -A3 droiddeck_wayland_game`
|
||||
and the `GPU_TARGET` line of the pipe table. DEVICE/DEVICE on the game layer with an empty GPU
|
||||
target = the DPU does the scaling; CLIENT = no gain on this panel orientation → try the
|
||||
handheld's native orientation / a resolution equal to the panel's.
|
||||
@@ -271,7 +271,7 @@ harden: async fence instead of CPU wait, per-window layers, presentation feedbac
|
||||
`getLatchTime`); option (a) WSI + protocol + wcp = 2–3 days + 1 day device proving; option (c) =
|
||||
3–4 days + the same proving; (b) = not buildable.
|
||||
|
||||
## 6. What the prototype in this branch does (`BANNER_WAYLAND_ZERO_COPY=1`)
|
||||
## 6. What the prototype in this branch does (`DROIDDECK_WAYLAND_ZERO_COPY=1`)
|
||||
|
||||
Files: `src/sc_layer.{c,h}` (new), `src/vk_present.{c,h}` (`vkp_image_import_dmabuf` with a
|
||||
blit-destination role, `vkp_blit_image`, `vkp_update_map`, `vkp_map_draw`, `vkp_window`,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* See ahb_swapchain.h and ZERO_COPY_SPIKE.md. */
|
||||
#define _GNU_SOURCE
|
||||
#include "ahb_swapchain.h"
|
||||
#include "banner_color.h"
|
||||
#include "banner_ext.h"
|
||||
#include "droiddeck_color.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "sc_layer.h"
|
||||
#include "vk_present.h"
|
||||
#include "banner-ahb-v1-server-protocol.h"
|
||||
@@ -22,10 +22,10 @@
|
||||
#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL /* DRM_FORMAT_MOD_QCOM_COMPRESSED (UBWC) */
|
||||
|
||||
/* linux/dma-buf.h (not in the NDK sysroot's older headers): sync_file export / import. */
|
||||
struct banner_dma_buf_sync_file { uint32_t flags; int32_t fd; };
|
||||
#define BANNER_DMA_BUF_SYNC_READ 1u
|
||||
#define BANNER_DMA_BUF_IOCTL_EXPORT_SYNC_FILE _IOWR('b', 2, struct banner_dma_buf_sync_file)
|
||||
#define BANNER_DMA_BUF_IOCTL_IMPORT_SYNC_FILE _IOW('b', 3, struct banner_dma_buf_sync_file)
|
||||
struct droiddeck_dma_buf_sync_file { uint32_t flags; int32_t fd; };
|
||||
#define DROIDDECK_DMA_BUF_SYNC_READ 1u
|
||||
#define DROIDDECK_DMA_BUF_IOCTL_EXPORT_SYNC_FILE _IOWR('b', 2, struct droiddeck_dma_buf_sync_file)
|
||||
#define DROIDDECK_DMA_BUF_IOCTL_IMPORT_SYNC_FILE _IOW('b', 3, struct droiddeck_dma_buf_sync_file)
|
||||
|
||||
extern volatile int g_zero_copy;
|
||||
|
||||
@@ -90,9 +90,9 @@ static void buf_free(struct ahb_buf *ab) {
|
||||
if (ab->fence_src) { wl_event_source_remove(ab->fence_src); ab->fence_src = NULL; }
|
||||
if (ab->fence_fd >= 0) { close(ab->fence_fd); ab->fence_fd = -1; }
|
||||
wl_list_remove(&ab->link);
|
||||
*banner_dmabuf_ahb_slot(ab->b) = NULL;
|
||||
*droiddeck_dmabuf_ahb_slot(ab->b) = NULL;
|
||||
AHardwareBuffer_release(ab->ahb);
|
||||
banner_dmabuf_unref(ab->b);
|
||||
droiddeck_dmabuf_unref(ab->b);
|
||||
free(ab);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ static void on_buffer_resource_destroyed(struct wl_listener *l, void *data) {
|
||||
|
||||
static void send_deferred_release(struct ahb_buf *ab) {
|
||||
if (ab->release_pending && ab->resource)
|
||||
banner_release_buffer(ab->surface, ab->resource, ab->surface != NULL, ab->deferred_ns);
|
||||
droiddeck_release_buffer(ab->surface, ab->resource, ab->surface != NULL, ab->deferred_ns);
|
||||
ab->release_pending = 0;
|
||||
ab->surface = NULL;
|
||||
}
|
||||
@@ -138,14 +138,14 @@ static void handle_released(uint64_t id, int fd) {
|
||||
if (!ab) { if (fd >= 0) close(fd); return; }
|
||||
ab->on_layer = 0;
|
||||
if (fd >= 0) {
|
||||
struct banner_dma_buf_sync_file imp = {.flags = BANNER_DMA_BUF_SYNC_READ, .fd = fd};
|
||||
int dmabuf_fd = banner_dmabuf_fd(ab->b);
|
||||
if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, BANNER_DMA_BUF_IOCTL_IMPORT_SYNC_FILE, &imp) == 0) {
|
||||
struct droiddeck_dma_buf_sync_file imp = {.flags = DROIDDECK_DMA_BUF_SYNC_READ, .fd = fd};
|
||||
int dmabuf_fd = droiddeck_dmabuf_fd(ab->b);
|
||||
if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, DROIDDECK_DMA_BUF_IOCTL_IMPORT_SYNC_FILE, &imp) == 0) {
|
||||
close(fd);
|
||||
} else {
|
||||
if (!g_import_failed_logged) {
|
||||
g_import_failed_logged = 1;
|
||||
banner_log("layer", "zero-copy: DMA_BUF_IOCTL_IMPORT_SYNC_FILE failed (%s): releases wait for the display's fence here instead",
|
||||
droiddeck_log("layer", "zero-copy: DMA_BUF_IOCTL_IMPORT_SYNC_FILE failed (%s): releases wait for the display's fence here instead",
|
||||
strerror(errno));
|
||||
}
|
||||
/* Wait it out in the event loop (a sync_file is readable once signalled). */
|
||||
@@ -196,23 +196,23 @@ void ahb_swapchain_layer_released(void *token, int release_fd) {
|
||||
/* ---- present */
|
||||
|
||||
int ahb_swapchain_has_ahb(const struct dmabuf_buffer *b) {
|
||||
return b && *banner_dmabuf_ahb_slot((struct dmabuf_buffer *)b) != NULL;
|
||||
return b && *droiddeck_dmabuf_ahb_slot((struct dmabuf_buffer *)b) != NULL;
|
||||
}
|
||||
|
||||
int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_w, int scene_h) {
|
||||
struct ahb_buf *ab = b ? *banner_dmabuf_ahb_slot(b) : NULL;
|
||||
struct ahb_buf *ab = b ? *droiddeck_dmabuf_ahb_slot(b) : NULL;
|
||||
if (!ab) return -1;
|
||||
int dmabuf_fd = banner_dmabuf_fd(b);
|
||||
int dmabuf_fd = droiddeck_dmabuf_fd(b);
|
||||
/* Acquire fence: the game's render fence, which its driver put into the dma-buf before the
|
||||
* commit (Mesa's implicit sync). The display waits on it, not the CPU. */
|
||||
int acquire = -1;
|
||||
struct banner_dma_buf_sync_file exp = {.flags = BANNER_DMA_BUF_SYNC_READ, .fd = -1};
|
||||
if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, BANNER_DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &exp) == 0 && exp.fd >= 0) {
|
||||
struct droiddeck_dma_buf_sync_file exp = {.flags = DROIDDECK_DMA_BUF_SYNC_READ, .fd = -1};
|
||||
if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, DROIDDECK_DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &exp) == 0 && exp.fd >= 0) {
|
||||
acquire = exp.fd;
|
||||
} else if (dmabuf_fd >= 0) {
|
||||
if (!g_export_failed_logged) {
|
||||
g_export_failed_logged = 1;
|
||||
banner_log("layer", "zero-copy: DMA_BUF_IOCTL_EXPORT_SYNC_FILE failed (%s): waiting for each frame on the CPU instead",
|
||||
droiddeck_log("layer", "zero-copy: DMA_BUF_IOCTL_EXPORT_SYNC_FILE failed (%s): waiting for each frame on the CPU instead",
|
||||
strerror(errno));
|
||||
}
|
||||
struct pollfd p = {.fd = dmabuf_fd, .events = POLLIN}; /* readable = the writers are done */
|
||||
@@ -220,7 +220,7 @@ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_
|
||||
do { r = poll(&p, 1, 100); } while (r < 0 && errno == EINTR);
|
||||
}
|
||||
int r = sc_layer_present_ahb(ab->ahb, ab->w, ab->h, acquire, (void *)(uintptr_t)ab->id, scene_w, scene_h,
|
||||
s ? banner_surface_color(s) : NULL, ab->format);
|
||||
s ? droiddeck_surface_color(s) : NULL, ab->format);
|
||||
if (r < 0) return -1;
|
||||
if (r == 1) return 0; /* nothing of it on screen: not on the layer, nothing to release later */
|
||||
if (!ab->on_layer) {
|
||||
@@ -231,8 +231,8 @@ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_
|
||||
if (s && g_announced != s) {
|
||||
char name[160];
|
||||
g_announced = s;
|
||||
banner_surface_describe(s, name, sizeof(name));
|
||||
banner_log("layer", "zero-copy: presenting %s without a copy", name);
|
||||
droiddeck_surface_describe(s, name, sizeof(name));
|
||||
droiddeck_log("layer", "zero-copy: presenting %s without a copy", name);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ int ahb_swapchain_last_frame_age_ms(void) {
|
||||
}
|
||||
|
||||
int ahb_swapchain_defer_release(struct dmabuf_buffer *b, struct wl_resource *buffer, struct surface *s, int paced) {
|
||||
struct ahb_buf *ab = b ? *banner_dmabuf_ahb_slot(b) : NULL;
|
||||
struct ahb_buf *ab = b ? *droiddeck_dmabuf_ahb_slot(b) : NULL;
|
||||
if (!ab || !ab->on_layer || !buffer || ab->resource != buffer) return 0;
|
||||
ab->release_pending = 1;
|
||||
ab->deferred_ns = now_ns();
|
||||
@@ -268,7 +268,7 @@ unsigned ahb_swapchain_stats_take(void) {
|
||||
int ahb_swapchain_advertised(void) { return g_advertised; }
|
||||
|
||||
uint32_t ahb_swapchain_ahb_format(const struct dmabuf_buffer *b) {
|
||||
struct ahb_buf *ab = b ? *banner_dmabuf_ahb_slot((struct dmabuf_buffer *)b) : NULL;
|
||||
struct ahb_buf *ab = b ? *droiddeck_dmabuf_ahb_slot((struct dmabuf_buffer *)b) : NULL;
|
||||
return ab ? ab->format : 0;
|
||||
}
|
||||
|
||||
@@ -279,14 +279,14 @@ static void ahb_destroy(struct wl_client *c, struct wl_resource *r) { wl_resourc
|
||||
static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_resource *buffer, int32_t sock,
|
||||
uint32_t width, uint32_t height, uint32_t stride, uint32_t mod_hi, uint32_t mod_lo,
|
||||
uint32_t image_count) {
|
||||
struct dmabuf_buffer *b = banner_dmabuf_from_resource(buffer);
|
||||
struct dmabuf_buffer *b = droiddeck_dmabuf_from_resource(buffer);
|
||||
uint64_t modifier = ((uint64_t)mod_hi << 32) | mod_lo;
|
||||
if (!b) {
|
||||
banner_log("layer", "zero-copy: %s attached an AHardwareBuffer to a non-dma-buf wl_buffer, ignored", banner_client_name(c));
|
||||
droiddeck_log("layer", "zero-copy: %s attached an AHardwareBuffer to a non-dma-buf wl_buffer, ignored", droiddeck_client_name(c));
|
||||
close(sock);
|
||||
return;
|
||||
}
|
||||
void **slot = banner_dmabuf_ahb_slot(b);
|
||||
void **slot = droiddeck_dmabuf_ahb_slot(b);
|
||||
if (*slot) { close(sock); return; } /* once per buffer */
|
||||
|
||||
/* The handle was written into the socket before the request was sent; a short wait is just
|
||||
@@ -298,17 +298,17 @@ static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_res
|
||||
int rc = pr > 0 ? AHardwareBuffer_recvHandleFromUnixSocket(sock, &ahb) : -1;
|
||||
close(sock);
|
||||
if (rc != 0 || !ahb) {
|
||||
banner_log("layer", "zero-copy: receiving %s's AHardwareBuffer failed (%s)", banner_client_name(c),
|
||||
droiddeck_log("layer", "zero-copy: receiving %s's AHardwareBuffer failed (%s)", droiddeck_client_name(c),
|
||||
pr > 0 ? "recvHandleFromUnixSocket" : "nothing on the socket");
|
||||
return;
|
||||
}
|
||||
AHardwareBuffer_Desc d;
|
||||
AHardwareBuffer_describe(ahb, &d);
|
||||
int bw, bh;
|
||||
banner_dmabuf_size(b, &bw, &bh);
|
||||
droiddeck_dmabuf_size(b, &bw, &bh);
|
||||
if ((int)d.width != bw || (int)d.height != bh || (int)width != bw || (int)height != bh) {
|
||||
banner_log("layer", "zero-copy: %s's AHardwareBuffer is %ux%u but its wl_buffer %dx%d, ignored",
|
||||
banner_client_name(c), d.width, d.height, bw, bh);
|
||||
droiddeck_log("layer", "zero-copy: %s's AHardwareBuffer is %ux%u but its wl_buffer %dx%d, ignored",
|
||||
droiddeck_client_name(c), d.width, d.height, bw, bh);
|
||||
AHardwareBuffer_release(ahb);
|
||||
return;
|
||||
}
|
||||
@@ -317,7 +317,7 @@ static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_res
|
||||
if (!ab) { AHardwareBuffer_release(ahb); wl_client_post_no_memory(c); return; }
|
||||
ab->id = g_next_id++;
|
||||
ab->b = b;
|
||||
banner_dmabuf_ref(b);
|
||||
droiddeck_dmabuf_ref(b);
|
||||
ab->ahb = ahb;
|
||||
ab->w = bw; ab->h = bh;
|
||||
ab->stride = d.stride ? d.stride : stride;
|
||||
@@ -335,8 +335,8 @@ static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_res
|
||||
if (g_last_chain.client != c || g_last_chain.w != bw || g_last_chain.h != bh ||
|
||||
g_last_chain.image_count != image_count || g_last_chain.modifier != modifier || g_last_chain.format != d.format) {
|
||||
g_last_chain = (struct chain_seen){c, bw, bh, image_count, modifier, d.format};
|
||||
banner_log("layer", "zero-copy: AHB swapchain from %s (%u images, %dx%d, %s, %s, stride %u px)", banner_client_name(c),
|
||||
image_count, bw, bh, banner_ahb_format_name(d.format), modifier == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)"
|
||||
droiddeck_log("layer", "zero-copy: AHB swapchain from %s (%u images, %dx%d, %s, %s, stride %u px)", droiddeck_client_name(c),
|
||||
image_count, bw, bh, droiddeck_ahb_format_name(d.format), modifier == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)"
|
||||
: modifier == 0 ? "linear" : "unknown modifier", ab->stride);
|
||||
}
|
||||
}
|
||||
@@ -363,7 +363,7 @@ static void bind_ahb(struct wl_client *c, void *data, uint32_t ver, uint32_t id)
|
||||
/* One line per program, not per surface-format query (each binds its own). */
|
||||
if (last_named != c) {
|
||||
last_named = c;
|
||||
banner_log("layer", "zero-copy: %s bound banner_ahb_v1 version %u (%s)", banner_client_name(c), ver,
|
||||
droiddeck_log("layer", "zero-copy: %s bound banner_ahb_v1 version %u (%s)", droiddeck_client_name(c), ver,
|
||||
ver >= 2 ? "follows the live switch" : "version 1: decides from its launch environment only");
|
||||
}
|
||||
}
|
||||
@@ -372,7 +372,7 @@ void ahb_swapchain_set_mode(int on, int live) {
|
||||
on = on ? 1 : 0;
|
||||
g_zero_copy = on;
|
||||
if (!g_advertised) {
|
||||
if (live) banner_log("layer", "zero-copy switched %s from the drawer, but no display layer is available on this device: no change",
|
||||
if (live) droiddeck_log("layer", "zero-copy switched %s from the drawer, but no display layer is available on this device: no change",
|
||||
on ? "on" : "off");
|
||||
return;
|
||||
}
|
||||
@@ -390,37 +390,37 @@ void ahb_swapchain_set_mode(int on, int live) {
|
||||
memset(&g_last_chain, 0, sizeof(g_last_chain));
|
||||
g_announced = NULL;
|
||||
if (live)
|
||||
banner_log("layer", "zero-copy switched %s from the drawer: %d bound program%s told to rebuild their swapchains%s",
|
||||
droiddeck_log("layer", "zero-copy switched %s from the drawer: %d bound program%s told to rebuild their swapchains%s",
|
||||
on ? "on" : "off", told, told == 1 ? "" : "s",
|
||||
on ? "; frames go on the display layer once the new gralloc swapchain is up"
|
||||
: "; gralloc frames still in flight stay on the layer (or take the copy path) until then");
|
||||
else
|
||||
banner_log("layer", "zero-copy: %s at launch (%s)", on ? "on" : "off",
|
||||
on ? "BANNER_WAYLAND_ZERO_COPY=1: games present their own gralloc buffers on the display layer"
|
||||
droiddeck_log("layer", "zero-copy: %s at launch (%s)", on ? "on" : "off",
|
||||
on ? "DROIDDECK_WAYLAND_ZERO_COPY=1: games present their own gralloc buffers on the display layer"
|
||||
: "the drawer's Zero-copy presentation switch turns it on live");
|
||||
if (live) wl_display_flush_clients(banner_get_display());
|
||||
banner_request_redraw();
|
||||
if (live) wl_display_flush_clients(droiddeck_get_display());
|
||||
droiddeck_request_redraw();
|
||||
}
|
||||
|
||||
void ahb_swapchain_init(struct wl_display *display) {
|
||||
wl_list_init(&g_bufs);
|
||||
wl_list_init(&g_clients);
|
||||
if (!sc_layer_available()) {
|
||||
banner_log("layer", "zero-copy: no display layer on this device (see the line above): banner_ahb_v1 not advertised");
|
||||
droiddeck_log("layer", "zero-copy: no display layer on this device (see the line above): banner_ahb_v1 not advertised");
|
||||
return;
|
||||
}
|
||||
g_loop = wl_display_get_event_loop(display);
|
||||
if (pipe2(g_rel_pipe, O_CLOEXEC | O_NONBLOCK) != 0) {
|
||||
banner_log("error", "zero-copy: release pipe creation failed (%s); zero-copy presents disabled", strerror(errno));
|
||||
droiddeck_log("error", "zero-copy: release pipe creation failed (%s); zero-copy presents disabled", strerror(errno));
|
||||
g_rel_pipe[0] = g_rel_pipe[1] = -1;
|
||||
return;
|
||||
}
|
||||
wl_event_loop_add_fd(g_loop, g_rel_pipe[0], WL_EVENT_READABLE, on_release_pipe, NULL);
|
||||
if (!wl_global_create(display, &banner_ahb_v1_interface, 2, NULL, bind_ahb)) {
|
||||
banner_log("error", "zero-copy: banner_ahb_v1 global creation failed");
|
||||
droiddeck_log("error", "zero-copy: banner_ahb_v1 global creation failed");
|
||||
return;
|
||||
}
|
||||
g_advertised = 1;
|
||||
banner_log("layer", "zero-copy: banner_ahb_v1 version 2 advertised (games built for it present their own gralloc buffers while the switch is on)");
|
||||
droiddeck_log("layer", "zero-copy: banner_ahb_v1 version 2 advertised (games built for it present their own gralloc buffers while the switch is on)");
|
||||
ahb_swapchain_set_mode(g_zero_copy, 0);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef AHB_SWAPCHAIN_H
|
||||
#define AHB_SWAPCHAIN_H
|
||||
/*
|
||||
* Zero-copy game frames (layer mode, BANNER_WAYLAND_ZERO_COPY=1 at launch, the drawer's "Zero-copy
|
||||
* Zero-copy game frames (layer mode, DROIDDECK_WAYLAND_ZERO_COPY=1 at launch, the drawer's "Zero-copy
|
||||
* presentation" switch live): the guest half of the design in ZERO_COPY_SPIKE.md. Our Wayland Turnip
|
||||
* (banners-turnip-wayland, patches/wayland/banner_ahb_wsi.py) binds the private global banner_ahb_v1
|
||||
* (version 2), which this compositor advertises on every session where a display layer is possible,
|
||||
@@ -63,21 +63,21 @@ uint32_t ahb_swapchain_ahb_format(const struct dmabuf_buffer *b);
|
||||
void ahb_swapchain_layer_released(void *token, int release_fd);
|
||||
|
||||
/* ---- ahb_swapchain.c -> compositor.c (hooks) */
|
||||
struct dmabuf_buffer *banner_dmabuf_from_resource(struct wl_resource *buffer);
|
||||
int banner_dmabuf_fd(const struct dmabuf_buffer *b); /* plane 0's dma-buf */
|
||||
void banner_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h);
|
||||
void **banner_dmabuf_ahb_slot(struct dmabuf_buffer *b); /* this module's per-buffer state */
|
||||
void banner_dmabuf_ref(struct dmabuf_buffer *b);
|
||||
void banner_dmabuf_unref(struct dmabuf_buffer *b);
|
||||
struct dmabuf_buffer *droiddeck_dmabuf_from_resource(struct wl_resource *buffer);
|
||||
int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b); /* plane 0's dma-buf */
|
||||
void droiddeck_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h);
|
||||
void **droiddeck_dmabuf_ahb_slot(struct dmabuf_buffer *b); /* this module's per-buffer state */
|
||||
void droiddeck_dmabuf_ref(struct dmabuf_buffer *b);
|
||||
void droiddeck_dmabuf_unref(struct dmabuf_buffer *b);
|
||||
/* Give a wl_buffer back to its client now (paced = 0) or on the FPS limiter's cadence (s != NULL).
|
||||
* since_ns = when the compositor let go of it (CLOCK_MONOTONIC; the perf line's release latency). */
|
||||
void banner_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns);
|
||||
void droiddeck_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns);
|
||||
/* Redraw the scene on the next tick (the present path may have changed). */
|
||||
void banner_request_redraw(void);
|
||||
void droiddeck_request_redraw(void);
|
||||
/* "<title>" (program) of the window a surface belongs to, for the log. */
|
||||
void banner_surface_describe(const struct surface *s, char *out, size_t size);
|
||||
/* The surface's current image description (banner_color.h), NULL = none / HDR gate closed. */
|
||||
struct banner_color;
|
||||
const struct banner_color *banner_surface_color(const struct surface *s);
|
||||
void droiddeck_surface_describe(const struct surface *s, char *out, size_t size);
|
||||
/* The surface's current image description (droiddeck_color.h), NULL = none / HDR gate closed. */
|
||||
struct droiddeck_color;
|
||||
const struct droiddeck_color *droiddeck_surface_color(const struct surface *s);
|
||||
|
||||
#endif
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Interface between compositor.c and the extension modules that live in their own files:
|
||||
* wl_clipboard.c wl_data_device_manager + zwlr_data_control_manager_v1 (selection only)
|
||||
* wl_text_input.c zwp_text_input_manager_v3 (host IME / soft keyboard)
|
||||
* wl_toplevel_icon.c xdg_toplevel_icon_manager_v1 (accepted, not rendered)
|
||||
* banner_ext.c the host→compositor message queue shared by the modules
|
||||
* Everything here runs on the compositor (dispatch) thread unless noted.
|
||||
*/
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <wayland-server.h>
|
||||
|
||||
void banner_log(const char *tag, const char *fmt, ...);
|
||||
|
||||
/* ---- compositor.c → modules */
|
||||
void banner_ext_init(struct wl_display *display); /* creates the globals + host queue */
|
||||
void banner_clipboard_keyboard_focus(struct wl_client *client); /* keyboard focus moved; NULL = none */
|
||||
void banner_text_input_refocus(void); /* the text-input target may have changed */
|
||||
void banner_text_input_surface_gone(struct wl_resource *surface);
|
||||
|
||||
/* ---- modules → compositor.c */
|
||||
const char *banner_client_name(struct wl_client *client);
|
||||
struct wl_display *banner_get_display(void);
|
||||
/* The surface text input follows (the last clicked window, else the topmost program window). */
|
||||
struct wl_resource *banner_ime_target(void);
|
||||
/* Scene (virtual desktop) origin of a toplevel surface; 0,0 when unknown. */
|
||||
void banner_surface_scene_origin(struct wl_resource *surface, int *x, int *y);
|
||||
/* Deliver a key press/release through the wl_keyboard path as if typed. */
|
||||
void banner_inject_key(uint32_t evdev, int pressed);
|
||||
|
||||
/* ---- modules → app (JNI upcalls; waylandcomp_jni.c) */
|
||||
void banner_on_clipboard_text(const char *utf8, int len);
|
||||
void banner_on_text_input(int enabled, const char *program, int x, int y, int w, int h);
|
||||
|
||||
/* ---- app → modules (callable from any thread; queued to the compositor thread) */
|
||||
void banner_host_clipboard_text(const char *utf8, int len); /* len 0 = clear */
|
||||
void banner_host_text_commit(const char *utf8, int len);
|
||||
void banner_host_text_preedit(const char *utf8, int len, int cursor_begin, int cursor_end);
|
||||
void banner_host_text_delete(int before, int after);
|
||||
void banner_host_zero_copy(int on, int live); /* the drawer's zero-copy switch (ahb_swapchain_set_mode) */
|
||||
void banner_host_hdr_output(int on); /* the drawer's HDR output switch (banner_color_set_output) */
|
||||
|
||||
/* ---- module internals reached through the queue (banner_ext.c → modules) */
|
||||
void clipboard_init(struct wl_display *display);
|
||||
void clipboard_host_text(char *utf8, size_t len); /* takes ownership of utf8 */
|
||||
void text_input_init(struct wl_display *display);
|
||||
void text_input_host_commit(const char *utf8, size_t len);
|
||||
void text_input_host_preedit(const char *utf8, size_t len, int cursor_begin, int cursor_end);
|
||||
void text_input_host_delete(int before, int after);
|
||||
void toplevel_icon_init(struct wl_display *display);
|
||||
@@ -0,0 +1,9 @@
|
||||
#version 450
|
||||
// Wayland buffers carry premultiplied alpha; the pipeline blends ONE, ONE_MINUS_SRC_ALPHA.
|
||||
layout(binding = 0) uniform sampler2D Surface;
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = texture(Surface, uv);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#version 450
|
||||
// One surface as a quad: dst = the target rectangle in NDC, src = the buffer rectangle in UV.
|
||||
layout(push_constant) uniform PC { vec4 dst; vec4 src; } pc;
|
||||
layout(location = 0) out vec2 uv;
|
||||
|
||||
void main() {
|
||||
vec2 t = vec2(gl_VertexIndex & 1, gl_VertexIndex >> 1);
|
||||
gl_Position = vec4(mix(pc.dst.xy, pc.dst.zw, t), 0.0, 1.0);
|
||||
uv = mix(pc.src.xy, pc.src.zw, t);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 1115.1.0
|
||||
#pragma once
|
||||
const uint32_t blend_frag_code[] = {
|
||||
0x07230203,0x00010000,0x0008000b,0x00000014,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
|
||||
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x00000009,0x00000011,0x00030010,
|
||||
0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,
|
||||
0x00000000,0x00050005,0x00000009,0x4374756f,0x726f6c6f,0x00000000,0x00040005,0x0000000d,
|
||||
0x66727553,0x00656361,0x00030005,0x00000011,0x00007675,0x00040047,0x00000009,0x0000001e,
|
||||
0x00000000,0x00040047,0x0000000d,0x00000021,0x00000000,0x00040047,0x0000000d,0x00000022,
|
||||
0x00000000,0x00040047,0x00000011,0x0000001e,0x00000000,0x00020013,0x00000002,0x00030021,
|
||||
0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,0x00000006,
|
||||
0x00000004,0x00040020,0x00000008,0x00000003,0x00000007,0x0004003b,0x00000008,0x00000009,
|
||||
0x00000003,0x00090019,0x0000000a,0x00000006,0x00000001,0x00000000,0x00000000,0x00000000,
|
||||
0x00000001,0x00000000,0x0003001b,0x0000000b,0x0000000a,0x00040020,0x0000000c,0x00000000,
|
||||
0x0000000b,0x0004003b,0x0000000c,0x0000000d,0x00000000,0x00040017,0x0000000f,0x00000006,
|
||||
0x00000002,0x00040020,0x00000010,0x00000001,0x0000000f,0x0004003b,0x00000010,0x00000011,
|
||||
0x00000001,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,
|
||||
0x0004003d,0x0000000b,0x0000000e,0x0000000d,0x0004003d,0x0000000f,0x00000012,0x00000011,
|
||||
0x00050057,0x00000007,0x00000013,0x0000000e,0x00000012,0x0003003e,0x00000009,0x00000013,
|
||||
0x000100fd,0x00010038
|
||||
};
|
||||
@@ -0,0 +1,315 @@
|
||||
/* Alpha composition: one surface drawn source-over onto the scene - see blend_pass.h. Compositor thread. */
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "blend_pass.h"
|
||||
#include "vk_loader.h"
|
||||
#include "vk_present.h" /* droiddeck_log, vkp_gpu_name */
|
||||
#include <string.h>
|
||||
|
||||
/* SPIR-V, pre-compiled (glslangValidator -V blend.vert --vn blend_vert_code, the same for blend.frag)
|
||||
* and committed next to the GLSL: the NDK build compiles no shaders. */
|
||||
#include "blend_vert.h"
|
||||
#include "blend_frag.h"
|
||||
|
||||
struct pc_blend { float dst[4]; float src[4]; };
|
||||
|
||||
static VkDevice g_dev;
|
||||
static int g_ready; /* 0 = not built, 1 = ok, -1 = unavailable (said once) */
|
||||
static VkSampler g_sampler;
|
||||
static VkDescriptorSetLayout g_dsl;
|
||||
static VkPipelineLayout g_pl;
|
||||
static VkDescriptorPool g_dpool;
|
||||
static VkShaderModule g_vert, g_frag;
|
||||
static unsigned g_frame = 1;
|
||||
|
||||
/* One render pass + pipeline per target format: the screen swapchain's, the scene image's, the HDR
|
||||
* composition's. */
|
||||
#define NFMT 4
|
||||
static struct { VkFormat fmt; VkRenderPass rp; VkPipeline pipe; } g_out[NFMT];
|
||||
|
||||
#define NSRC 32
|
||||
static struct { VkImage img; VkImageView view; VkDescriptorSet ds; unsigned frame; } g_src[NSRC];
|
||||
#define NDST 12
|
||||
static struct { VkImage img; VkFormat fmt; int w, h; VkImageView view; VkFramebuffer fb; unsigned frame; } g_dst[NDST];
|
||||
|
||||
void blendp_bind_device(VkDevice dev) { g_dev = dev; }
|
||||
|
||||
void blendp_begin_frame(void) { g_frame++; }
|
||||
|
||||
static void src_release(int i) {
|
||||
if (g_src[i].ds) g_vk.FreeDescriptorSets(g_dev, g_dpool, 1, &g_src[i].ds);
|
||||
if (g_src[i].view) g_vk.DestroyImageView(g_dev, g_src[i].view, NULL);
|
||||
memset(&g_src[i], 0, sizeof(g_src[i]));
|
||||
}
|
||||
|
||||
static void dst_release(int i) {
|
||||
if (g_dst[i].fb) g_vk.DestroyFramebuffer(g_dev, g_dst[i].fb, NULL);
|
||||
if (g_dst[i].view) g_vk.DestroyImageView(g_dev, g_dst[i].view, NULL);
|
||||
memset(&g_dst[i], 0, sizeof(g_dst[i]));
|
||||
}
|
||||
|
||||
void blendp_forget_image(VkImage img) {
|
||||
if (!g_dev || !img) return;
|
||||
for (int i = 0; i < NSRC; i++) if (g_src[i].img == img) src_release(i);
|
||||
for (int i = 0; i < NDST; i++) if (g_dst[i].img == img) dst_release(i);
|
||||
}
|
||||
|
||||
static VkShaderModule shader(const uint32_t *code, size_t size) {
|
||||
VkShaderModule m = VK_NULL_HANDLE;
|
||||
VkShaderModuleCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = size, .pCode = code};
|
||||
if (g_vk.CreateShaderModule(g_dev, &ci, NULL, &m) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||
return m;
|
||||
}
|
||||
|
||||
/* The target keeps what the blits put there (LOAD) and stays in TRANSFER_DST_OPTIMAL for the blits
|
||||
* that follow. The dependencies order the pass between them. */
|
||||
static VkRenderPass make_rp(VkFormat fmt) {
|
||||
VkAttachmentDescription att = {.format = fmt, .samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
|
||||
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
|
||||
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL};
|
||||
VkAttachmentReference ref = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
||||
VkSubpassDescription sub = {.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, .colorAttachmentCount = 1,
|
||||
.pColorAttachments = &ref};
|
||||
VkSubpassDependency deps[2] = {
|
||||
{.srcSubpass = VK_SUBPASS_EXTERNAL, .dstSubpass = 0,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT},
|
||||
{.srcSubpass = 0, .dstSubpass = VK_SUBPASS_EXTERNAL,
|
||||
.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT |
|
||||
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT}};
|
||||
VkRenderPassCreateInfo rci = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, .attachmentCount = 1,
|
||||
.pAttachments = &att, .subpassCount = 1, .pSubpasses = &sub,
|
||||
.dependencyCount = 2, .pDependencies = deps};
|
||||
VkRenderPass rp = VK_NULL_HANDLE;
|
||||
if (g_vk.CreateRenderPass(g_dev, &rci, NULL, &rp) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||
return rp;
|
||||
}
|
||||
|
||||
static VkPipeline make_pipe(VkRenderPass rp) {
|
||||
VkPipelineShaderStageCreateInfo st[2] = {
|
||||
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_VERTEX_BIT,
|
||||
.module = g_vert, .pName = "main"},
|
||||
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
.module = g_frag, .pName = "main"}};
|
||||
VkPipelineVertexInputStateCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
||||
VkPipelineInputAssemblyStateCreateInfo ia = {.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
|
||||
.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP};
|
||||
VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||
VkPipelineDynamicStateCreateInfo ds = {.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
|
||||
.dynamicStateCount = 2, .pDynamicStates = dyn};
|
||||
VkPipelineViewportStateCreateInfo vp = {.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
|
||||
.viewportCount = 1, .scissorCount = 1};
|
||||
VkPipelineRasterizationStateCreateInfo rs = {.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
|
||||
.polygonMode = VK_POLYGON_MODE_FILL, .cullMode = VK_CULL_MODE_NONE,
|
||||
.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, .lineWidth = 1.0f};
|
||||
VkPipelineMultisampleStateCreateInfo ms = {.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
|
||||
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT};
|
||||
/* The target's alpha is left alone: the screen is opaque whatever is drawn onto it. */
|
||||
VkPipelineColorBlendAttachmentState ba = {
|
||||
.blendEnable = VK_TRUE, .srcColorBlendFactor = VK_BLEND_FACTOR_ONE,
|
||||
.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, .colorBlendOp = VK_BLEND_OP_ADD,
|
||||
.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
|
||||
.alphaBlendOp = VK_BLEND_OP_ADD,
|
||||
.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT};
|
||||
VkPipelineColorBlendStateCreateInfo cb = {.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
|
||||
.attachmentCount = 1, .pAttachments = &ba};
|
||||
VkGraphicsPipelineCreateInfo pi = {.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .stageCount = 2,
|
||||
.pStages = st, .pVertexInputState = &vi, .pInputAssemblyState = &ia,
|
||||
.pViewportState = &vp, .pRasterizationState = &rs, .pMultisampleState = &ms,
|
||||
.pColorBlendState = &cb, .pDynamicState = &ds, .layout = g_pl,
|
||||
.renderPass = rp, .subpass = 0};
|
||||
VkPipeline p = VK_NULL_HANDLE;
|
||||
if (g_vk.CreateGraphicsPipelines(g_dev, VK_NULL_HANDLE, 1, &pi, NULL, &p) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||
return p;
|
||||
}
|
||||
|
||||
static void objects_destroy(void) {
|
||||
if (g_vert) g_vk.DestroyShaderModule(g_dev, g_vert, NULL);
|
||||
if (g_frag) g_vk.DestroyShaderModule(g_dev, g_frag, NULL);
|
||||
if (g_dpool) g_vk.DestroyDescriptorPool(g_dev, g_dpool, NULL);
|
||||
if (g_pl) g_vk.DestroyPipelineLayout(g_dev, g_pl, NULL);
|
||||
if (g_dsl) g_vk.DestroyDescriptorSetLayout(g_dev, g_dsl, NULL);
|
||||
if (g_sampler) g_vk.DestroySampler(g_dev, g_sampler, NULL);
|
||||
g_vert = g_frag = VK_NULL_HANDLE; g_dpool = VK_NULL_HANDLE; g_pl = VK_NULL_HANDLE;
|
||||
g_dsl = VK_NULL_HANDLE; g_sampler = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
static int objects_ensure(void) {
|
||||
if (g_ready) return g_ready == 1 ? 0 : -1;
|
||||
g_ready = -1;
|
||||
if (!g_dev || !g_vk.CreateGraphicsPipelines) goto fail;
|
||||
VkSamplerCreateInfo sci = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .magFilter = VK_FILTER_LINEAR,
|
||||
.minFilter = VK_FILTER_LINEAR, .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
|
||||
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
||||
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
||||
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE};
|
||||
if (g_vk.CreateSampler(g_dev, &sci, NULL, &g_sampler) != VK_SUCCESS) goto fail;
|
||||
VkDescriptorSetLayoutBinding b = {.binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
.descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT};
|
||||
VkDescriptorSetLayoutCreateInfo dli = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
||||
.bindingCount = 1, .pBindings = &b};
|
||||
if (g_vk.CreateDescriptorSetLayout(g_dev, &dli, NULL, &g_dsl) != VK_SUCCESS) goto fail;
|
||||
VkPushConstantRange pcr = {.stageFlags = VK_SHADER_STAGE_VERTEX_BIT, .offset = 0, .size = sizeof(struct pc_blend)};
|
||||
VkPipelineLayoutCreateInfo pli = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
|
||||
.pSetLayouts = &g_dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pcr};
|
||||
if (g_vk.CreatePipelineLayout(g_dev, &pli, NULL, &g_pl) != VK_SUCCESS) goto fail;
|
||||
VkDescriptorPoolSize ps = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, NSRC};
|
||||
VkDescriptorPoolCreateInfo dpi = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
|
||||
.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, .maxSets = NSRC,
|
||||
.poolSizeCount = 1, .pPoolSizes = &ps};
|
||||
if (g_vk.CreateDescriptorPool(g_dev, &dpi, NULL, &g_dpool) != VK_SUCCESS) goto fail;
|
||||
if (!(g_vert = shader(blend_vert_code, sizeof(blend_vert_code)))) goto fail;
|
||||
if (!(g_frag = shader(blend_frag_code, sizeof(blend_frag_code)))) goto fail;
|
||||
g_ready = 1;
|
||||
droiddeck_log("gpu", "alpha composition ready on %s: translucent surfaces are blended over what is under them",
|
||||
vkp_gpu_name());
|
||||
return 0;
|
||||
fail:
|
||||
objects_destroy();
|
||||
droiddeck_log("error", "gpu: the alpha composition pass could not be built on this driver; translucent surfaces "
|
||||
"are drawn opaque this session");
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int out_slot(VkFormat fmt) {
|
||||
int i;
|
||||
for (i = 0; i < NFMT && g_out[i].pipe; i++) if (g_out[i].fmt == fmt) return i;
|
||||
if (i == NFMT) return -1;
|
||||
VkRenderPass rp = make_rp(fmt);
|
||||
VkPipeline pipe = rp ? make_pipe(rp) : VK_NULL_HANDLE;
|
||||
if (!pipe) {
|
||||
if (rp) g_vk.DestroyRenderPass(g_dev, rp, NULL);
|
||||
return -1;
|
||||
}
|
||||
g_out[i].fmt = fmt; g_out[i].rp = rp; g_out[i].pipe = pipe;
|
||||
return i;
|
||||
}
|
||||
|
||||
static VkImageView make_view(VkImage img, VkFormat fmt) {
|
||||
VkImageView v = VK_NULL_HANDLE;
|
||||
VkImageViewCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = img,
|
||||
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = fmt,
|
||||
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
|
||||
if (g_vk.CreateImageView(g_dev, &ci, NULL, &v) != VK_SUCCESS) return VK_NULL_HANDLE;
|
||||
return v;
|
||||
}
|
||||
|
||||
/* The slot to recycle: a free one, else the one unused for longest - never one this command buffer
|
||||
* already refers to. -1 = every slot is in this frame. */
|
||||
#define OLDEST(arr, n, out) do { \
|
||||
unsigned age_ = 0; \
|
||||
(out) = -1; \
|
||||
for (int k_ = 0; k_ < (n); k_++) { \
|
||||
if (!(arr)[k_].img) { (out) = k_; break; } \
|
||||
if ((arr)[k_].frame != g_frame && g_frame - (arr)[k_].frame > age_) { age_ = g_frame - (arr)[k_].frame; (out) = k_; } \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static VkDescriptorSet src_ensure(VkImage img, VkFormat fmt, VkImageLayout layout) {
|
||||
int i;
|
||||
for (i = 0; i < NSRC; i++)
|
||||
if (g_src[i].img == img) { g_src[i].frame = g_frame; return g_src[i].ds; }
|
||||
OLDEST(g_src, NSRC, i);
|
||||
if (i < 0) return VK_NULL_HANDLE;
|
||||
src_release(i);
|
||||
if (!(g_src[i].view = make_view(img, fmt))) return VK_NULL_HANDLE;
|
||||
VkDescriptorSetAllocateInfo ai = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
|
||||
.descriptorPool = g_dpool, .descriptorSetCount = 1, .pSetLayouts = &g_dsl};
|
||||
if (g_vk.AllocateDescriptorSets(g_dev, &ai, &g_src[i].ds) != VK_SUCCESS) {
|
||||
g_src[i].ds = VK_NULL_HANDLE;
|
||||
src_release(i);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
VkDescriptorImageInfo ii = {.sampler = g_sampler, .imageView = g_src[i].view, .imageLayout = layout};
|
||||
VkWriteDescriptorSet wr = {.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = g_src[i].ds, .dstBinding = 0,
|
||||
.descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
||||
.pImageInfo = &ii};
|
||||
g_vk.UpdateDescriptorSets(g_dev, 1, &wr, 0, NULL);
|
||||
g_src[i].img = img; g_src[i].frame = g_frame;
|
||||
return g_src[i].ds;
|
||||
}
|
||||
|
||||
static VkFramebuffer dst_ensure(VkImage img, VkFormat fmt, int w, int h, VkRenderPass rp) {
|
||||
int i;
|
||||
for (i = 0; i < NDST; i++) {
|
||||
if (g_dst[i].img != img) continue;
|
||||
if (g_dst[i].fmt == fmt && g_dst[i].w == w && g_dst[i].h == h) { g_dst[i].frame = g_frame; return g_dst[i].fb; }
|
||||
if (g_dst[i].frame == g_frame) return VK_NULL_HANDLE;
|
||||
break;
|
||||
}
|
||||
if (i == NDST) OLDEST(g_dst, NDST, i);
|
||||
if (i < 0) return VK_NULL_HANDLE;
|
||||
dst_release(i);
|
||||
if (!(g_dst[i].view = make_view(img, fmt))) return VK_NULL_HANDLE;
|
||||
VkFramebufferCreateInfo fi = {.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, .renderPass = rp,
|
||||
.attachmentCount = 1, .pAttachments = &g_dst[i].view, .width = (uint32_t)w,
|
||||
.height = (uint32_t)h, .layers = 1};
|
||||
if (g_vk.CreateFramebuffer(g_dev, &fi, NULL, &g_dst[i].fb) != VK_SUCCESS) {
|
||||
g_dst[i].fb = VK_NULL_HANDLE;
|
||||
dst_release(i);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
g_dst[i].img = img; g_dst[i].fmt = fmt; g_dst[i].w = w; g_dst[i].h = h; g_dst[i].frame = g_frame;
|
||||
return g_dst[i].fb;
|
||||
}
|
||||
|
||||
static void src_layout(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkImageLayout to) {
|
||||
int sampling = to == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkImageMemoryBarrier b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = from, .newLayout = to,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = img, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1},
|
||||
.srcAccessMask = sampling ? VK_ACCESS_TRANSFER_READ_BIT : VK_ACCESS_SHADER_READ_BIT,
|
||||
.dstAccessMask = sampling ? VK_ACCESS_SHADER_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT};
|
||||
g_vk.CmdPipelineBarrier(cmd, sampling ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||
sampling ? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 0, NULL, 0, NULL, 1, &b);
|
||||
}
|
||||
|
||||
int blendp_draw(VkCommandBuffer cmd, VkImage src, VkFormat src_fmt, int src_w, int src_h, int src_general,
|
||||
VkImage dst, VkFormat dst_fmt, int dst_w, int dst_h, const VkImageBlit *blit) {
|
||||
if (!src || !dst || src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0 || objects_ensure() != 0) return -1;
|
||||
int slot = out_slot(dst_fmt);
|
||||
if (slot < 0) return -1;
|
||||
VkImageLayout sampled = src_general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkDescriptorSet ds = src_ensure(src, src_fmt, sampled);
|
||||
if (!ds) return -1;
|
||||
VkFramebuffer fb = dst_ensure(dst, dst_fmt, dst_w, dst_h, g_out[slot].rp);
|
||||
if (!fb) return -1;
|
||||
|
||||
const VkOffset3D *d = blit->dstOffsets, *s = blit->srcOffsets;
|
||||
int x0 = d[0].x < d[1].x ? d[0].x : d[1].x, x1 = d[0].x < d[1].x ? d[1].x : d[0].x;
|
||||
int y0 = d[0].y < d[1].y ? d[0].y : d[1].y, y1 = d[0].y < d[1].y ? d[1].y : d[0].y;
|
||||
if (x0 < 0) x0 = 0;
|
||||
if (y0 < 0) y0 = 0;
|
||||
if (x1 > dst_w) x1 = dst_w;
|
||||
if (y1 > dst_h) y1 = dst_h;
|
||||
if (x1 <= x0 || y1 <= y0) return 0;
|
||||
|
||||
struct pc_blend pc = {
|
||||
.dst = {2.0f * d[0].x / dst_w - 1.0f, 2.0f * d[0].y / dst_h - 1.0f,
|
||||
2.0f * d[1].x / dst_w - 1.0f, 2.0f * d[1].y / dst_h - 1.0f},
|
||||
.src = {(float)s[0].x / src_w, (float)s[0].y / src_h, (float)s[1].x / src_w, (float)s[1].y / src_h}};
|
||||
|
||||
if (!src_general) src_layout(cmd, src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sampled);
|
||||
VkRenderPassBeginInfo rpi = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .renderPass = g_out[slot].rp,
|
||||
.framebuffer = fb,
|
||||
.renderArea = {{x0, y0}, {(uint32_t)(x1 - x0), (uint32_t)(y1 - y0)}}};
|
||||
VkViewport vp = {0, 0, (float)dst_w, (float)dst_h, 0, 1};
|
||||
g_vk.CmdBeginRenderPass(cmd, &rpi, VK_SUBPASS_CONTENTS_INLINE);
|
||||
g_vk.CmdSetViewport(cmd, 0, 1, &vp);
|
||||
g_vk.CmdSetScissor(cmd, 0, 1, &rpi.renderArea);
|
||||
g_vk.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, g_out[slot].pipe);
|
||||
g_vk.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, g_pl, 0, 1, &ds, 0, NULL);
|
||||
g_vk.CmdPushConstants(cmd, g_pl, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc);
|
||||
g_vk.CmdDraw(cmd, 4, 1, 0, 0);
|
||||
g_vk.CmdEndRenderPass(cmd);
|
||||
if (!src_general) src_layout(cmd, src, sampled, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef BLEND_PASS_H
|
||||
#define BLEND_PASS_H
|
||||
/*
|
||||
* Alpha composition. The scene is composed with blits, which replace what is under them; a surface
|
||||
* with translucent pixels (gamescope's overlay and notification planes over a game, a toolkit's
|
||||
* shadowed popup) is drawn through this pass instead, source-over with premultiplied alpha.
|
||||
*
|
||||
* Compositor thread. Objects are built on the first use; if the driver refuses them the pass is
|
||||
* unavailable for the session (logged once) and the caller keeps blitting.
|
||||
*/
|
||||
#define VK_USE_PLATFORM_ANDROID_KHR
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
/* Called from dev_init (device up). */
|
||||
void blendp_bind_device(VkDevice dev);
|
||||
/* A source or target image is about to be destroyed: drop what is cached for it. Frames are fenced,
|
||||
* so nothing submitted still uses it. */
|
||||
void blendp_forget_image(VkImage img);
|
||||
/* A new command buffer is being recorded: what the previous one used may be recycled again. */
|
||||
void blendp_begin_frame(void);
|
||||
|
||||
/* Record one surface over `dst`. `dst` is in TRANSFER_DST_OPTIMAL before and after. `src` was created
|
||||
* with SAMPLED usage and is in GENERAL (src_general) or TRANSFER_SRC_OPTIMAL, and is left there.
|
||||
* `blit` is the draw as the blit path maps it. 0 = recorded, -1 = unavailable (nothing recorded). */
|
||||
int blendp_draw(VkCommandBuffer cmd, VkImage src, VkFormat src_fmt, int src_w, int src_h, int src_general,
|
||||
VkImage dst, VkFormat dst_fmt, int dst_w, int dst_h, const VkImageBlit *blit);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,55 @@
|
||||
// 1115.1.0
|
||||
#pragma once
|
||||
const uint32_t blend_vert_code[] = {
|
||||
0x07230203,0x00010000,0x0008000b,0x0000003a,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
|
||||
0x0008000f,0x00000000,0x00000004,0x6e69616d,0x00000000,0x0000000c,0x0000001b,0x00000031,
|
||||
0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,0x00000000,0x00030005,
|
||||
0x00000009,0x00000074,0x00060005,0x0000000c,0x565f6c67,0x65747265,0x646e4978,0x00007865,
|
||||
0x00060005,0x00000019,0x505f6c67,0x65567265,0x78657472,0x00000000,0x00060006,0x00000019,
|
||||
0x00000000,0x505f6c67,0x7469736f,0x006e6f69,0x00070006,0x00000019,0x00000001,0x505f6c67,
|
||||
0x746e696f,0x657a6953,0x00000000,0x00070006,0x00000019,0x00000002,0x435f6c67,0x4470696c,
|
||||
0x61747369,0x0065636e,0x00070006,0x00000019,0x00000003,0x435f6c67,0x446c6c75,0x61747369,
|
||||
0x0065636e,0x00030005,0x0000001b,0x00000000,0x00030005,0x0000001d,0x00004350,0x00040006,
|
||||
0x0000001d,0x00000000,0x00747364,0x00040006,0x0000001d,0x00000001,0x00637273,0x00030005,
|
||||
0x0000001f,0x00006370,0x00030005,0x00000031,0x00007675,0x00040047,0x0000000c,0x0000000b,
|
||||
0x0000002a,0x00030047,0x00000019,0x00000002,0x00050048,0x00000019,0x00000000,0x0000000b,
|
||||
0x00000000,0x00050048,0x00000019,0x00000001,0x0000000b,0x00000001,0x00050048,0x00000019,
|
||||
0x00000002,0x0000000b,0x00000003,0x00050048,0x00000019,0x00000003,0x0000000b,0x00000004,
|
||||
0x00030047,0x0000001d,0x00000002,0x00050048,0x0000001d,0x00000000,0x00000023,0x00000000,
|
||||
0x00050048,0x0000001d,0x00000001,0x00000023,0x00000010,0x00040047,0x00000031,0x0000001e,
|
||||
0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,
|
||||
0x00000020,0x00040017,0x00000007,0x00000006,0x00000002,0x00040020,0x00000008,0x00000007,
|
||||
0x00000007,0x00040015,0x0000000a,0x00000020,0x00000001,0x00040020,0x0000000b,0x00000001,
|
||||
0x0000000a,0x0004003b,0x0000000b,0x0000000c,0x00000001,0x0004002b,0x0000000a,0x0000000e,
|
||||
0x00000001,0x00040017,0x00000015,0x00000006,0x00000004,0x00040015,0x00000016,0x00000020,
|
||||
0x00000000,0x0004002b,0x00000016,0x00000017,0x00000001,0x0004001c,0x00000018,0x00000006,
|
||||
0x00000017,0x0006001e,0x00000019,0x00000015,0x00000006,0x00000018,0x00000018,0x00040020,
|
||||
0x0000001a,0x00000003,0x00000019,0x0004003b,0x0000001a,0x0000001b,0x00000003,0x0004002b,
|
||||
0x0000000a,0x0000001c,0x00000000,0x0004001e,0x0000001d,0x00000015,0x00000015,0x00040020,
|
||||
0x0000001e,0x00000009,0x0000001d,0x0004003b,0x0000001e,0x0000001f,0x00000009,0x00040020,
|
||||
0x00000020,0x00000009,0x00000015,0x0004002b,0x00000006,0x00000029,0x00000000,0x0004002b,
|
||||
0x00000006,0x0000002a,0x3f800000,0x00040020,0x0000002e,0x00000003,0x00000015,0x00040020,
|
||||
0x00000030,0x00000003,0x00000007,0x0004003b,0x00000030,0x00000031,0x00000003,0x00050036,
|
||||
0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000008,
|
||||
0x00000009,0x00000007,0x0004003d,0x0000000a,0x0000000d,0x0000000c,0x000500c7,0x0000000a,
|
||||
0x0000000f,0x0000000d,0x0000000e,0x0004006f,0x00000006,0x00000010,0x0000000f,0x0004003d,
|
||||
0x0000000a,0x00000011,0x0000000c,0x000500c3,0x0000000a,0x00000012,0x00000011,0x0000000e,
|
||||
0x0004006f,0x00000006,0x00000013,0x00000012,0x00050050,0x00000007,0x00000014,0x00000010,
|
||||
0x00000013,0x0003003e,0x00000009,0x00000014,0x00050041,0x00000020,0x00000021,0x0000001f,
|
||||
0x0000001c,0x0004003d,0x00000015,0x00000022,0x00000021,0x0007004f,0x00000007,0x00000023,
|
||||
0x00000022,0x00000022,0x00000000,0x00000001,0x00050041,0x00000020,0x00000024,0x0000001f,
|
||||
0x0000001c,0x0004003d,0x00000015,0x00000025,0x00000024,0x0007004f,0x00000007,0x00000026,
|
||||
0x00000025,0x00000025,0x00000002,0x00000003,0x0004003d,0x00000007,0x00000027,0x00000009,
|
||||
0x0008000c,0x00000007,0x00000028,0x00000001,0x0000002e,0x00000023,0x00000026,0x00000027,
|
||||
0x00050051,0x00000006,0x0000002b,0x00000028,0x00000000,0x00050051,0x00000006,0x0000002c,
|
||||
0x00000028,0x00000001,0x00070050,0x00000015,0x0000002d,0x0000002b,0x0000002c,0x00000029,
|
||||
0x0000002a,0x00050041,0x0000002e,0x0000002f,0x0000001b,0x0000001c,0x0003003e,0x0000002f,
|
||||
0x0000002d,0x00050041,0x00000020,0x00000032,0x0000001f,0x0000000e,0x0004003d,0x00000015,
|
||||
0x00000033,0x00000032,0x0007004f,0x00000007,0x00000034,0x00000033,0x00000033,0x00000000,
|
||||
0x00000001,0x00050041,0x00000020,0x00000035,0x0000001f,0x0000000e,0x0004003d,0x00000015,
|
||||
0x00000036,0x00000035,0x0007004f,0x00000007,0x00000037,0x00000036,0x00000036,0x00000002,
|
||||
0x00000003,0x0004003d,0x00000007,0x00000038,0x00000009,0x0008000c,0x00000007,0x00000039,
|
||||
0x00000001,0x0000002e,0x00000034,0x00000037,0x00000038,0x0003003e,0x00000031,0x00000039,
|
||||
0x000100fd,0x00010038
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* What compositor.c shares with the protocol modules split out of it (wl_dmabuf.c): the dma-buf
|
||||
* buffer records, the per-client bookkeeping and the few functions and globals both sides use.
|
||||
* Everything declared here is hidden: it is internal to libbannerwayland, not part of its ABI.
|
||||
* Everything declared here is hidden: it is internal to libdroiddeckwayland, not part of its ABI.
|
||||
*/
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
@@ -13,7 +13,7 @@
|
||||
#define COMPOSITOR_INTERNAL __attribute__((visibility("hidden")))
|
||||
|
||||
#ifndef WLOGE
|
||||
#define WLOGE(...) __android_log_print(ANDROID_LOG_ERROR, "BannerWayland", __VA_ARGS__)
|
||||
#define WLOGE(...) __android_log_print(ANDROID_LOG_ERROR, "DroidDeckWayland", __VA_ARGS__)
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ dmabuf buffers */
|
||||
@@ -60,6 +60,9 @@ struct client_info {
|
||||
/* An OpenGL program whose EGL gave up on the GPU: it asked for dma-buf feedback (EGL's
|
||||
* Wayland GPU path always does), never made a dma-buf buffer, and draws wl_shm frames. */
|
||||
unsigned asked_feedback : 1, shm_gl_said : 1;
|
||||
/* Clients that describe opaque regions can use alpha outside those regions. Wine windows
|
||||
* never describe them, and their unused alpha channel must not make them translucent. */
|
||||
unsigned declares_opaque : 1;
|
||||
unsigned dmabuf_buffers, shm_frames;
|
||||
struct client_info *next;
|
||||
};
|
||||
@@ -68,8 +71,8 @@ struct client_info {
|
||||
COMPOSITOR_INTERNAL struct client_info *client_info_of(struct wl_client *client);
|
||||
COMPOSITOR_INTERNAL void dmabuf_buffer_unref(struct dmabuf_buffer *b);
|
||||
COMPOSITOR_INTERNAL extern const struct wl_buffer_interface dbuf_buffer_impl;
|
||||
extern volatile int g_ubwc; /* BANNER_WAYLAND_UBWC (waylandcomp_jni.c sets it) */
|
||||
extern volatile int g_no_render_node; /* BANNER_WAYLAND_NO_RENDER_NODE */
|
||||
extern volatile int g_ubwc; /* DROIDDECK_WAYLAND_UBWC (waylandcomp_jni.c sets it) */
|
||||
extern volatile int g_no_render_node; /* DROIDDECK_WAYLAND_NO_RENDER_NODE */
|
||||
|
||||
/* ---- wl_dmabuf.c */
|
||||
COMPOSITOR_INTERNAL extern dev_t g_main_device; /* the render node clients are told to allocate on */
|
||||
|
||||
+47
-47
@@ -1,5 +1,5 @@
|
||||
#ifndef BANNER_COLOR_H
|
||||
#define BANNER_COLOR_H
|
||||
#ifndef DROIDDECK_COLOR_H
|
||||
#define DROIDDECK_COLOR_H
|
||||
/*
|
||||
* 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
|
||||
@@ -8,18 +8,18 @@
|
||||
* with DXVK_HDR=1 (the app exports it when the setting is on).
|
||||
*
|
||||
* THE GATE. Nothing here exists for a session unless all of these hold when the compositor starts:
|
||||
* - the container's / game's "HDR output" setting is on, or BANNER_WAYLAND_HDR=1 in their
|
||||
* - the container's / game's "HDR output" setting is on, or DROIDDECK_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);
|
||||
* - 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
|
||||
* In game, the drawer's HDR output switch (droiddeck_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
|
||||
* 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;
|
||||
* DROIDDECK_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).
|
||||
*
|
||||
* WHAT AN OPEN GATE OFFERS (exactly the subset Mesa binds, HDR_RECON.md §2.2): wp_color_manager_v1
|
||||
@@ -35,13 +35,13 @@
|
||||
#include <wayland-server.h>
|
||||
|
||||
/* ADataSpace values (NDK <android/data_space.h>) the layer path emits. */
|
||||
#define BANNER_ADATASPACE_UNKNOWN 0
|
||||
#define BANNER_ADATASPACE_BT2020_PQ 163971072 /* STANDARD_BT2020 | TRANSFER_ST2084 | RANGE_FULL = 0x09c60000 */
|
||||
#define BANNER_ADATASPACE_BT2020_HLG 168165376 /* STANDARD_BT2020 | TRANSFER_HLG | RANGE_FULL */
|
||||
#define DROIDDECK_ADATASPACE_UNKNOWN 0
|
||||
#define DROIDDECK_ADATASPACE_BT2020_PQ 163971072 /* STANDARD_BT2020 | TRANSFER_ST2084 | RANGE_FULL = 0x09c60000 */
|
||||
#define DROIDDECK_ADATASPACE_BT2020_HLG 168165376 /* STANDARD_BT2020 | TRANSFER_HLG | RANGE_FULL */
|
||||
|
||||
/* One image description as the program made it, and what it becomes on a display layer. Immutable:
|
||||
* a changed description is a new record with a new identity. */
|
||||
struct banner_color {
|
||||
struct droiddeck_color {
|
||||
uint32_t identity; /* the id the `ready` event carried (never 0) */
|
||||
uint32_t primaries, tf; /* wp_color_manager_v1 named values */
|
||||
int32_t dataspace; /* ADataSpace on a display layer; 0 = none (treated as sRGB) */
|
||||
@@ -54,97 +54,97 @@ struct banner_color {
|
||||
};
|
||||
|
||||
/* ---- app -> compositor (JNI, any thread) */
|
||||
/* mode 0 = off, 1 = BANNER_WAYLAND_HDR=1, 2 = BANNER_WAYLAND_HDR=force (testing). source names where
|
||||
/* mode 0 = off, 1 = DROIDDECK_WAYLAND_HDR=1, 2 = DROIDDECK_WAYLAND_HDR=force (testing). source names where
|
||||
* the switch came from; dxvk_hdr = DXVK_HDR=1 is in the game's environment; zero_copy_forced = the
|
||||
* app turned zero-copy presentation on for this session because HDR needs it. Before the start. */
|
||||
void banner_color_set_request(int mode, const char *source, int dxvk_hdr, int zero_copy_forced);
|
||||
void droiddeck_color_set_request(int mode, const char *source, int dxvk_hdr, int zero_copy_forced);
|
||||
/* The display the game is on, as android.view.Display reports it. Before the start (it feeds the
|
||||
* gate) and again whenever it changes (logged; the gate is decided once per session). */
|
||||
void banner_color_set_display(int id, const char *name, const char *formats, int hdr10, float max_lum,
|
||||
void droiddeck_color_set_display(int id, const char *name, const char *formats, int hdr10, float max_lum,
|
||||
float max_avg, float min_lum, int ratio_available, float ratio, int api);
|
||||
/* One reading of Display.getHdrSdrRatio() (API 34+; < 0 = not available). listener = it came from
|
||||
* the display's ratio listener rather than the app's periodic sampler. */
|
||||
void banner_color_ratio_sample(float ratio, int listener);
|
||||
void droiddeck_color_ratio_sample(float ratio, int listener);
|
||||
/* Milliseconds since an HDR frame last went onto a display layer; -1 = none this session. */
|
||||
int banner_color_last_frame_age_ms(void);
|
||||
int droiddeck_color_last_frame_age_ms(void);
|
||||
/* -1 = the compositor has not decided yet, 0 = closed, 1 = open. */
|
||||
int banner_color_gate_state(void);
|
||||
int droiddeck_color_gate_state(void);
|
||||
/* The session is ending: write the summary line ("HDR on screen: …"). */
|
||||
void banner_color_session_end(void);
|
||||
void droiddeck_color_session_end(void);
|
||||
/* 1 while HDR frames are really on screen: a frame tagged BT2020_PQ reached a display layer or the HDR
|
||||
* swapchain in the last 1.5 s AND, where the display reports an HDR/SDR ratio, its last reading is
|
||||
* above 1.01 (the HUD badge). */
|
||||
int banner_color_hdr_on_screen(void);
|
||||
int droiddeck_color_hdr_on_screen(void);
|
||||
/* The HUD / drawer state: 0 = no HDR frames on screen (or not confirmed yet), 1 = HDR frames on screen
|
||||
* with headroom (as banner_color_hdr_on_screen), 2 = HDR frames on screen but the display has given
|
||||
* with headroom (as droiddeck_color_hdr_on_screen), 2 = HDR frames on screen but the display has given
|
||||
* them NO headroom (ratio <= 1.01) for 5 s or more - the brightness slider at maximum, or a screen
|
||||
* recording (Android turns HDR headroom off while the screen is recorded). */
|
||||
int banner_color_hdr_state(void);
|
||||
int droiddeck_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
|
||||
void droiddeck_color_set_sdr_white(float nits);
|
||||
float droiddeck_color_sdr_white(void);
|
||||
/* The HDR session was asked for (DROIDDECK_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);
|
||||
int droiddeck_color_requested(void);
|
||||
/* The drawer's live "HDR output" switch, per session, starting ON. On = HDR frames go to the display as
|
||||
* HDR; off = the SAME frames are composed tone-mapped to SDR (hdr_compose.h) - the game is told nothing
|
||||
* and keeps rendering HDR (DXVK_HDR and the colour-manager offer were decided at launch). Compositor
|
||||
* thread (the app posts it through banner_host_hdr_output); logs the flip and redraws. */
|
||||
void banner_color_set_output(int on);
|
||||
* thread (the app posts it through droiddeck_host_hdr_output); logs the flip and redraws. */
|
||||
void droiddeck_color_set_output(int on);
|
||||
/* 1 = HDR output on (any thread). */
|
||||
int banner_color_output(void);
|
||||
int droiddeck_color_output(void);
|
||||
/* 1 while an HDR game's frames are being shown tone-mapped to SDR (the last one < 1.5 s ago); any thread. */
|
||||
int banner_color_tonemapped_on_screen(void);
|
||||
int droiddeck_color_tonemapped_on_screen(void);
|
||||
/* Device evidence beside the HDR/SDR headroom (the app, non-root APIs; any thread; HDR sessions only):
|
||||
* PowerManager thermal status (0..6, -1 unknown) + getThermalHeadroom(10) (< 0 = not available),
|
||||
* Settings.System SCREEN_BRIGHTNESS (0..255, -1 unknown) + SCREEN_BRIGHTNESS_MODE (1 auto, 0 manual).
|
||||
* Changes of status and brightness are logged; every no-headroom line, the 10 s line and the verdict
|
||||
* carry the last values. */
|
||||
void banner_color_env_sample(int thermal, float headroom, int brightness, int bmode);
|
||||
void droiddeck_color_env_sample(int thermal, float headroom, int brightness, int bmode);
|
||||
|
||||
/* ---- the explicit HDR headroom request (Android 15+: ASurfaceTransaction_setDesiredHdrHeadroom on the game
|
||||
* layer, SurfaceView.setDesiredHdrHeadroom on the screen surface). Some phones only boost HDR when asked. */
|
||||
/* The ratio to ask for an HDR frame of `c`: content peak (max CLL, else mastering max, else the display's
|
||||
* peak) / SDR white (203), capped at the display's highest ratio when it reports one; 0 = ask nothing
|
||||
* (not HDR, or the display reports it cannot boost). `why` gets the one-line reason. Any thread. */
|
||||
float banner_color_desired_headroom(const struct banner_color *c, char *why, size_t n);
|
||||
float droiddeck_color_desired_headroom(const struct droiddeck_color *c, char *why, size_t n);
|
||||
/* What was last asked for (> 0 the ratio, 0 nothing, -1 the API is missing): for the no-headroom lines. */
|
||||
void banner_color_note_headroom_request(float ratio);
|
||||
void droiddeck_color_note_headroom_request(float ratio);
|
||||
/* Display.getHighestHdrSdrRatio() (Android 16+; <= 0 = not reported). Any thread; logged when it arrives. */
|
||||
void banner_color_set_highest_ratio(float ratio);
|
||||
void droiddeck_color_set_highest_ratio(float ratio);
|
||||
/* The headroom the SCREEN surface should ask for (frames through the HDR10 swapchain in the last 1.5 s),
|
||||
* 0 = none; `why` gets the reason. The app polls it and applies it. Any thread. */
|
||||
float banner_color_screen_headroom(char *why, size_t n);
|
||||
float droiddeck_color_screen_headroom(char *why, size_t n);
|
||||
|
||||
/* ---- compositor.c -> here */
|
||||
/* Decide the gate (call after ahb_swapchain_init) and create the global when it is open. */
|
||||
void banner_color_init(struct wl_display *display);
|
||||
void droiddeck_color_init(struct wl_display *display);
|
||||
/* The gate is open: the 10-bit dma-buf formats may be advertised, descriptions may exist. */
|
||||
int banner_color_hdr_open(void);
|
||||
int droiddeck_color_hdr_open(void);
|
||||
/* wl_surface.commit: the pending image description (if any request touched it) becomes current. */
|
||||
void banner_color_commit(struct wl_resource *surface);
|
||||
void droiddeck_color_commit(struct wl_resource *surface);
|
||||
/* The surface's current image description; NULL = none (sRGB, today's default). */
|
||||
const struct banner_color *banner_color_of(struct wl_resource *surface);
|
||||
const struct droiddeck_color *droiddeck_color_of(struct wl_resource *surface);
|
||||
/* A program disconnected: if it presented HDR, its part of the summary is written now. */
|
||||
void banner_color_client_gone(struct wl_client *client);
|
||||
void droiddeck_color_client_gone(struct wl_client *client);
|
||||
/* The 10 s summary tick (one `color` line when anything HDR happened in the window). */
|
||||
void banner_color_stats_tick(void);
|
||||
void droiddeck_color_stats_tick(void);
|
||||
/* A frame of an HDR-described surface went through the compositor's own 8-bit sRGB pass this scene
|
||||
* (who = the window, reason = why it did not get the display layer). Shown untone-mapped. */
|
||||
void banner_color_frame_copied(const char *who, const char *reason);
|
||||
void droiddeck_color_frame_copied(const char *who, const char *reason);
|
||||
|
||||
/* ---- sc_layer.c / compositor.c -> here: a NEW frame of an HDR-described surface was shown, and how */
|
||||
enum banner_hdr_path {
|
||||
BANNER_HDR_ZERO_COPY = 0, /* the game's own gralloc buffer on its display layer, tagged */
|
||||
BANNER_HDR_LAYER_COPY, /* one 8-bit copy of the game's frame on its layer, tagged */
|
||||
BANNER_HDR_COMPOSED, /* the whole scene composed into one PQ picture on the game layer (hdr_compose.h) */
|
||||
BANNER_HDR_SWAPCHAIN, /* composed into PQ and presented through an HDR10 swapchain (frame generation) */
|
||||
BANNER_HDR_TONEMAPPED, /* composed and tone-mapped to SDR (HDR output switched off, or a present that
|
||||
enum droiddeck_hdr_path {
|
||||
DROIDDECK_HDR_ZERO_COPY = 0, /* the game's own gralloc buffer on its display layer, tagged */
|
||||
DROIDDECK_HDR_LAYER_COPY, /* one 8-bit copy of the game's frame on its layer, tagged */
|
||||
DROIDDECK_HDR_COMPOSED, /* the whole scene composed into one PQ picture on the game layer (hdr_compose.h) */
|
||||
DROIDDECK_HDR_SWAPCHAIN, /* composed into PQ and presented through an HDR10 swapchain (frame generation) */
|
||||
DROIDDECK_HDR_TONEMAPPED, /* composed and tone-mapped to SDR (HDR output switched off, or a present that
|
||||
* cannot carry HDR) - NOT HDR */
|
||||
};
|
||||
/* ahb_format = the buffer's AHARDWAREBUFFER_FORMAT_* where one exists (0 otherwise). */
|
||||
void banner_color_frame_shown(const struct banner_color *c, int path, uint32_t ahb_format);
|
||||
void droiddeck_color_frame_shown(const struct droiddeck_color *c, int path, uint32_t ahb_format);
|
||||
/* "RGBA1010102 (10-bit)" etc. for an AHARDWAREBUFFER_FORMAT_* value (static buffer). */
|
||||
const char *banner_ahb_format_name(uint32_t format);
|
||||
const char *droiddeck_ahb_format_name(uint32_t format);
|
||||
|
||||
#endif
|
||||
+13
-13
@@ -2,7 +2,7 @@
|
||||
* Host → compositor message queue plus the init of the extension modules.
|
||||
*
|
||||
* 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
|
||||
* droiddeck_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
|
||||
* wl_resources.
|
||||
*/
|
||||
@@ -13,9 +13,9 @@
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <android/log.h>
|
||||
#include "banner_ext.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "ahb_swapchain.h"
|
||||
#include "banner_color.h"
|
||||
#include "droiddeck_color.h"
|
||||
|
||||
enum host_kind { HOST_CLIPBOARD = 1, HOST_TEXT_COMMIT, HOST_TEXT_PREEDIT, HOST_TEXT_DELETE, HOST_ZERO_COPY, HOST_HDR_OUTPUT };
|
||||
|
||||
@@ -53,22 +53,22 @@ static void host_post(enum host_kind kind, const char *text, size_t len, int a,
|
||||
}
|
||||
}
|
||||
|
||||
void banner_host_clipboard_text(const char *utf8, int len) {
|
||||
void droiddeck_host_clipboard_text(const char *utf8, int len) {
|
||||
host_post(HOST_CLIPBOARD, utf8, len > 0 ? (size_t)len : 0, 0, 0);
|
||||
}
|
||||
void banner_host_text_commit(const char *utf8, int len) {
|
||||
void droiddeck_host_text_commit(const char *utf8, int len) {
|
||||
host_post(HOST_TEXT_COMMIT, utf8, len > 0 ? (size_t)len : 0, 0, 0);
|
||||
}
|
||||
void banner_host_text_preedit(const char *utf8, int len, int cursor_begin, int cursor_end) {
|
||||
void droiddeck_host_text_preedit(const char *utf8, int len, int cursor_begin, int cursor_end) {
|
||||
host_post(HOST_TEXT_PREEDIT, utf8, len > 0 ? (size_t)len : 0, cursor_begin, cursor_end);
|
||||
}
|
||||
void banner_host_text_delete(int before, int after) {
|
||||
void droiddeck_host_text_delete(int before, int after) {
|
||||
host_post(HOST_TEXT_DELETE, NULL, 0, before, after);
|
||||
}
|
||||
void banner_host_zero_copy(int on, int live) {
|
||||
void droiddeck_host_zero_copy(int on, int live) {
|
||||
host_post(HOST_ZERO_COPY, NULL, 0, on, live);
|
||||
}
|
||||
void banner_host_hdr_output(int on) {
|
||||
void droiddeck_host_hdr_output(int on) {
|
||||
host_post(HOST_HDR_OUTPUT, NULL, 0, on, 0);
|
||||
}
|
||||
|
||||
@@ -99,17 +99,17 @@ static int on_wake(int fd, uint32_t mask, void *data) {
|
||||
ahb_swapchain_set_mode(m->a, m->b);
|
||||
break;
|
||||
case HOST_HDR_OUTPUT:
|
||||
banner_color_set_output(m->a);
|
||||
droiddeck_color_set_output(m->a);
|
||||
break;
|
||||
}
|
||||
free(m->text);
|
||||
free(m);
|
||||
}
|
||||
wl_display_flush_clients(banner_get_display());
|
||||
wl_display_flush_clients(droiddeck_get_display());
|
||||
return 0;
|
||||
}
|
||||
|
||||
void banner_ext_init(struct wl_display *display) {
|
||||
void droiddeck_ext_init(struct wl_display *display) {
|
||||
struct wl_event_loop *loop = wl_display_get_event_loop(display);
|
||||
if (pipe2(g_wake, O_CLOEXEC | O_NONBLOCK) == 0) {
|
||||
wl_event_loop_add_fd(loop, g_wake[0], WL_EVENT_READABLE, on_wake, NULL);
|
||||
@@ -118,7 +118,7 @@ void banner_ext_init(struct wl_display *display) {
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (queued) { char c = 1; ssize_t n = write(g_wake[1], &c, 1); (void)n; }
|
||||
} else
|
||||
__android_log_print(ANDROID_LOG_ERROR, "BannerWayland", "host queue pipe failed");
|
||||
__android_log_print(ANDROID_LOG_ERROR, "DroidDeckWayland", "host queue pipe failed");
|
||||
clipboard_init(display);
|
||||
text_input_init(display);
|
||||
toplevel_icon_init(display);
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Interface between compositor.c and the extension modules that live in their own files:
|
||||
* wl_clipboard.c wl_data_device_manager + zwlr_data_control_manager_v1 (selection only)
|
||||
* wl_text_input.c zwp_text_input_manager_v3 (host IME / soft keyboard)
|
||||
* wl_toplevel_icon.c xdg_toplevel_icon_manager_v1 (accepted, not rendered)
|
||||
* droiddeck_ext.c the host→compositor message queue shared by the modules
|
||||
* Everything here runs on the compositor (dispatch) thread unless noted.
|
||||
*/
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <wayland-server.h>
|
||||
|
||||
void droiddeck_log(const char *tag, const char *fmt, ...);
|
||||
|
||||
/* ---- compositor.c → modules */
|
||||
void droiddeck_ext_init(struct wl_display *display); /* creates the globals + host queue */
|
||||
void droiddeck_clipboard_keyboard_focus(struct wl_client *client); /* keyboard focus moved; NULL = none */
|
||||
void droiddeck_text_input_refocus(void); /* the text-input target may have changed */
|
||||
void droiddeck_text_input_surface_gone(struct wl_resource *surface);
|
||||
|
||||
/* ---- modules → compositor.c */
|
||||
const char *droiddeck_client_name(struct wl_client *client);
|
||||
struct wl_display *droiddeck_get_display(void);
|
||||
/* The surface text input follows (the last clicked window, else the topmost program window). */
|
||||
struct wl_resource *droiddeck_ime_target(void);
|
||||
/* Scene (virtual desktop) origin of a toplevel surface; 0,0 when unknown. */
|
||||
void droiddeck_surface_scene_origin(struct wl_resource *surface, int *x, int *y);
|
||||
/* Deliver a key press/release through the wl_keyboard path as if typed. */
|
||||
void droiddeck_inject_key(uint32_t evdev, int pressed);
|
||||
|
||||
/* ---- modules → app (JNI upcalls; waylandcomp_jni.c) */
|
||||
void droiddeck_on_clipboard_text(const char *utf8, int len);
|
||||
void droiddeck_on_text_input(int enabled, const char *program, int x, int y, int w, int h);
|
||||
|
||||
/* ---- app → modules (callable from any thread; queued to the compositor thread) */
|
||||
void droiddeck_host_clipboard_text(const char *utf8, int len); /* len 0 = clear */
|
||||
void droiddeck_host_text_commit(const char *utf8, int len);
|
||||
void droiddeck_host_text_preedit(const char *utf8, int len, int cursor_begin, int cursor_end);
|
||||
void droiddeck_host_text_delete(int before, int after);
|
||||
void droiddeck_host_zero_copy(int on, int live); /* the drawer's zero-copy switch (ahb_swapchain_set_mode) */
|
||||
void droiddeck_host_hdr_output(int on); /* the drawer's HDR output switch (droiddeck_color_set_output) */
|
||||
|
||||
/* ---- module internals reached through the queue (droiddeck_ext.c → modules) */
|
||||
void clipboard_init(struct wl_display *display);
|
||||
void clipboard_host_text(char *utf8, size_t len); /* takes ownership of utf8 */
|
||||
void text_input_init(struct wl_display *display);
|
||||
void text_input_host_commit(const char *utf8, size_t len);
|
||||
void text_input_host_preedit(const char *utf8, size_t len, int cursor_begin, int cursor_end);
|
||||
void text_input_host_delete(int before, int after);
|
||||
void toplevel_icon_init(struct wl_display *display);
|
||||
@@ -2,7 +2,7 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "effects_chain.h"
|
||||
#include "vk_loader.h"
|
||||
#include "vk_present.h" /* banner_log */
|
||||
#include "vk_present.h" /* droiddeck_log */
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -115,7 +115,7 @@ static void log_settings(const struct fx_settings *s) {
|
||||
char line[400];
|
||||
int n;
|
||||
if (!spatial_mode(s->scaling) && !effect_count(s)) {
|
||||
banner_log("effects", "all off: scaling=%s (plain blit)", scaling_name(s->scaling));
|
||||
droiddeck_log("effects", "all off: scaling=%s (plain blit)", scaling_name(s->scaling));
|
||||
return;
|
||||
}
|
||||
n = snprintf(line, sizeof(line), "scaling=%s", scaling_name(s->scaling));
|
||||
@@ -133,7 +133,7 @@ static void log_settings(const struct fx_settings *s) {
|
||||
if (s->crt) n += snprintf(line + n, sizeof(line) - n, ", CRT on");
|
||||
if (s->deband_on) n += snprintf(line + n, sizeof(line) - n, ", Deband on %d%%", s->deband_pct);
|
||||
(void)n;
|
||||
banner_log("effects", "%s", line);
|
||||
droiddeck_log("effects", "%s", line);
|
||||
}
|
||||
|
||||
int vkp_effects_sync(void) {
|
||||
@@ -262,7 +262,7 @@ static int target_ensure(struct fx_target *t, int w, int h, const char *what) {
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
VkResult r = g_vk.CreateImage(g_dev, &ii, NULL, &t->img);
|
||||
if (r != VK_SUCCESS) { banner_log("error", "effects: %s image %dx%d: vkCreateImage %d", what, w, h, (int)r); return -1; }
|
||||
if (r != VK_SUCCESS) { droiddeck_log("error", "effects: %s image %dx%d: vkCreateImage %d", what, w, h, (int)r); return -1; }
|
||||
VkMemoryRequirements req;
|
||||
g_vk.GetImageMemoryRequirements(g_dev, t->img, &req);
|
||||
int idx = memtype(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
@@ -271,7 +271,7 @@ static int target_ensure(struct fx_target *t, int w, int h, const char *what) {
|
||||
.memoryTypeIndex = (uint32_t)(idx < 0 ? 0 : idx)};
|
||||
r = g_vk.AllocateMemory(g_dev, &mai, NULL, &t->mem);
|
||||
if (r != VK_SUCCESS) {
|
||||
banner_log("error", "effects: %s image %dx%d (%llu bytes): vkAllocateMemory %d", what, w, h,
|
||||
droiddeck_log("error", "effects: %s image %dx%d (%llu bytes): vkAllocateMemory %d", what, w, h,
|
||||
(unsigned long long)req.size, (int)r);
|
||||
target_destroy(t); return -1;
|
||||
}
|
||||
@@ -312,7 +312,7 @@ static VkShaderModule shader(const uint32_t *code, size_t size) {
|
||||
* 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; }
|
||||
if (!frag) { droiddeck_log("error", "effects: %s: vkCreateShaderModule failed", name); return VK_NULL_HANDLE; }
|
||||
VkPipelineShaderStageCreateInfo st[2] = {
|
||||
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_VERTEX_BIT, .module = vert, .pName = "main"},
|
||||
{.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_FRAGMENT_BIT, .module = frag, .pName = "main"}};
|
||||
@@ -340,7 +340,7 @@ static VkPipeline post_pipeline(VkShaderModule vert, const uint32_t *frag_code,
|
||||
VkPipeline p = VK_NULL_HANDLE;
|
||||
VkResult r = g_vk.CreateGraphicsPipelines(g_dev, VK_NULL_HANDLE, 1, &pi, NULL, &p);
|
||||
g_vk.DestroyShaderModule(g_dev, frag, NULL);
|
||||
if (r != VK_SUCCESS) { banner_log("error", "effects: %s: vkCreateGraphicsPipelines %d", name, (int)r); return VK_NULL_HANDLE; }
|
||||
if (r != VK_SUCCESS) { droiddeck_log("error", "effects: %s: vkCreateGraphicsPipelines %d", name, (int)r); return VK_NULL_HANDLE; }
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ void vkp_effects_set_formats(VkFormat scene_fmt, VkFormat target_fmt) {
|
||||
g_fx_fmt = target_fmt;
|
||||
g_scene_fmt = scene_fmt;
|
||||
if (g_dev)
|
||||
banner_log("effects", "chain now works in %s (%s)",
|
||||
droiddeck_log("effects", "chain now works in %s (%s)",
|
||||
target_fmt == FX_FORMAT ? "8-bit RGBA"
|
||||
: target_fmt == VK_FORMAT_R16G16B16A16_SFLOAT ? "FP16 RGBA" : "10-bit RGB",
|
||||
target_fmt == FX_FORMAT ? "SDR frames"
|
||||
@@ -383,7 +383,7 @@ void vkp_effects_set_formats(VkFormat scene_fmt, VkFormat target_fmt) {
|
||||
* unavailable for this session (logged once; frames fall back to the plain blit). */
|
||||
static int objects_ensure(void) {
|
||||
if (g_ready) return g_ready == 1 ? 0 : -1;
|
||||
if (!g_dev || !g_vk.CreateGraphicsPipelines) { g_ready = -1; banner_log("error", "effects: no device for the chain"); return -1; }
|
||||
if (!g_dev || !g_vk.CreateGraphicsPipelines) { g_ready = -1; droiddeck_log("error", "effects: no device for the chain"); return -1; }
|
||||
/* Render pass: one colour target, overwritten whole (DONT_CARE), left SHADER_READ_ONLY for the
|
||||
* next pass. The dependencies order this pass after whoever read the target last (previous
|
||||
* pass / previous frame's blit) and before whoever samples or blits it next. */
|
||||
@@ -448,12 +448,12 @@ static int objects_ensure(void) {
|
||||
#undef MK
|
||||
g_vk.DestroyShaderModule(g_dev, vert, NULL);
|
||||
g_ready = 1;
|
||||
banner_log("effects", "chain ready: 13 passes (SGSR, SGSR HQ, NIS, FSR EASU+RCAS, CAS, colour, FXAA, Toon, HDR, NTSC, CRT, deband) on %s", vkp_gpu_name());
|
||||
droiddeck_log("effects", "chain ready: 13 passes (SGSR, SGSR HQ, NIS, FSR EASU+RCAS, CAS, colour, FXAA, Toon, HDR, NTSC, CRT, deband) on %s", vkp_gpu_name());
|
||||
return 0;
|
||||
fail:
|
||||
objects_destroy();
|
||||
g_ready = -1;
|
||||
banner_log("error", "effects: the screen-effect chain could not be built on this driver; effects are off for this session");
|
||||
droiddeck_log("error", "effects: the screen-effect chain could not be built on this driver; effects are off for this session");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,15 @@
|
||||
#include <time.h>
|
||||
#include <android/log.h>
|
||||
|
||||
void banner_log(const char *tag, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
#define FGLOG(...) banner_log("framegen", __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "BannerWayland", __VA_ARGS__)
|
||||
void droiddeck_log(const char *tag, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
#define FGLOG(...) droiddeck_log("framegen", __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "DroidDeckWayland", __VA_ARGS__)
|
||||
|
||||
/* ---- app controls (written from any thread, read on the compositor thread) ------------- */
|
||||
static _Atomic int g_kind = VKP_FG_ENGINE_LSFG;
|
||||
static _Atomic int g_armed;
|
||||
static _Atomic int g_mult = 2;
|
||||
static _Atomic int g_target; /* LSFG adaptive fps, 0 = fixed multiplier */
|
||||
static _Atomic int g_model = 3;
|
||||
static _Atomic int g_preset = 2;
|
||||
static _Atomic int g_cfg_dirty = 1;
|
||||
@@ -51,6 +52,7 @@ static struct {
|
||||
|
||||
/* ---- engine + generation ring (compositor thread) ------------------------------------ */
|
||||
static int g_engine_kind = -1; /* kind of the running engine, -1 = none */
|
||||
static int g_engine_cache_gen = -1; /* the LSFG cache it loaded */
|
||||
static int g_engine_ok;
|
||||
/* The engine of this kind (with this cache) could not start or build its chain; not retried
|
||||
* every frame - a new engine selection or a new cache clears it. */
|
||||
@@ -92,15 +94,20 @@ static int64_t now_ns(void) {
|
||||
void vkp_framegen_set_engine(int kind) {
|
||||
if (kind != VKP_FG_ENGINE_LSFG && kind != VKP_FG_ENGINE_WINFG) kind = VKP_FG_ENGINE_LSFG;
|
||||
atomic_store(&g_kind, kind);
|
||||
atomic_store(&g_failed_kind, -1);
|
||||
atomic_store(&g_cfg_dirty, 1);
|
||||
}
|
||||
|
||||
void vkp_framegen_set_armed(int armed, int multiplier) {
|
||||
void vkp_framegen_set_armed(int armed, int multiplier, int target_fps) {
|
||||
if (target_fps < 0) target_fps = 0;
|
||||
/* Adaptive sizes the ring and the swapchain for the most it may ever generate. */
|
||||
if (target_fps) multiplier = VKP_FG_MAX_GENERATIONS + 1;
|
||||
if (multiplier < 2) multiplier = 2;
|
||||
if (multiplier > VKP_FG_MAX_GENERATIONS + 1) multiplier = VKP_FG_MAX_GENERATIONS + 1;
|
||||
if (atomic_exchange(&g_mult, multiplier) != multiplier) atomic_store(&g_cfg_dirty, 1);
|
||||
if (atomic_exchange(&g_target, target_fps) != target_fps) atomic_store(&g_cfg_dirty, 1);
|
||||
atomic_store(&g_armed, armed ? 1 : 0);
|
||||
LOGD("framegen: set_armed(%d, x%d)", armed, multiplier);
|
||||
LOGD("framegen: set_armed(%d, x%d, target %d fps)", armed, multiplier, target_fps);
|
||||
}
|
||||
|
||||
void vkp_framegen_set_lsfg_cache_path(const char *path) {
|
||||
@@ -285,7 +292,9 @@ int vkp_framegen_extra_images(void) {
|
||||
}
|
||||
|
||||
static int ensure_engine(int kind) {
|
||||
if (g_engine_ok && g_engine_kind == kind) return 1;
|
||||
const int cache_gen = atomic_load(&g_cache_gen);
|
||||
if (g_engine_ok && g_engine_kind == kind && (kind != VKP_FG_ENGINE_LSFG || g_engine_cache_gen == cache_gen))
|
||||
return 1;
|
||||
if (start_failed_for(kind)) return 0;
|
||||
if (g_engine_ok) { destroy_ring(); fge_stop(); g_engine_ok = 0; g_engine_kind = -1; }
|
||||
char *path = NULL;
|
||||
@@ -293,7 +302,7 @@ static int ensure_engine(int kind) {
|
||||
if (g_cache_path) path = strdup(g_cache_path);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
if (kind == VKP_FG_ENGINE_LSFG && !path) {
|
||||
FGLOG("LSFG Native can't start: no shader cache (import Lossless.dll in Settings)");
|
||||
FGLOG("LSFG Native can't start: no shader cache from Lossless.dll");
|
||||
mark_failed(kind);
|
||||
return 0;
|
||||
}
|
||||
@@ -305,29 +314,39 @@ static int ensure_engine(int kind) {
|
||||
mark_failed(kind);
|
||||
return 0;
|
||||
}
|
||||
g_engine_ok = 1; g_engine_kind = kind;
|
||||
g_engine_ok = 1; g_engine_kind = kind; g_engine_cache_gen = cache_gen;
|
||||
atomic_store(&g_cfg_dirty, 1);
|
||||
g_built_w = g_built_h = 0;
|
||||
FGLOG("%s engine ready (%s)", fge_engine_name(kind), fge_build_info());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int g_logged_mult;
|
||||
/* "x3 (2 interpolated frames per game frame)" or "adaptive to 90 fps (up to 3 ...)". */
|
||||
static const char *describe_mode(char *buf, size_t n, int mult, int target) {
|
||||
if (target) snprintf(buf, n, "adaptive to %d fps (up to %d interpolated frames per game frame)", target, mult - 1);
|
||||
else snprintf(buf, n, "x%d (%d interpolated frame%s per game frame)", mult, mult - 1, mult == 2 ? "" : "s");
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int g_logged_mult, g_logged_target;
|
||||
static void log_arm_transition(int armed, int kind, int mult) {
|
||||
const int target = atomic_load(&g_target);
|
||||
char mode[96];
|
||||
if (armed == g_was_armed) {
|
||||
if (armed && mult != g_logged_mult) {
|
||||
g_logged_mult = mult;
|
||||
FGLOG("%s multiplier changed to x%d (%d interpolated frames per game frame)", fge_engine_name(kind), mult, mult - 1);
|
||||
if (armed && (mult != g_logged_mult || target != g_logged_target)) {
|
||||
g_logged_mult = mult; g_logged_target = target;
|
||||
FGLOG("%s now %s", fge_engine_name(kind), describe_mode(mode, sizeof(mode), mult, target));
|
||||
}
|
||||
return;
|
||||
}
|
||||
g_was_armed = armed;
|
||||
if (armed) {
|
||||
g_logged_mult = mult;
|
||||
g_logged_mult = mult; g_logged_target = target;
|
||||
float flow, hz;
|
||||
pthread_mutex_lock(&g_lock); flow = g_flow; hz = g_refresh_hz; pthread_mutex_unlock(&g_lock);
|
||||
FGLOG("%s x%d armed (flow scale %.2f, panel %.0f Hz): real frames are presented one slot late "
|
||||
"with the interpolated frames ahead of them", fge_engine_name(kind), mult, (double)flow, (double)hz);
|
||||
FGLOG("%s %s armed (flow scale %.2f, panel %.0f Hz): real frames are presented one slot late "
|
||||
"with the interpolated frames ahead of them", fge_engine_name(kind),
|
||||
describe_mode(mode, sizeof(mode), mult, target), (double)flow, (double)hz);
|
||||
} else {
|
||||
FGLOG("frame generation off (%llu frames generated this session)", (unsigned long long)g_total_generated);
|
||||
g_generating_logged = 0;
|
||||
@@ -348,7 +367,8 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view,
|
||||
if (atomic_exchange(&g_cfg_dirty, 0)) {
|
||||
float flow, hz;
|
||||
pthread_mutex_lock(&g_lock); flow = g_flow; hz = g_refresh_hz; pthread_mutex_unlock(&g_lock);
|
||||
fge_configure((uint32_t)mult, flow, hz, atomic_load(&g_model), atomic_load(&g_preset));
|
||||
fge_configure((uint32_t)mult, (uint32_t)atomic_load(&g_target), flow, hz,
|
||||
atomic_load(&g_model), atomic_load(&g_preset));
|
||||
}
|
||||
if (!fge_prepare((uint32_t)w, (uint32_t)h, fmt)) {
|
||||
if (fmt != VK_FORMAT_R8G8B8A8_UNORM && fge_unavailable()) {
|
||||
@@ -426,8 +446,9 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view,
|
||||
0, 0, NULL, 0, NULL, nb, bars);
|
||||
if (!g_generating_logged) {
|
||||
g_generating_logged = 1;
|
||||
FGLOG("generating: %s x%d at %dx%d (%u interpolated frame%s per game frame%s)",
|
||||
fge_engine_name(kind), mult, w, h, n_gen, n_gen == 1 ? "" : "s",
|
||||
char mode[96];
|
||||
FGLOG("generating: %s %s at %dx%d%s", fge_engine_name(kind),
|
||||
describe_mode(mode, sizeof(mode), mult, atomic_load(&g_target)), w, h,
|
||||
fmt == VK_FORMAT_R16G16B16A16_SFLOAT ? ", FP16: the HDR picture" : "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,10 @@ enum { VKP_FG_ENGINE_LSFG = 0, VKP_FG_ENGINE_WINFG = 1 };
|
||||
|
||||
/* ---- app controls (any thread) ------------------------------------------------------- */
|
||||
void vkp_framegen_set_engine(int kind);
|
||||
/* multiplier 2..4; armed = 0 stops generating (the ring and the engine stay for a re-arm). */
|
||||
void vkp_framegen_set_armed(int armed, int multiplier);
|
||||
/* LSFG Native: the SPIR-V cache built from the user's Lossless.dll (LsfgNative.ensureCache). */
|
||||
/* multiplier 2..4, or target_fps > 0 for LSFG's adaptive mode (up to 4x, as the target needs);
|
||||
* armed = 0 stops generating (the ring and the engine stay for a re-arm). */
|
||||
void vkp_framegen_set_armed(int armed, int multiplier, int target_fps);
|
||||
/* LSFG Native: the SPIR-V cache built from the user's Lossless.dll (Lossless.sync); a new path reloads the engine. */
|
||||
void vkp_framegen_set_lsfg_cache_path(const char *path);
|
||||
/* Flow scale 0.25..1.0 and the panel's real refresh rate (the pacer never generates above it). */
|
||||
void vkp_framegen_set_tuning(float flow_scale, float refresh_hz);
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
#include "lsfg/lsfg_vkd.h"
|
||||
#include "winfg/winfg_engine.h"
|
||||
|
||||
#define ELOG(...) __android_log_print(ANDROID_LOG_INFO, "BannerWayland", "framegen: " __VA_ARGS__)
|
||||
#define ELOGE(...) __android_log_print(ANDROID_LOG_ERROR, "BannerWayland", "framegen: " __VA_ARGS__)
|
||||
#define ELOG(...) __android_log_print(ANDROID_LOG_INFO, "DroidDeckWayland", "framegen: " __VA_ARGS__)
|
||||
#define ELOGE(...) __android_log_print(ANDROID_LOG_ERROR, "DroidDeckWayland", "framegen: " __VA_ARGS__)
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -197,11 +197,11 @@ int fge_unavailable(void) {
|
||||
return g_lsfg && g_lsfg->unavailable() ? 1 : 0;
|
||||
}
|
||||
|
||||
void fge_configure(uint32_t multiplier, float flow_scale, float refresh_hz, int model,
|
||||
int perf_preset) {
|
||||
void fge_configure(uint32_t multiplier, uint32_t target_fps, float flow_scale, float refresh_hz,
|
||||
int model, int perf_preset) {
|
||||
if (multiplier < 2) multiplier = 2;
|
||||
if (g_kind == 1 && g_winfg) g_winfg->configure(multiplier, model, perf_preset, flow_scale);
|
||||
else if (g_lsfg) g_lsfg->configure(multiplier, 0, flow_scale, refresh_hz);
|
||||
else if (g_lsfg) g_lsfg->configure(multiplier, target_fps, flow_scale, refresh_hz);
|
||||
}
|
||||
|
||||
int fge_prepare(uint32_t w, uint32_t h, VkFormat fmt) {
|
||||
|
||||
@@ -45,8 +45,9 @@ int fge_start(int kind, const char *cache_path);
|
||||
void fge_stop(void);
|
||||
/* The engine came up but later found it cannot generate (chain build failed). */
|
||||
int fge_unavailable(void);
|
||||
void fge_configure(uint32_t multiplier, float flow_scale, float refresh_hz, int model,
|
||||
int perf_preset);
|
||||
/* target_fps > 0: LSFG picks 0..3 generations per frame to reach it (Win-FG ignores it). */
|
||||
void fge_configure(uint32_t multiplier, uint32_t target_fps, float flow_scale, float refresh_hz,
|
||||
int model, int perf_preset);
|
||||
/* Build/rebuild the chain for this extent; the guest extent for the flow pyramid is the same
|
||||
* (the Wayland scene is the game's own size in fullscreen). 1 = chain valid. */
|
||||
int fge_prepare(uint32_t w, uint32_t h, VkFormat fmt);
|
||||
|
||||
@@ -17,8 +17,9 @@ int fge_format_ok(VkFormat fmt) { (void)fmt; return 0; }
|
||||
int fge_start(int kind, const char *cache_path) { (void)kind; (void)cache_path; return -1; }
|
||||
void fge_stop(void) {}
|
||||
int fge_unavailable(void) { return 1; }
|
||||
void fge_configure(uint32_t multiplier, float flow_scale, float refresh_hz, int model, int perf_preset) {
|
||||
(void)multiplier; (void)flow_scale; (void)refresh_hz; (void)model; (void)perf_preset;
|
||||
void fge_configure(uint32_t multiplier, uint32_t target_fps, float flow_scale, float refresh_hz, int model,
|
||||
int perf_preset) {
|
||||
(void)multiplier; (void)target_fps; (void)flow_scale; (void)refresh_hz; (void)model; (void)perf_preset;
|
||||
}
|
||||
int fge_prepare(uint32_t w, uint32_t h, VkFormat fmt) { (void)w; (void)h; (void)fmt; return 0; }
|
||||
uint32_t fge_plan(uint32_t capacity, uint64_t source_frames, float presented_rate) {
|
||||
|
||||
@@ -14,9 +14,9 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFrameGenEngine(JN
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFrameGenArmed(JNIEnv *env, jclass clazz,
|
||||
jboolean armed, jint multiplier) {
|
||||
jboolean armed, jint multiplier, jint targetFps) {
|
||||
(void)env; (void)clazz;
|
||||
vkp_framegen_set_armed(armed ? 1 : 0, (int)multiplier);
|
||||
vkp_framegen_set_armed(armed ? 1 : 0, (int)multiplier, (int)targetFps);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "hdr_compose.h"
|
||||
#include "vk_loader.h"
|
||||
#include "vk_present.h" /* banner_log, vkp_gpu_name */
|
||||
#include "vk_present.h" /* droiddeck_log, vkp_gpu_name */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -190,7 +190,7 @@ static int objects_ensure(void) {
|
||||
if (!(g_out[i].pipe = make_pipe(g_out[i].rp))) goto fail;
|
||||
}
|
||||
g_ready = 1;
|
||||
banner_log("color", "HDR composition ready on %s: scenes that mix HDR and SDR content are put into one "
|
||||
droiddeck_log("color", "HDR composition ready on %s: scenes that mix HDR and SDR content are put into one "
|
||||
"encoding (PQ BT.2020, or tone-mapped sRGB) instead of being shown washed out", vkp_gpu_name());
|
||||
return 0;
|
||||
fail:
|
||||
@@ -200,7 +200,7 @@ fail:
|
||||
g_dev = keep;
|
||||
}
|
||||
g_ready = -1;
|
||||
banner_log("error", "color: the HDR composition pass could not be built on this driver; HDR scenes that cannot "
|
||||
droiddeck_log("error", "color: the HDR composition pass could not be built on this driver; HDR scenes that cannot "
|
||||
"stay on the game's own layer are shown the old way (washed out) this session");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#define _GNU_SOURCE
|
||||
#include "sc_layer.h"
|
||||
#include "ahb_swapchain.h"
|
||||
#include "banner_color.h"
|
||||
#include "droiddeck_color.h"
|
||||
#include "vk_present.h"
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <android/native_window.h>
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
/* The gralloc buffer handle behind an AHardwareBuffer (AOSP cutils/native_handle.h, which the NDK
|
||||
* does not ship; the layout is a stable ABI: fds first, then ints). */
|
||||
struct banner_native_handle { int version; int numFds; int numInts; int data[]; };
|
||||
struct droiddeck_native_handle { int version; int numFds; int numInts; int data[]; };
|
||||
|
||||
/* ---- libandroid SurfaceControl API (dlsym) ---------------------------------------------------- */
|
||||
|
||||
@@ -44,12 +44,12 @@ typedef struct ASurfaceTransactionStats ASurfaceTransactionStats;
|
||||
typedef void (*sc_complete_fn)(void *context, ASurfaceTransactionStats *stats);
|
||||
|
||||
/* <android/hdr_metadata.h> (NDK, API 29), declared here like the rest of this table: stable ABI. */
|
||||
struct banner_color_xy { float x, y; };
|
||||
struct banner_hdr_smpte2086 {
|
||||
struct banner_color_xy displayPrimaryRed, displayPrimaryGreen, displayPrimaryBlue, whitePoint;
|
||||
struct droiddeck_color_xy { float x, y; };
|
||||
struct droiddeck_hdr_smpte2086 {
|
||||
struct droiddeck_color_xy displayPrimaryRed, displayPrimaryGreen, displayPrimaryBlue, whitePoint;
|
||||
float maxLuminance, minLuminance;
|
||||
};
|
||||
struct banner_hdr_cta861_3 { float maxContentLightLevel, maxFrameAverageLightLevel; };
|
||||
struct droiddeck_hdr_cta861_3 { float maxContentLightLevel, maxFrameAverageLightLevel; };
|
||||
|
||||
static struct {
|
||||
ASurfaceControl *(*createFromWindow)(ANativeWindow *, const char *);
|
||||
@@ -69,11 +69,11 @@ static struct {
|
||||
* the layer simply carries no vote and the app's surface vote is all there is. */
|
||||
void (*setFrameRate)(ASurfaceTransaction *, ASurfaceControl *, float, int8_t);
|
||||
void (*setFrameRateStrategy)(ASurfaceTransaction *, ASurfaceControl *, float, int8_t, int8_t);
|
||||
/* Colour (API 29), optional: only an HDR session ever calls them (banner_color.h's gate needs
|
||||
/* Colour (API 29), optional: only an HDR session ever calls them (droiddeck_color.h's gate needs
|
||||
* setBufferDataSpace; the metadata calls are sent when present). */
|
||||
void (*setBufferDataSpace)(ASurfaceTransaction *, ASurfaceControl *, int32_t);
|
||||
void (*setHdrMetadata_smpte2086)(ASurfaceTransaction *, ASurfaceControl *, const struct banner_hdr_smpte2086 *);
|
||||
void (*setHdrMetadata_cta861_3)(ASurfaceTransaction *, ASurfaceControl *, const struct banner_hdr_cta861_3 *);
|
||||
void (*setHdrMetadata_smpte2086)(ASurfaceTransaction *, ASurfaceControl *, const struct droiddeck_hdr_smpte2086 *);
|
||||
void (*setHdrMetadata_cta861_3)(ASurfaceTransaction *, ASurfaceControl *, const struct droiddeck_hdr_cta861_3 *);
|
||||
/* Not in the public NDK headers (vndk/hardware_buffer.h) but exported by libnativewindow.so on
|
||||
* every device; Mesa's Android WSI calls it for every gralloc buffer it imports. */
|
||||
const void *(*getNativeHandle)(const AHardwareBuffer *);
|
||||
@@ -85,6 +85,7 @@ static struct {
|
||||
|
||||
#define ASC_VISIBILITY_HIDE 0
|
||||
#define ASC_VISIBILITY_SHOW 1
|
||||
#define ASC_TRANSPARENCY_TRANSLUCENT 1
|
||||
#define ASC_TRANSPARENCY_OPAQUE 2
|
||||
|
||||
static int load_api(void) {
|
||||
@@ -94,12 +95,12 @@ static int load_api(void) {
|
||||
if (!nw) nw = dlopen("libnativewindow.so", RTLD_NOW);
|
||||
api.getNativeHandle = nw ? dlsym(nw, "AHardwareBuffer_getNativeHandle") : NULL;
|
||||
if (!api.getNativeHandle) {
|
||||
banner_log("layer", "unavailable: libnativewindow.so has no AHardwareBuffer_getNativeHandle");
|
||||
droiddeck_log("layer", "unavailable: libnativewindow.so has no AHardwareBuffer_getNativeHandle");
|
||||
return -1;
|
||||
}
|
||||
void *lib = dlopen("libandroid.so", RTLD_NOW | RTLD_NOLOAD);
|
||||
if (!lib) lib = dlopen("libandroid.so", RTLD_NOW);
|
||||
if (!lib) { banner_log("layer", "unavailable: dlopen(libandroid.so): %s", dlerror()); return -1; }
|
||||
if (!lib) { droiddeck_log("layer", "unavailable: dlopen(libandroid.so): %s", dlerror()); return -1; }
|
||||
#define SYM(field, name) api.field = dlsym(lib, name)
|
||||
SYM(createFromWindow, "ASurfaceControl_createFromWindow");
|
||||
SYM(release, "ASurfaceControl_release");
|
||||
@@ -124,7 +125,7 @@ static int load_api(void) {
|
||||
if (!api.createFromWindow || !api.release || !api.txCreate || !api.txDelete || !api.txApply ||
|
||||
!api.setBuffer || !api.setZOrder || !api.setVisibility || !api.setGeometry ||
|
||||
!api.setBufferTransparency || !api.reparent || !api.setOnComplete || !api.prevReleaseFence) {
|
||||
banner_log("layer", "unavailable: libandroid.so lacks part of the ASurfaceControl API (Android 10+)");
|
||||
droiddeck_log("layer", "unavailable: libandroid.so lacks part of the ASurfaceControl API (Android 10+)");
|
||||
return -1;
|
||||
}
|
||||
api.state = 1;
|
||||
@@ -213,10 +214,10 @@ static AHardwareBuffer *g_blank;
|
||||
static void layers_init(void) {
|
||||
if (g_layers_ready) return;
|
||||
g_layers_ready = 1;
|
||||
g_layers[SC_LAYER_GAME] = (struct layer){.name = "banner_wayland_game", .z = 1, .pool_n = POOL_MAX,
|
||||
g_layers[SC_LAYER_GAME] = (struct layer){.name = "droiddeck_wayland_game", .z = 1, .pool_n = POOL_MAX,
|
||||
.cur_slot = -1, .votes_rate = 1, .fps_applied = -1.0f, .hr_applied = -1.0f,
|
||||
.ds_applied = -1};
|
||||
g_layers[SC_LAYER_OVERLAY] = (struct layer){.name = "banner_wayland_overlay", .z = 2, .pool_n = 3,
|
||||
g_layers[SC_LAYER_OVERLAY] = (struct layer){.name = "droiddeck_wayland_overlay", .z = 2, .pool_n = 3,
|
||||
.cur_slot = -1, .votes_rate = 0, .fps_applied = -1.0f,
|
||||
.hr_applied = -1.0f, .ds_applied = -1};
|
||||
for (int i = 0; i < SC_LAYER_COUNT; i++)
|
||||
@@ -329,7 +330,7 @@ static void retire_sc(struct layer *l) {
|
||||
if (l->cur_token) ahb_swapchain_layer_released(l->cur_token, -1);
|
||||
api.release(l->sc);
|
||||
}
|
||||
banner_log("layer", "%s: SurfaceControl retired (window %p)", l->name, (void *)l->win);
|
||||
droiddeck_log("layer", "%s: SurfaceControl retired (window %p)", l->name, (void *)l->win);
|
||||
l->sc = NULL; l->win = NULL;
|
||||
l->shown = 0; l->cur_slot = -1; l->cur_token = NULL; l->geo_valid = 0;
|
||||
l->fps_applied = -1.0f; /* the next SurfaceControl carries no vote until it is re-applied */
|
||||
@@ -367,7 +368,7 @@ static void drain_and_free_pools(void) {
|
||||
/* Native-handle sniff, the same one Mesa's u_gralloc fallback uses (u_gralloc_fallback.c): a QTI
|
||||
* gralloc private_handle_t carries the magic 'gmsm' as its first int and the UBWC flag
|
||||
* (PRIV_FLAGS_UBWC_ALIGNED, 0x08000000) in the next one. Returns 0 when the layout is unknown. */
|
||||
static int sniff_modifier(const struct banner_native_handle *h, uint64_t *mod) {
|
||||
static int sniff_modifier(const struct droiddeck_native_handle *h, uint64_t *mod) {
|
||||
const uint32_t gmsm = ('g' << 24) | ('m' << 16) | ('s' << 8) | 'm';
|
||||
if (!h || h->numFds < 1 || h->numInts < 2) return 0;
|
||||
if ((uint32_t)h->data[h->numFds] != gmsm) return 0;
|
||||
@@ -386,7 +387,7 @@ static int g_alloc_failed; /* set by alloc_slot when gralloc or the import refus
|
||||
* TIER_PLAIN the same without the vendor bit (a gralloc that refuses unknown bits; what we asked before).
|
||||
* TIER_LINEAR plus a CPU bit, so the layout is known to be linear even when the handle is unreadable. */
|
||||
enum { TIER_UBWC, TIER_PLAIN, TIER_LINEAR };
|
||||
#define BANNER_AHB_USAGE_VENDOR_UBWC (1ULL << 28) /* AHARDWAREBUFFER_USAGE_VENDOR_0 */
|
||||
#define DROIDDECK_AHB_USAGE_VENDOR_UBWC (1ULL << 28) /* AHARDWAREBUFFER_USAGE_VENDOR_0 */
|
||||
|
||||
static const char *tier_name(int tier) {
|
||||
return tier == TIER_UBWC ? "asked for UBWC" : tier == TIER_PLAIN ? "no UBWC request" : "linear by request (CPU bit)";
|
||||
@@ -403,22 +404,22 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm
|
||||
* composer overlay (the display scans it out). See the tiers above for the rest. */
|
||||
.usage = AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
|
||||
AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY |
|
||||
(tier == TIER_UBWC ? BANNER_AHB_USAGE_VENDOR_UBWC : 0) |
|
||||
(tier == TIER_UBWC ? DROIDDECK_AHB_USAGE_VENDOR_UBWC : 0) |
|
||||
(tier == TIER_LINEAR ? AHARDWAREBUFFER_USAGE_CPU_READ_RARELY : 0)};
|
||||
AHardwareBuffer *ahb = NULL;
|
||||
if (AHardwareBuffer_allocate(&d, &ahb) != 0 || !ahb) {
|
||||
if (tier < TIER_LINEAR) {
|
||||
banner_log("layer", "%s: gralloc refused a %dx%d pool buffer (%s): trying %s", l->name, w, h,
|
||||
droiddeck_log("layer", "%s: gralloc refused a %dx%d pool buffer (%s): trying %s", l->name, w, h,
|
||||
tier_name(tier), tier_name(tier + 1));
|
||||
l->alloc_tier = tier + 1;
|
||||
continue;
|
||||
}
|
||||
banner_log("error", "layer: %s: AHardwareBuffer_allocate %dx%d (format %#x) failed", l->name, w, h, fmt);
|
||||
droiddeck_log("error", "layer: %s: AHardwareBuffer_allocate %dx%d (format %#x) failed", l->name, w, h, fmt);
|
||||
g_alloc_failed = 1;
|
||||
return -1;
|
||||
}
|
||||
AHardwareBuffer_Desc got; AHardwareBuffer_describe(ahb, &got);
|
||||
const struct banner_native_handle *nh = api.getNativeHandle(ahb);
|
||||
const struct droiddeck_native_handle *nh = api.getNativeHandle(ahb);
|
||||
uint64_t mod = MOD_LINEAR;
|
||||
if (!sniff_modifier(nh, &mod)) {
|
||||
/* Not a handle we can read: the only layout we can assume is linear, and only if the buffer
|
||||
@@ -426,7 +427,7 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm
|
||||
if (tier < TIER_LINEAR) {
|
||||
AHardwareBuffer_release(ahb);
|
||||
l->alloc_tier = TIER_LINEAR;
|
||||
banner_log("layer", "%s: gralloc handle layout unknown (%d fds, %d ints): using linear pool buffers",
|
||||
droiddeck_log("layer", "%s: gralloc handle layout unknown (%d fds, %d ints): using linear pool buffers",
|
||||
l->name, nh ? nh->numFds : -1, nh ? nh->numInts : -1);
|
||||
continue;
|
||||
}
|
||||
@@ -438,7 +439,7 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm
|
||||
struct vkp_image *img = fd >= 0 ? vkp_image_import_dmabuf(fd, fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888,
|
||||
mod, w, h, got.stride * 4, 0, 1) : NULL;
|
||||
if (!img) {
|
||||
banner_log("layer", "%s: import of a %s %dx%d pool buffer (stride %u px) into the compositor's Turnip failed",
|
||||
droiddeck_log("layer", "%s: import of a %s %dx%d pool buffer (stride %u px) into the compositor's Turnip failed",
|
||||
l->name, mod == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", w, h, got.stride);
|
||||
AHardwareBuffer_release(ahb);
|
||||
if (tier < TIER_LINEAR) { l->alloc_tier = tier + 1; continue; }
|
||||
@@ -450,7 +451,7 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm
|
||||
if (l->logged_w != w || l->logged_h != h || l->logged_fmt != fmt || l->logged_mod != mod ||
|
||||
l->logged_tier != tier) {
|
||||
l->logged_w = w; l->logged_h = h; l->logged_fmt = fmt; l->logged_mod = mod; l->logged_tier = tier;
|
||||
banner_log("layer", "%s: pool buffers %dx%d%s: %s (%s), stride %u px, up to %d buffers (gralloc handle %d fds / %d ints)",
|
||||
droiddeck_log("layer", "%s: pool buffers %dx%d%s: %s (%s), stride %u px, up to %d buffers (gralloc handle %d fds / %d ints)",
|
||||
l->name, w, h, fmt == AHB_RGB10A2 ? " 10-bit RGBA1010102" : "",
|
||||
mod == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)" : "linear", tier_name(tier),
|
||||
got.stride, l->pool_n, nh ? nh->numFds : -1, nh ? nh->numInts : -1);
|
||||
@@ -531,10 +532,10 @@ void sc_layer_probe_dmabuf_fd(int fd) {
|
||||
struct { uint32_t flags; int32_t fd; } exp = {.flags = 1u /* DMA_BUF_SYNC_READ */, .fd = -1};
|
||||
/* DMA_BUF_IOCTL_EXPORT_SYNC_FILE = _IOWR('b', 2, struct dma_buf_export_sync_file) */
|
||||
if (ioctl(fd, _IOWR('b', 2, exp), &exp) == 0) {
|
||||
banner_log("layer", "kernel exports sync_file fences from the game's dma-buf: zero-copy acquire fences can come from the buffer itself");
|
||||
droiddeck_log("layer", "kernel exports sync_file fences from the game's dma-buf: zero-copy acquire fences can come from the buffer itself");
|
||||
if (exp.fd >= 0) close(exp.fd);
|
||||
} else {
|
||||
banner_log("layer", "DMA_BUF_IOCTL_EXPORT_SYNC_FILE on the game's dma-buf failed (%s): acquire fences must be exported by the game's driver (sync_fd) instead",
|
||||
droiddeck_log("layer", "DMA_BUF_IOCTL_EXPORT_SYNC_FILE on the game's dma-buf failed (%s): acquire fences must be exported by the game's driver (sync_fd) instead",
|
||||
strerror(errno));
|
||||
}
|
||||
}
|
||||
@@ -548,7 +549,7 @@ static int ensure_sc(struct layer *l) {
|
||||
if (l->sc && l->win != win) retire_sc(l);
|
||||
if (!l->sc) {
|
||||
l->sc = api.createFromWindow(win, l->name);
|
||||
if (!l->sc) { banner_log("error", "layer: ASurfaceControl_createFromWindow(%s) failed", l->name); return -1; }
|
||||
if (!l->sc) { droiddeck_log("error", "layer: ASurfaceControl_createFromWindow(%s) failed", l->name); return -1; }
|
||||
l->win = win;
|
||||
l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f;
|
||||
ASurfaceTransaction *tx = api.txCreate();
|
||||
@@ -558,7 +559,7 @@ static int ensure_sc(struct layer *l) {
|
||||
apply_frame_rate(tx, l);
|
||||
api.txApply(tx); api.txDelete(tx);
|
||||
}
|
||||
banner_log("layer", "SurfaceControl \"%s\" created as a child of the screen surface (z=%d)", l->name, (int)l->z);
|
||||
droiddeck_log("layer", "SurfaceControl \"%s\" created as a child of the screen surface (z=%d)", l->name, (int)l->z);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -584,7 +585,7 @@ static void apply_geometry(ASurfaceTransaction *tx, struct layer *l, const int r
|
||||
l->geo_src = srcR; l->geo_dst = dstR; l->geo_valid = 1;
|
||||
char cov[96] = "";
|
||||
if (l->ds_applied > 0) coverage_text(l, cov, sizeof(cov));
|
||||
banner_log("layer", "%s geometry: buffer %d,%d-%d,%d -> screen %d,%d-%d,%d%s%s%s", l->name,
|
||||
droiddeck_log("layer", "%s geometry: buffer %d,%d-%d,%d -> screen %d,%d-%d,%d%s%s%s", l->name,
|
||||
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], cov[0] ? " (HDR layer " : "", cov, cov[0] ? ")" : "");
|
||||
}
|
||||
}
|
||||
@@ -605,11 +606,11 @@ static void apply_frame_rate(ASurfaceTransaction *tx, struct layer *l) {
|
||||
}
|
||||
int first = l->fps_applied < 0.0f;
|
||||
l->fps_applied = want;
|
||||
if (want > 0.0f) banner_log("layer", "display frame-rate vote on %s: %.2f Hz", l->name, want);
|
||||
else if (!first) banner_log("layer", "display frame-rate vote on %s cleared (panel runs free)", l->name);
|
||||
if (want > 0.0f) droiddeck_log("layer", "display frame-rate vote on %s: %.2f Hz", l->name, want);
|
||||
else if (!first) droiddeck_log("layer", "display frame-rate vote on %s cleared (panel runs free)", l->name);
|
||||
}
|
||||
|
||||
/* ---- colour (HDR, banner_color.h) -------------------------------------------------------------
|
||||
/* ---- colour (HDR, droiddeck_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
|
||||
* display never shows an HDR frame decoded as sRGB or the other way round. A layer that has never been
|
||||
@@ -629,67 +630,67 @@ static void coverage_text(const struct layer *l, char *out, size_t n) {
|
||||
|
||||
/* API 35: ask the display for HDR headroom explicitly while the layer carries an HDR frame, and clear the
|
||||
* request (0 = no preference) when it stops. Only on change; a layer never tagged is never touched. */
|
||||
static void apply_headroom(ASurfaceTransaction *tx, struct layer *l, const struct banner_color *c) {
|
||||
static void apply_headroom(ASurfaceTransaction *tx, struct layer *l, const struct droiddeck_color *c) {
|
||||
static int said_missing;
|
||||
if (!api.setDesiredHdrHeadroom) {
|
||||
if (c && c->dataspace && !said_missing) {
|
||||
said_missing = 1;
|
||||
banner_color_note_headroom_request(-1.0f);
|
||||
banner_log("color", "HDR headroom request not available on %s (Android < 15 has no "
|
||||
droiddeck_color_note_headroom_request(-1.0f);
|
||||
droiddeck_log("color", "HDR headroom request not available on %s (Android < 15 has no "
|
||||
"ASurfaceTransaction_setDesiredHdrHeadroom): the layer relies on Android's default", l->name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const float want = (c && c->dataspace) ? banner_color_desired_headroom(c, NULL, 0) : 0.0f;
|
||||
const float want = (c && c->dataspace) ? droiddeck_color_desired_headroom(c, NULL, 0) : 0.0f;
|
||||
if (l->hr_applied < 0.0f && want == 0.0f && !(c && c->dataspace)) return; /* never asked: leave it be */
|
||||
if (l->hr_applied >= 0.0f && fabsf(want - l->hr_applied) < 0.005f) return;
|
||||
char why[200] = "";
|
||||
if (c && c->dataspace) banner_color_desired_headroom(c, why, sizeof(why));
|
||||
if (c && c->dataspace) droiddeck_color_desired_headroom(c, why, sizeof(why));
|
||||
api.setDesiredHdrHeadroom(tx, l->sc, want);
|
||||
const int first = l->hr_applied < 0.0f;
|
||||
l->hr_applied = want;
|
||||
banner_color_note_headroom_request(want);
|
||||
droiddeck_color_note_headroom_request(want);
|
||||
if (want > 0.0f)
|
||||
banner_log("color", "requested HDR headroom %.1fx on %s (%s)", want, l->name, why);
|
||||
droiddeck_log("color", "requested HDR headroom %.1fx on %s (%s)", want, l->name, why);
|
||||
else if (c && c->dataspace)
|
||||
banner_log("color", "no HDR headroom requested on %s: %s", l->name, why);
|
||||
droiddeck_log("color", "no HDR headroom requested on %s: %s", l->name, why);
|
||||
else if (!first)
|
||||
banner_log("color", "HDR headroom request on %s cleared (no preference): the frame on the layer is not HDR", l->name);
|
||||
droiddeck_log("color", "HDR headroom request on %s cleared (no preference): the frame on the layer is not HDR", l->name);
|
||||
}
|
||||
|
||||
static void apply_colour(ASurfaceTransaction *tx, struct layer *l, const struct banner_color *c) {
|
||||
int32_t want = c ? c->dataspace : BANNER_ADATASPACE_UNKNOWN;
|
||||
static void apply_colour(ASurfaceTransaction *tx, struct layer *l, const struct droiddeck_color *c) {
|
||||
int32_t want = c ? c->dataspace : DROIDDECK_ADATASPACE_UNKNOWN;
|
||||
uint32_t md = (c && c->dataspace) ? c->identity : 0;
|
||||
if (!api.setBufferDataSpace) return;
|
||||
if (l->ds_applied < 0 && want == BANNER_ADATASPACE_UNKNOWN) return; /* never tagged: leave it be */
|
||||
if (l->ds_applied < 0 && want == DROIDDECK_ADATASPACE_UNKNOWN) return; /* never tagged: leave it be */
|
||||
apply_headroom(tx, l, want ? c : NULL);
|
||||
if (want == l->ds_applied && md == l->md_applied) return;
|
||||
api.setBufferDataSpace(tx, l->sc, want);
|
||||
struct banner_hdr_smpte2086 st;
|
||||
struct banner_hdr_cta861_3 cta;
|
||||
struct droiddeck_hdr_smpte2086 st;
|
||||
struct droiddeck_hdr_cta861_3 cta;
|
||||
int st_on = c && c->dataspace && c->has_st2086, cta_on = c && c->dataspace && c->has_cta861;
|
||||
if (st_on) {
|
||||
st = (struct banner_hdr_smpte2086){
|
||||
st = (struct droiddeck_hdr_smpte2086){
|
||||
.displayPrimaryRed = {c->red[0], c->red[1]}, .displayPrimaryGreen = {c->green[0], c->green[1]},
|
||||
.displayPrimaryBlue = {c->blue[0], c->blue[1]}, .whitePoint = {c->white[0], c->white[1]},
|
||||
.maxLuminance = c->max_lum, .minLuminance = c->min_lum};
|
||||
}
|
||||
if (cta_on) cta = (struct banner_hdr_cta861_3){.maxContentLightLevel = c->max_cll, .maxFrameAverageLightLevel = c->max_fall};
|
||||
if (cta_on) cta = (struct droiddeck_hdr_cta861_3){.maxContentLightLevel = c->max_cll, .maxFrameAverageLightLevel = c->max_fall};
|
||||
/* NULL clears what a previous description set: metadata never outlives the frame it belongs to. */
|
||||
if (api.setHdrMetadata_smpte2086) api.setHdrMetadata_smpte2086(tx, l->sc, st_on ? &st : NULL);
|
||||
if (api.setHdrMetadata_cta861_3) api.setHdrMetadata_cta861_3(tx, l->sc, cta_on ? &cta : NULL);
|
||||
if (want) {
|
||||
char cov[96];
|
||||
coverage_text(l, cov, sizeof(cov));
|
||||
banner_log("color", "%s: dataspace %s (%#x) set on the display layer for image description #%u%s%s; SMPTE 2086 "
|
||||
"%s, CTA-861.3 %s [%s]", l->name, want == BANNER_ADATASPACE_BT2020_PQ ? "BT2020_PQ" : "HDR",
|
||||
droiddeck_log("color", "%s: dataspace %s (%#x) set on the display layer for image description #%u%s%s; SMPTE 2086 "
|
||||
"%s, CTA-861.3 %s [%s]", l->name, want == DROIDDECK_ADATASPACE_BT2020_PQ ? "BT2020_PQ" : "HDR",
|
||||
(unsigned)want, c->identity, cov[0] ? ", " : "", cov,
|
||||
st_on ? (api.setHdrMetadata_smpte2086 ? "sent" : "not supported by this Android") : "none given",
|
||||
cta_on ? (api.setHdrMetadata_cta861_3 ? "sent" : "not supported by this Android") : "none given",
|
||||
c->text);
|
||||
}
|
||||
else
|
||||
banner_log("color", "%s: dataspace back to UNKNOWN (sRGB), HDR metadata cleared - the frame on the layer is "
|
||||
droiddeck_log("color", "%s: dataspace back to UNKNOWN (sRGB), HDR metadata cleared - the frame on the layer is "
|
||||
"not an HDR frame", l->name);
|
||||
l->ds_applied = want;
|
||||
l->md_applied = md;
|
||||
@@ -710,7 +711,7 @@ void sc_layer_hdr_symbols(char *out, size_t size) {
|
||||
static void log_layer_count(void) {
|
||||
if (g_two_logged) return;
|
||||
g_two_logged = 1;
|
||||
banner_log("layer", "%d display layers in use: \"%s\" (z=%d) and \"%s\" (z=%d) above it, both children of the "
|
||||
droiddeck_log("layer", "%d display layers in use: \"%s\" (z=%d) and \"%s\" (z=%d) above it, both children of the "
|
||||
"screen surface; the app's own views (drawer, HUD, pointer) stay above both. %d is the cap - more "
|
||||
"would push SurfaceFlinger into GPU client composition",
|
||||
SC_LAYER_COUNT, g_layers[SC_LAYER_GAME].name, (int)g_layers[SC_LAYER_GAME].z,
|
||||
@@ -747,7 +748,7 @@ static ASurfaceControl *swap_sc_begin(struct layer *l) {
|
||||
if (!win || win != l->win) return NULL; /* the window changed: ensure_sc re-creates it anyway */
|
||||
ASurfaceControl *fresh = api.createFromWindow(win, l->name);
|
||||
if (!fresh) {
|
||||
banner_log("error", "layer: %s: composition recovery could not create a new SurfaceControl", l->name);
|
||||
droiddeck_log("error", "layer: %s: composition recovery could not create a new SurfaceControl", l->name);
|
||||
return NULL;
|
||||
}
|
||||
ASurfaceControl *old = l->sc;
|
||||
@@ -759,14 +760,15 @@ static ASurfaceControl *swap_sc_begin(struct layer *l) {
|
||||
l->geo_valid = 0;
|
||||
l->fps_applied = -1.0f;
|
||||
l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f;
|
||||
banner_log("layer", "composition recovery: %s got a fresh SurfaceControl now that nothing is above "
|
||||
droiddeck_log("layer", "composition recovery: %s got a fresh SurfaceControl now that nothing is above "
|
||||
"the game (measured on this panel: hardware composition does NOT return from this alone)", l->name);
|
||||
return old;
|
||||
}
|
||||
|
||||
/* The transaction that puts pool slot `idx` of layer `l` on screen at `r`, as `color` (NULL = no
|
||||
* description). 0 = applied. */
|
||||
static int present_slot(struct layer *l, int idx, const int r[8], const struct banner_color *color) {
|
||||
static int present_slot(struct layer *l, int idx, const int r[8], const struct droiddeck_color *color,
|
||||
int translucent) {
|
||||
struct slot *s = &l->slots[idx];
|
||||
ASurfaceTransaction *tx = api.txCreate();
|
||||
if (!tx) return -1;
|
||||
@@ -774,9 +776,8 @@ static int present_slot(struct layer *l, int idx, const int r[8], const struct b
|
||||
if (old) api.setZOrder(tx, l->sc, l->z);
|
||||
/* The blit was waited for on the CPU, so no acquire fence is needed (-1). */
|
||||
api.setBuffer(tx, l->sc, s->ahb, -1);
|
||||
/* The compositor composes with blits, which overwrite: nothing is ever alpha-blended on the
|
||||
* copy path either, so every layer is opaque and the picture matches it pixel for pixel. */
|
||||
api.setBufferTransparency(tx, l->sc, ASC_TRANSPARENCY_OPAQUE);
|
||||
api.setBufferTransparency(tx, l->sc,
|
||||
translucent ? ASC_TRANSPARENCY_TRANSLUCENT : ASC_TRANSPARENCY_OPAQUE);
|
||||
apply_geometry(tx, l, r);
|
||||
apply_frame_rate(tx, l);
|
||||
apply_colour(tx, l, color);
|
||||
@@ -791,7 +792,7 @@ static int present_slot(struct layer *l, int idx, const int r[8], const struct b
|
||||
api.txDelete(tx);
|
||||
l->cur_slot = idx;
|
||||
l->cur_token = NULL;
|
||||
if (!l->shown) { l->shown = 1; banner_log("layer", "%s: layer shown", l->name); }
|
||||
if (!l->shown) { l->shown = 1; droiddeck_log("layer", "%s: layer shown", l->name); }
|
||||
g_stat_layer_frames++;
|
||||
return 0;
|
||||
}
|
||||
@@ -804,7 +805,7 @@ static void log_drop(struct layer *l) {
|
||||
l->drops_unlogged++;
|
||||
int64_t t = now_ns();
|
||||
if (!l->drop_logged_ns || t - l->drop_logged_ns > 30000000000LL) {
|
||||
banner_log("layer", "%s: no free layer buffer (display still holds all %d): %u frame%s dropped%s", l->name,
|
||||
droiddeck_log("layer", "%s: no free layer buffer (display still holds all %d): %u frame%s dropped%s", l->name,
|
||||
l->pool_n, l->drops_unlogged, l->drops_unlogged == 1 ? "" : "s",
|
||||
l->drop_logged_ns ? " since the last such line" : "");
|
||||
l->drop_logged_ns = t;
|
||||
@@ -819,7 +820,7 @@ unsigned sc_layer_drops_take(void) {
|
||||
}
|
||||
|
||||
int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, void *token, int scene_w, int scene_h,
|
||||
const struct banner_color *color, uint32_t ahb_format) {
|
||||
const struct droiddeck_color *color, uint32_t ahb_format) {
|
||||
struct layer *l = layer_of(SC_LAYER_GAME);
|
||||
int r[8];
|
||||
if (!ahb || !token || ensure_sc(l) != 0) goto unavailable;
|
||||
@@ -835,6 +836,7 @@ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, voi
|
||||
apply_geometry(tx, l, r); apply_frame_rate(tx, l); apply_colour(tx, l, color);
|
||||
api.txApply(tx); api.txDelete(tx);
|
||||
}
|
||||
vkp_signal_first_frame();
|
||||
return 0;
|
||||
}
|
||||
ASurfaceTransaction *tx = api.txCreate();
|
||||
@@ -861,16 +863,19 @@ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, voi
|
||||
api.txDelete(tx);
|
||||
l->cur_slot = -1;
|
||||
l->cur_token = token;
|
||||
if (!l->shown) { l->shown = 1; banner_log("layer", "%s: layer shown", l->name); }
|
||||
if (!l->first_logged) { l->first_logged = 1; vkp_signal_first_frame(); }
|
||||
if (color && color->dataspace) banner_color_frame_shown(color, BANNER_HDR_ZERO_COPY, ahb_format);
|
||||
if (!l->shown) { l->shown = 1; droiddeck_log("layer", "%s: layer shown", l->name); }
|
||||
/* The notification is re-armed for each session; the layer and its logging flag can
|
||||
* outlive it. Let the notification's own one-shot decide whether to fire. */
|
||||
if (!l->first_logged) l->first_logged = 1;
|
||||
vkp_signal_first_frame();
|
||||
if (color && color->dataspace) droiddeck_color_frame_shown(color, DROIDDECK_HDR_ZERO_COPY, ahb_format);
|
||||
return 0;
|
||||
unavailable:
|
||||
if (acquire_fd >= 0) close(acquire_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const struct banner_color *color) {
|
||||
int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const struct droiddeck_color *color) {
|
||||
struct layer *l = layer_of(SC_LAYER_GAME);
|
||||
if (!src || ensure_sc(l) != 0) return -1;
|
||||
int sw = vkp_image_width(src), sh = vkp_image_height(src);
|
||||
@@ -883,15 +888,15 @@ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const stru
|
||||
int idx = take_free_slot(l, sw, sh, AHB_RGBA8, &wait_fd);
|
||||
if (idx < 0) { log_drop(l); return 0; }
|
||||
if (vkp_blit_image(src, l->slots[idx].img, wait_fd) != 0) return -1;
|
||||
if (present_slot(l, idx, r, color) != 0) return -1;
|
||||
if (color && color->dataspace) banner_color_frame_shown(color, BANNER_HDR_LAYER_COPY, AHB_RGBA8);
|
||||
if (present_slot(l, idx, r, color, 0) != 0) return -1;
|
||||
if (color && color->dataspace) droiddeck_color_frame_shown(color, DROIDDECK_HDR_LAYER_COPY, AHB_RGBA8);
|
||||
if (!l->first_logged) {
|
||||
l->first_logged = 1;
|
||||
banner_log("layer", "presenting %dx%d game frames on their own SurfaceControl layer (%s pool, %d buffers); "
|
||||
droiddeck_log("layer", "presenting %dx%d game frames on their own SurfaceControl layer (%s pool, %d buffers); "
|
||||
"HUD and pointer stay Android views above it", sw, sh,
|
||||
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", l->pool_n);
|
||||
vkp_signal_first_frame();
|
||||
}
|
||||
vkp_signal_first_frame();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -908,14 +913,14 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int
|
||||
int idx = take_free_slot(l, rw, rh, AHB_RGBA8, &wait_fd);
|
||||
if (idx < 0) { vkp_pass_abort(); log_drop(l); return 0; }
|
||||
if (vkp_pass_copy_to(l->slots[idx].img, wait_fd) != 0) return -1;
|
||||
if (present_slot(l, idx, r, NULL) != 0) return -1; /* the effects chain's result is 8-bit sRGB */
|
||||
if (present_slot(l, idx, r, NULL, 0) != 0) return -1; /* the effects chain's result is 8-bit sRGB */
|
||||
if (!l->first_logged) {
|
||||
l->first_logged = 1;
|
||||
banner_log("layer", "presenting %dx%d frames on their own SurfaceControl layer (%s pool, %d buffers); "
|
||||
droiddeck_log("layer", "presenting %dx%d frames on their own SurfaceControl layer (%s pool, %d buffers); "
|
||||
"HUD and pointer stay Android views above it", rw, rh,
|
||||
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", l->pool_n);
|
||||
vkp_signal_first_frame();
|
||||
}
|
||||
vkp_signal_first_frame();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -930,7 +935,7 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int
|
||||
static uint32_t g_hdr_pool_fmt = AHB_RGB10A2;
|
||||
|
||||
int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf,
|
||||
int scene_w, int scene_h, const struct banner_color *color) {
|
||||
int scene_w, int scene_h, const struct droiddeck_color *color) {
|
||||
struct layer *l = layer_of(SC_LAYER_GAME);
|
||||
int rw = 0, rh = 0, r[8];
|
||||
if (!draws || n <= 0 || !hf || ensure_sc(l) != 0) return -1;
|
||||
@@ -944,33 +949,34 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct
|
||||
int idx = take_free_slot(l, rw, rh, fmt, &wait_fd);
|
||||
if (idx < 0 && g_alloc_failed && fmt == AHB_RGB10A2) {
|
||||
g_hdr_pool_fmt = AHB_RGBA8;
|
||||
banner_log("color", "%s: this device will not make a 10-bit layer buffer (RGBA1010102): the HDR picture goes "
|
||||
droiddeck_log("color", "%s: this device will not make a 10-bit layer buffer (RGBA1010102): the HDR picture goes "
|
||||
"on 8-bit buffers instead (still tagged BT2020_PQ; less precision)", l->name);
|
||||
idx = take_free_slot(l, rw, rh, g_hdr_pool_fmt, &wait_fd);
|
||||
}
|
||||
if (idx < 0) { vkp_pass_abort(); log_drop(l); return 0; }
|
||||
if (vkp_pass_copy_to(l->slots[idx].img, wait_fd) != 0) return -1;
|
||||
if (present_slot(l, idx, r, tm ? NULL : color) != 0) return -1; /* tone-mapped = plain sRGB: no tag */
|
||||
if (present_slot(l, idx, r, tm ? NULL : color, 0) != 0) return -1; /* tone-mapped = plain sRGB: no tag */
|
||||
if (color && color->dataspace)
|
||||
banner_color_frame_shown(color, tm ? BANNER_HDR_TONEMAPPED : BANNER_HDR_COMPOSED, tm ? AHB_RGBA8 : g_hdr_pool_fmt);
|
||||
droiddeck_color_frame_shown(color, tm ? DROIDDECK_HDR_TONEMAPPED : DROIDDECK_HDR_COMPOSED, tm ? AHB_RGBA8 : g_hdr_pool_fmt);
|
||||
static int said = -1;
|
||||
if (said != tm) {
|
||||
said = tm;
|
||||
if (tm)
|
||||
banner_log("color", "tone-mapped picture on the game's display layer: %dx%d, %s 8-bit buffers, untagged "
|
||||
droiddeck_log("color", "tone-mapped picture on the game's display layer: %dx%d, %s 8-bit buffers, untagged "
|
||||
"(sRGB) - HDR output is switched off", rw, rh,
|
||||
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear");
|
||||
else {
|
||||
char cov[96];
|
||||
coverage_text(l, cov, sizeof(cov));
|
||||
banner_log("color", "HDR picture on its own display layer: %dx%d, %s %s buffers, tagged %s%s%s", rw, rh,
|
||||
droiddeck_log("color", "HDR picture on its own display layer: %dx%d, %s %s buffers, tagged %s%s%s", rw, rh,
|
||||
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear",
|
||||
g_hdr_pool_fmt == AHB_RGB10A2 ? "10-bit" : "8-bit",
|
||||
color && color->dataspace == BANNER_ADATASPACE_BT2020_PQ ? "BT2020_PQ" : "HDR",
|
||||
color && color->dataspace == DROIDDECK_ADATASPACE_BT2020_PQ ? "BT2020_PQ" : "HDR",
|
||||
cov[0] ? ", " : "", cov);
|
||||
}
|
||||
}
|
||||
if (!l->first_logged) { l->first_logged = 1; vkp_signal_first_frame(); }
|
||||
if (!l->first_logged) l->first_logged = 1;
|
||||
vkp_signal_first_frame();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1019,19 +1025,19 @@ int sc_layer_overlay_affordable(void) {
|
||||
int first = g_overlay_declined < 0;
|
||||
g_overlay_declined = want;
|
||||
if (want)
|
||||
banner_log("layer", "overlay layer declined: this display rotates every layer %d° and the game "
|
||||
droiddeck_log("layer", "overlay layer declined: this display rotates every layer %d° and the game "
|
||||
"layer is scaled %dx%d -> %dx%d, and on that combination a second layer drops the "
|
||||
"whole frame to GPU composition for the rest of the session (measured). The window "
|
||||
"above the game goes on the copy path instead - same picture, one blit.",
|
||||
deg, sr[0], sr[1], ds[0], ds[1]);
|
||||
else if (!first)
|
||||
banner_log("layer", "overlay layer allowed again: the game layer is no longer both rotated and "
|
||||
droiddeck_log("layer", "overlay layer allowed again: the game layer is no longer both rotated and "
|
||||
"scaled, so a second display layer costs nothing here");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
int sc_layer_present_overlay(struct vkp_image *src, const int geo[8]) {
|
||||
int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int translucent) {
|
||||
struct layer *l = layer_of(SC_LAYER_OVERLAY);
|
||||
if (!src || !geo) return -1;
|
||||
/* The caller decides with sc_layer_overlay_affordable() BEFORE it commits the game to a layer;
|
||||
@@ -1046,7 +1052,7 @@ int sc_layer_present_overlay(struct vkp_image *src, const int geo[8]) {
|
||||
/* The window is copied into the layer buffer 1:1; `geo` crops it and places it, so the layer
|
||||
* is exactly the window's rectangle on screen and nothing else is blended anywhere. */
|
||||
if (vkp_blit_image(src, l->slots[idx].img, wait_fd) != 0) return -1;
|
||||
if (present_slot(l, idx, geo, NULL) != 0) return -1;
|
||||
if (present_slot(l, idx, geo, NULL, translucent) != 0) return -1;
|
||||
if (!l->first_logged) {
|
||||
l->first_logged = 1;
|
||||
log_layer_count();
|
||||
@@ -1076,7 +1082,7 @@ void sc_layer_hide(void) {
|
||||
* SurfaceControl, since it goes up and down with every effects/frame-generation toggle. */
|
||||
if (g_layers[SC_LAYER_OVERLAY].sc) sc_layer_hide_overlay();
|
||||
if (g_layers[SC_LAYER_GAME].shown) hide_layer(&g_layers[SC_LAYER_GAME]);
|
||||
if (said) banner_log("layer", "layers hidden (scene is not a single fullscreen window)");
|
||||
if (said) droiddeck_log("layer", "layers hidden (scene is not a single fullscreen window)");
|
||||
}
|
||||
|
||||
void sc_layer_hide_overlay(void) {
|
||||
@@ -1092,7 +1098,7 @@ void sc_layer_hide_overlay(void) {
|
||||
* buffers stay allocated for the next window. */
|
||||
if (l->shown) hide_layer(l);
|
||||
retire_sc(l);
|
||||
banner_log("layer", "%s: gone (nothing is above the game any more)", l->name);
|
||||
droiddeck_log("layer", "%s: gone (nothing is above the game any more)", l->name);
|
||||
/* ...and that is the half the measurement said is not enough: arm the game layer's
|
||||
* SurfaceControl swap, which the next frame performs (swap_sc_begin). */
|
||||
if (g_layers[SC_LAYER_GAME].sc) g_layers[SC_LAYER_GAME].recreate_pending = 1;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef SC_LAYER_H
|
||||
#define SC_LAYER_H
|
||||
/*
|
||||
* Layer mode (BANNER_WAYLAND_ZERO_COPY=1): the scene is handed to SurfaceFlinger as a small,
|
||||
* Layer mode (DROIDDECK_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.
|
||||
*
|
||||
@@ -12,8 +12,8 @@
|
||||
* an ASurfaceControl has no input channel, so touch and mouse keep
|
||||
* reaching the SurfaceView exactly as they did.
|
||||
* +-- SurfaceView ..... the compositor's Vulkan swapchain; black while layer mode is up
|
||||
* +-- z=1 "banner_wayland_game" ...... the one fullscreen game window
|
||||
* +-- z=2 "banner_wayland_overlay" ... at most one window drawn ABOVE the game
|
||||
* +-- z=1 "droiddeck_wayland_game" ...... the one fullscreen game window
|
||||
* +-- z=2 "droiddeck_wayland_overlay" ... at most one window drawn ABOVE the game
|
||||
*
|
||||
* TWO layers is the hard cap (SC_LAYER_COUNT), and deliberately so: HWC composes only a few
|
||||
* layers before SurfaceFlinger falls back to GPU client composition, which would throw away the
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
struct vkp_image;
|
||||
struct vkp_draw;
|
||||
struct banner_color;
|
||||
struct droiddeck_color;
|
||||
|
||||
/* The layers, bottom first. The z-order is the array order (z = id + 1). */
|
||||
enum sc_layer_id { SC_LAYER_GAME = 0, SC_LAYER_OVERLAY = 1, SC_LAYER_COUNT = 2 };
|
||||
@@ -84,7 +84,7 @@ int sc_layer_available(void);
|
||||
* Decides where a zero-copy acquire fence can come from; result goes to the session log. */
|
||||
void sc_layer_probe_dmabuf_fd(int fd);
|
||||
|
||||
/* HDR (banner_color.h): can a display layer be told its buffer's colour encoding here
|
||||
/* HDR (droiddeck_color.h): can a display layer be told its buffer's colour encoding here
|
||||
* (ASurfaceTransaction_setBufferDataSpace, Android 10+)? Part of the HDR gate. */
|
||||
int sc_layer_can_tag_hdr(void);
|
||||
/* Which of the colour calls this libandroid has, for the gate's log line. */
|
||||
@@ -100,7 +100,7 @@ void sc_layer_hdr_symbols(char *out, size_t size);
|
||||
* free buffer), -1 = the layer path is unavailable this frame and the caller must draw the old way.
|
||||
* The copy into the layer buffer is 8-bit: an HDR frame keeps its tag (its colours stay right), and
|
||||
* loses precision. */
|
||||
int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const struct banner_color *color);
|
||||
int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const struct droiddeck_color *color);
|
||||
|
||||
/* GAME layer, screen effects on: run the compositor pass (composite `draws` + the effects chain,
|
||||
* vkp_pass_begin) and put its result on the layer. Same return values as sc_layer_present. */
|
||||
@@ -113,7 +113,7 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int
|
||||
* Same return values as sc_layer_present. */
|
||||
struct vkp_hdr_frame;
|
||||
int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf,
|
||||
int scene_w, int scene_h, const struct banner_color *color);
|
||||
int scene_w, int scene_h, const struct droiddeck_color *color);
|
||||
|
||||
/* GAME layer, zero-copy (ahb_swapchain.c): show the game's own w x h AHardwareBuffer, gated by
|
||||
* acquire_fd (a sync_file the layer waits on before reading; owned by the callee, -1 = none).
|
||||
@@ -124,7 +124,7 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct
|
||||
* frame (the caller draws the old way). `color` / `ahb_format`: the frame's image description (NULL =
|
||||
* none) and the buffer's AHARDWAREBUFFER_FORMAT_*, for the dataspace and the HDR evidence. */
|
||||
int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, void *token,
|
||||
int scene_w, int scene_h, const struct banner_color *color, uint32_t ahb_format);
|
||||
int scene_w, int scene_h, const struct droiddeck_color *color, uint32_t ahb_format);
|
||||
|
||||
/* Vote a panel refresh rate for the layer the game presents on (VRR / refresh-rate matching), the
|
||||
* same rate and compatibility the app votes on its own surface with Surface.setFrameRate; 0 = no
|
||||
@@ -150,8 +150,9 @@ int sc_layer_overlay_affordable(void);
|
||||
|
||||
/* OVERLAY layer: show `src` (one window's imported frame) above the game layer, at the placement
|
||||
* `geo` = {src x0,y0,x1,y1 in image pixels, dst x0,y0,x1,y1 in output pixels} from vkp_map_draw.
|
||||
* `translucent` preserves a client's alpha over the game for Steam notifications and overlays.
|
||||
* 0 = shown or dropped, -1 = unavailable (the caller must fall back to the copy path). */
|
||||
int sc_layer_present_overlay(struct vkp_image *src, const int geo[8]);
|
||||
int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int translucent);
|
||||
|
||||
/* The scene is not a single fullscreen window this frame: hide every layer that is up. */
|
||||
void sc_layer_hide(void);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <sys/stat.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#define TAG "BannerWayland"
|
||||
#define TAG "DroidDeckWayland"
|
||||
|
||||
struct vk_api g_vk;
|
||||
static void *g_handle;
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
#include "effects_chain.h"
|
||||
#include "framegen_bridge.h"
|
||||
#include "hdr_compose.h"
|
||||
#include "banner_color.h"
|
||||
#include "blend_pass.h"
|
||||
#include "droiddeck_color.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -18,9 +19,9 @@
|
||||
#include <pthread.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#define TAG "BannerWayland"
|
||||
#define TAG "DroidDeckWayland"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
#define LOGE(...) banner_log("error", __VA_ARGS__)
|
||||
#define LOGE(...) droiddeck_log("error", __VA_ARGS__)
|
||||
#define MOD_INVALID VKP_MOD_INVALID
|
||||
|
||||
struct vkp_image {
|
||||
@@ -32,6 +33,8 @@ struct vkp_image {
|
||||
void *map; /* shm images: persistently mapped linear memory */
|
||||
VkDeviceSize offset, row_pitch;
|
||||
int in_general; /* shm images: moved from PREINITIALIZED to GENERAL */
|
||||
VkFormat fmt;
|
||||
int sampled; /* created with SAMPLED usage: the alpha pass can read it */
|
||||
};
|
||||
|
||||
static ANativeWindow *g_window; /* the window frames go to; compositor thread only */
|
||||
@@ -70,7 +73,7 @@ static VkImage *g_images;
|
||||
static uint32_t g_nimg;
|
||||
static VkExtent2D g_extent;
|
||||
|
||||
static int g_first_frame_done; /* one-shot: fire banner_on_first_frame() on first present */
|
||||
static int g_first_frame_done; /* one-shot: fire droiddeck_on_first_frame() on first present */
|
||||
|
||||
/* The base surface under the display layers (vkp_base_black): 1 once a plain black frame has been
|
||||
* presented on the CURRENT swapchain and nothing else since. Every other present clears it, and so does
|
||||
@@ -176,7 +179,7 @@ static int64_t g_swap_retry_at_ns;
|
||||
static int g_swap_fail_logged;
|
||||
|
||||
/* Implemented in waylandcomp_jni.c - notifies Java (dismiss launch overlay). */
|
||||
extern void banner_on_first_frame(void);
|
||||
extern void droiddeck_on_first_frame(void);
|
||||
|
||||
static char g_gpu_name[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];
|
||||
const char *vkp_gpu_name(void) { return g_gpu_name; }
|
||||
@@ -230,6 +233,7 @@ static void destroy_swapchain(void) {
|
||||
if (g_dev_state != 1) return;
|
||||
g_vk.DeviceWaitIdle(g_dev);
|
||||
reset_sync();
|
||||
for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]);
|
||||
if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL);
|
||||
g_swapchain = VK_NULL_HANDLE;
|
||||
g_swap_md_identity = 0; /* a new swapchain gets the metadata again */
|
||||
@@ -272,7 +276,7 @@ int vkp_apply_window_request(void) {
|
||||
g_window = w;
|
||||
g_swap_retry_at_ns = 0;
|
||||
g_swap_fail_logged = 0;
|
||||
banner_log("gpu", w ? "screen surface attached" : "screen surface gone: presenting paused");
|
||||
droiddeck_log("gpu", w ? "screen surface attached" : "screen surface gone: presenting paused");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -349,7 +353,7 @@ static void update_map(int scene_w, int scene_h) {
|
||||
g_map.off_y = (float)(g_map.ry + (int)((g_map.rh - scene_h * aspect) * 0.5f));
|
||||
}
|
||||
g_map.valid = 1;
|
||||
banner_log("screen", "%s, %s: %dx%d scene shown %dx%d at %d,%d on the %dx%d output",
|
||||
droiddeck_log("screen", "%s, %s: %dx%d scene shown %dx%d at %d,%d on the %dx%d output",
|
||||
mode_name(mode), align_name(align), scene_w, scene_h,
|
||||
(int)(scene_w * g_map.kx + 0.5f), (int)(scene_h * g_map.ky + 0.5f),
|
||||
(int)g_map.off_x, (int)g_map.off_y, W, H);
|
||||
@@ -387,10 +391,10 @@ static int dev_init(void) {
|
||||
const char *inst_exts[3] = {VK_KHR_SURFACE_EXTENSION_NAME,
|
||||
VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, NULL};
|
||||
uint32_t n_inst_exts = 2;
|
||||
/* HDR sessions only (banner_color.h): the colour-space extension lets the swapchain carry HDR10 for
|
||||
/* HDR sessions only (droiddeck_color.h): the colour-space extension lets the swapchain carry HDR10 for
|
||||
* frame generation. Asked for only when the loader has it, and only when HDR was asked for, so
|
||||
* every other session creates exactly the instance it always did. */
|
||||
if (banner_color_requested() && g_vk.EnumerateInstanceExtensionProperties) {
|
||||
if (droiddeck_color_requested() && g_vk.EnumerateInstanceExtensionProperties) {
|
||||
uint32_t ne = 0;
|
||||
g_vk.EnumerateInstanceExtensionProperties(NULL, &ne, NULL);
|
||||
VkExtensionProperties *ie = ne ? calloc(ne, sizeof(*ie)) : NULL;
|
||||
@@ -403,14 +407,14 @@ static int dev_init(void) {
|
||||
}
|
||||
}
|
||||
free(ie);
|
||||
banner_log("color", "compositor instance %s VK_EXT_swapchain_colorspace (an HDR10 swapchain for frame "
|
||||
droiddeck_log("color", "compositor instance %s VK_EXT_swapchain_colorspace (an HDR10 swapchain for frame "
|
||||
"generation %s)", g_colorspace_ext ? "enables" : "has no",
|
||||
g_colorspace_ext ? "is possible where the screen surface offers one" : "is not possible: tone-mapped instead");
|
||||
}
|
||||
/* 1.3 like the X11 renderer's instance: the frame-generation probe (framegen_engine.cpp)
|
||||
* queries VkPhysicalDeviceVulkan12Features, and a 1.1 instance may be answered as 1.1. */
|
||||
VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
|
||||
.pApplicationName = "banner-wayland-present",
|
||||
.pApplicationName = "droiddeck-wayland-present",
|
||||
.apiVersion = VK_API_VERSION_1_3};
|
||||
VkInstanceCreateInfo ici = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
|
||||
.pApplicationInfo = &app,
|
||||
@@ -440,9 +444,9 @@ static int dev_init(void) {
|
||||
VkPhysicalDeviceProperties props;
|
||||
g_vk.GetPhysicalDeviceProperties(g_pd, &props);
|
||||
snprintf(g_gpu_name, sizeof(g_gpu_name), "%s", props.deviceName);
|
||||
banner_log("gpu", "compositor renders on %s with %s", props.deviceName,
|
||||
droiddeck_log("gpu", "compositor renders on %s with %s", props.deviceName,
|
||||
g_library_name ? g_library_name : "the system Vulkan driver");
|
||||
if (g_driver_path) banner_log("gpu", "driver folder %s", g_driver_path);
|
||||
if (g_driver_path) droiddeck_log("gpu", "driver folder %s", g_driver_path);
|
||||
}
|
||||
g_vk.GetPhysicalDeviceMemoryProperties(g_pd, &g_memprops);
|
||||
|
||||
@@ -460,7 +464,7 @@ static int dev_init(void) {
|
||||
LOGE("present: driver MISSING %s (dmabuf import will fail)", dev_exts[i]);
|
||||
/* HDR sessions only: the HDR10 swapchain (frame generation) can carry the game's metadata. */
|
||||
int want_hdr_md = 0;
|
||||
if (banner_color_requested()) {
|
||||
if (droiddeck_color_requested()) {
|
||||
want_hdr_md = has_ext(exts, ne, VK_EXT_HDR_METADATA_EXTENSION_NAME);
|
||||
if (want_hdr_md) dev_exts[n_dev_exts++] = VK_EXT_HDR_METADATA_EXTENSION_NAME;
|
||||
}
|
||||
@@ -483,7 +487,7 @@ static int dev_init(void) {
|
||||
.enabledExtensionCount = n_dev_exts, .ppEnabledExtensionNames = dev_exts};
|
||||
VkResult dr = g_vk.CreateDevice(g_pd, &dci, NULL, &g_dev);
|
||||
if (dr != VK_SUCCESS && fg_features) {
|
||||
banner_log("framegen", "the driver refused the LSFG feature set (%s); device created without it",
|
||||
droiddeck_log("framegen", "the driver refused the LSFG feature set (%s); device created without it",
|
||||
vk_result_name(dr));
|
||||
dci.pNext = NULL;
|
||||
fg_features = NULL;
|
||||
@@ -502,13 +506,13 @@ static int dev_init(void) {
|
||||
g_wait_sem = VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
banner_log("perf", "layer buffers: the display's release fences are waited for %s",
|
||||
droiddeck_log("perf", "layer buffers: the display's release fences are waited for %s",
|
||||
g_import_sem_fd ? "on the GPU (VK_KHR_external_semaphore_fd)"
|
||||
: "on the CPU (this driver has no VK_KHR_external_semaphore_fd)");
|
||||
if (want_hdr_md)
|
||||
g_set_hdr_metadata = (PFN_vkSetHdrMetadataEXT)g_vk.GetDeviceProcAddr(g_dev, "vkSetHdrMetadataEXT");
|
||||
if (banner_color_requested())
|
||||
banner_log("color", "compositor device %s VK_EXT_hdr_metadata (the HDR10 swapchain for frame generation %s)",
|
||||
if (droiddeck_color_requested())
|
||||
droiddeck_log("color", "compositor device %s VK_EXT_hdr_metadata (the HDR10 swapchain for frame generation %s)",
|
||||
g_set_hdr_metadata ? "enables" : "has no",
|
||||
g_set_hdr_metadata ? "carries the game's mastering metadata" : "goes without the game's metadata");
|
||||
|
||||
@@ -525,6 +529,7 @@ static int dev_init(void) {
|
||||
|
||||
vkp_effects_bind_device(g_dev, g_pd, &g_memprops);
|
||||
hdrc_bind_device(g_dev, &g_memprops);
|
||||
blendp_bind_device(g_dev);
|
||||
g_dev_state = 1;
|
||||
reset_sync();
|
||||
init_black_image();
|
||||
@@ -636,7 +641,7 @@ static void log_surface_formats(const VkSurfaceFormatKHR *f, uint32_t n) {
|
||||
vk_colorspace_short(f[i].colorSpace));
|
||||
else hdr_cut++;
|
||||
}
|
||||
banner_log("color", "screen surface lists %u format/colour-space pairs (%s); HDR-capable: %s%s", n, per,
|
||||
droiddeck_log("color", "screen surface lists %u format/colour-space pairs (%s); HDR-capable: %s%s", n, per,
|
||||
hdr_n ? hdr : "none", hdr_cut ? " (+more)" : "");
|
||||
}
|
||||
|
||||
@@ -673,7 +678,7 @@ static int swap_init_locked(void) {
|
||||
}
|
||||
VkSurfaceFormatKHR chosen = fmts[0];
|
||||
const VkSurfaceFormatKHR sdr_choice = fmts[0];
|
||||
if (banner_color_requested() && (!g_surface_formats_said || g_swap_hdr_want)) {
|
||||
if (droiddeck_color_requested() && (!g_surface_formats_said || g_swap_hdr_want)) {
|
||||
g_surface_formats_said = 1;
|
||||
log_surface_formats(fmts, nfmt);
|
||||
}
|
||||
@@ -697,13 +702,13 @@ static int swap_init_locked(void) {
|
||||
g_swap_is_hdr = 1;
|
||||
char fname[24];
|
||||
vk_format_short(chosen.format, fname, sizeof(fname));
|
||||
banner_log("color", "screen swapchain built as HDR10 (format %d %s, HDR10_ST2084): frame-generated HDR "
|
||||
droiddeck_log("color", "screen swapchain built as HDR10 (format %d %s, HDR10_ST2084): frame-generated HDR "
|
||||
"frames reach the display as PQ BT.2020%s", (int)chosen.format, fname,
|
||||
vk_format_bits(chosen.format) <= 8 ? " - an 8-bit swapchain: banding possible in smooth gradients"
|
||||
: "");
|
||||
} else {
|
||||
g_swap_hdr_unavailable = 1;
|
||||
banner_log("color", "the screen surface lists no HDR10 swapchain format among its %u format/colour-space "
|
||||
droiddeck_log("color", "the screen surface lists no HDR10 swapchain format among its %u format/colour-space "
|
||||
"pairs (see the line above): frames with frame generation are tone-mapped to SDR instead",
|
||||
nfmt);
|
||||
}
|
||||
@@ -743,7 +748,7 @@ static int swap_init_locked(void) {
|
||||
if (cr != VK_SUCCESS && g_swap_is_hdr) {
|
||||
/* The surface listed the HDR10 pair but refused the swapchain: say so, never ask again this
|
||||
* surface, and build the ordinary one now (frames tone-mapped) - never a black screen. */
|
||||
banner_log("color", "the driver refused the HDR10 swapchain (%s): frames with frame generation are "
|
||||
droiddeck_log("color", "the driver refused the HDR10 swapchain (%s): frames with frame generation are "
|
||||
"tone-mapped to SDR instead", vk_result_name(cr));
|
||||
g_swap_is_hdr = 0;
|
||||
g_swap_hdr_unavailable = 1;
|
||||
@@ -762,7 +767,7 @@ static int swap_init_locked(void) {
|
||||
g_images = calloc(g_nimg, sizeof(VkImage));
|
||||
g_vk.GetSwapchainImagesKHR(g_dev, g_swapchain, &g_nimg, g_images);
|
||||
|
||||
banner_log("gpu", "screen output %ux%u, %u buffers, vsync%s", g_extent.width, g_extent.height, g_nimg,
|
||||
droiddeck_log("gpu", "screen output %ux%u, %u buffers, vsync%s", g_extent.width, g_extent.height, g_nimg,
|
||||
g_swap_extra ? " (frame generation: several presents per frame)" : "");
|
||||
return 0;
|
||||
}
|
||||
@@ -828,7 +833,7 @@ static void init_black_image(void) {
|
||||
g_vk.WaitForFences(g_dev, 1, &g_fence, VK_TRUE, 1000000000ULL) == VK_SUCCESS)
|
||||
g_black_state = 1;
|
||||
else
|
||||
banner_log("gpu", "letterbox black image unavailable: the whole output is cleared every copy-path frame");
|
||||
droiddeck_log("gpu", "letterbox black image unavailable: the whole output is cleared every copy-path frame");
|
||||
}
|
||||
|
||||
/* Blit the black image over everything of swapchain image `img` (in TRANSFER_DST_OPTIMAL) OUTSIDE the
|
||||
@@ -873,9 +878,8 @@ const char *vkp_modifier_name(uint64_t modifier) {
|
||||
return other;
|
||||
}
|
||||
|
||||
/* 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? */
|
||||
static int modifier_importable(VkFormat fmt, uint64_t modifier) {
|
||||
/* Can the driver create and import this format/modifier with the requested image usage? */
|
||||
static int modifier_importable(VkFormat fmt, uint64_t modifier, VkImageUsageFlags usage) {
|
||||
if (!g_vk.GetPhysicalDeviceImageFormatProperties2) return 1; /* can't ask; the create will tell */
|
||||
VkPhysicalDeviceExternalImageFormatInfo ext = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO,
|
||||
@@ -886,7 +890,7 @@ static int modifier_importable(VkFormat fmt, uint64_t modifier) {
|
||||
VkPhysicalDeviceImageFormatInfo2 info = {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, .pNext = &mod,
|
||||
.format = fmt, .type = VK_IMAGE_TYPE_2D, .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT};
|
||||
.usage = usage};
|
||||
VkExternalImageFormatProperties extp = {.sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES};
|
||||
VkImageFormatProperties2 props = {.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, .pNext = &extp};
|
||||
if (g_vk.GetPhysicalDeviceImageFormatProperties2(g_pd, &info, &props) != VK_SUCCESS) return 0;
|
||||
@@ -914,7 +918,7 @@ int vkp_dmabuf_modifiers(uint32_t drm_format, uint64_t *out, int max) {
|
||||
if (m != VKP_MOD_LINEAR && m != VKP_MOD_QCOM_COMPRESSED) why = "unknown layout";
|
||||
else if (props[i].drmFormatModifierPlaneCount != 1) why = "not single-plane";
|
||||
else if (!(props[i].drmFormatModifierTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT)) why = "no blit source";
|
||||
else if (!modifier_importable(fmt, m)) why = "not importable as a dma-buf";
|
||||
else if (!modifier_importable(fmt, m, VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) why = "not importable as a dma-buf";
|
||||
if (why) {
|
||||
LOGI("dmabuf: %c%c%c%c %s reported by the driver but not advertised: %s",
|
||||
drm_format & 0xff, (drm_format >> 8) & 0xff, (drm_format >> 16) & 0xff,
|
||||
@@ -942,6 +946,10 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
|
||||
struct vkp_image *img = calloc(1, sizeof(*img));
|
||||
if (!img) return NULL;
|
||||
img->w = w; img->h = h; img->dmabuf = 1; img->blit_dst = as_blit_dst ? 1 : 0;
|
||||
img->fmt = drm_to_vk(drm_format);
|
||||
const VkImageUsageFlags sampled_usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
img->sampled = !as_blit_dst && g_vk.GetPhysicalDeviceImageFormatProperties2 &&
|
||||
modifier_importable(img->fmt, modifier, sampled_usage);
|
||||
|
||||
VkSubresourceLayout plane = {.offset = offset, .rowPitch = stride};
|
||||
VkImageDrmFormatModifierExplicitCreateInfoEXT modInfo = {
|
||||
@@ -952,17 +960,18 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
|
||||
.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT};
|
||||
VkImageCreateInfo ici = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .pNext = &extImg,
|
||||
.imageType = VK_IMAGE_TYPE_2D, .format = drm_to_vk(drm_format), .extent = {w, h, 1},
|
||||
.imageType = VK_IMAGE_TYPE_2D, .format = img->fmt, .extent = {w, h, 1},
|
||||
.mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
|
||||
.usage = as_blit_dst ? VK_IMAGE_USAGE_TRANSFER_DST_BIT : VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
.usage = as_blit_dst ? VK_IMAGE_USAGE_TRANSFER_DST_BIT
|
||||
: img->sampled ? sampled_usage : VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
VkResult cr = g_vk.CreateImage(g_dev, &ici, NULL, &img->image);
|
||||
if (cr != VK_SUCCESS) {
|
||||
LOGE("%s: vkCreateImage(%s, %dx%d, pitch %u, offset %u) -> %s%s", as_blit_dst ? "layer" : "dmabuf",
|
||||
vkp_modifier_name(modifier), w, h, stride, offset, vk_result_name(cr),
|
||||
(!as_blit_dst && modifier == VKP_MOD_QCOM_COMPRESSED)
|
||||
? ": the game's UBWC layout was refused; BANNER_WAYLAND_UBWC=0 forces linear buffers" : "");
|
||||
? ": the game's UBWC layout was refused; DROIDDECK_WAYLAND_UBWC=0 forces linear buffers" : "");
|
||||
free(img); return NULL;
|
||||
}
|
||||
|
||||
@@ -1008,18 +1017,35 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
|
||||
return img;
|
||||
}
|
||||
|
||||
/* Linear wl_shm images can use the blend pass only if the driver samples them with a linear filter. */
|
||||
static int shm_sampled(void) {
|
||||
static int known;
|
||||
if (!known) {
|
||||
const VkFormatFeatureFlags need = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT;
|
||||
VkFormatProperties2 fp = {.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2};
|
||||
if (g_vk.GetPhysicalDeviceFormatProperties2)
|
||||
g_vk.GetPhysicalDeviceFormatProperties2(g_pd, VK_FORMAT_B8G8R8A8_UNORM, &fp);
|
||||
known = (fp.formatProperties.linearTilingFeatures & need) == need ? 1 : -1;
|
||||
}
|
||||
return known == 1;
|
||||
}
|
||||
|
||||
struct vkp_image *vkp_image_create_shm(int w, int h) {
|
||||
if (w <= 0 || h <= 0 || dev_init() != 0) return NULL;
|
||||
|
||||
struct vkp_image *img = calloc(1, sizeof(*img));
|
||||
if (!img) return NULL;
|
||||
img->w = w; img->h = h;
|
||||
img->fmt = VK_FORMAT_B8G8R8A8_UNORM;
|
||||
img->sampled = shm_sampled();
|
||||
|
||||
VkImageCreateInfo ici = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = VK_FORMAT_B8G8R8A8_UNORM, .extent = {w, h, 1}, .mipLevels = 1, .arrayLayers = 1,
|
||||
.format = img->fmt, .extent = {w, h, 1}, .mipLevels = 1, .arrayLayers = 1,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_LINEAR,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | (img->sampled ? VK_IMAGE_USAGE_SAMPLED_BIT : 0),
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED};
|
||||
if (g_vk.CreateImage(g_dev, &ici, NULL, &img->image) != VK_SUCCESS) { free(img); return NULL; }
|
||||
|
||||
@@ -1062,6 +1088,7 @@ int vkp_image_height(const struct vkp_image *img) { return img ? img->h : 0; }
|
||||
void vkp_image_destroy(struct vkp_image *img) {
|
||||
if (!img) return;
|
||||
if (g_dev) {
|
||||
blendp_forget_image(img->image);
|
||||
if (img->map) g_vk.UnmapMemory(g_dev, img->mem);
|
||||
if (img->image) g_vk.DestroyImage(g_dev, img->image, NULL);
|
||||
if (img->mem) g_vk.FreeMemory(g_dev, img->mem, NULL);
|
||||
@@ -1111,8 +1138,21 @@ static int draw_to_scene_blit(const struct vkp_draw *d, int scene_w, int scene_h
|
||||
return map_draw(d, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, (float)scene_w, (float)scene_h, blit);
|
||||
}
|
||||
|
||||
/* A translucent draw needs source-over blending; every other draw keeps the existing blit path. */
|
||||
static void record_draw(VkCommandBuffer cmd, const struct vkp_draw *d, const VkImageBlit *blit, VkImage target,
|
||||
VkFormat target_fmt, int target_w, int target_h, VkFilter filter) {
|
||||
const struct vkp_image *im = d->img;
|
||||
if (d->blend && im->sampled &&
|
||||
blendp_draw(cmd, im->image, im->fmt, im->w, im->h, !im->dmabuf, target, target_fmt, target_w, target_h,
|
||||
blit) == 0)
|
||||
return;
|
||||
g_vk.CmdBlitImage(cmd, im->image, im->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, filter);
|
||||
}
|
||||
|
||||
static void destroy_scene_image(void) {
|
||||
if (g_scene.img) hdrc_forget_image(g_scene.img); /* the HDR pass may have drawn into it */
|
||||
if (g_scene.img) blendp_forget_image(g_scene.img);
|
||||
if (g_scene.img) g_vk.DestroyImage(g_dev, g_scene.img, NULL);
|
||||
if (g_scene.mem) g_vk.FreeMemory(g_dev, g_scene.mem, NULL);
|
||||
memset(&g_scene, 0, sizeof(g_scene));
|
||||
@@ -1142,7 +1182,7 @@ static int ensure_scene_image(int w, int h) {
|
||||
if (r != VK_SUCCESS) { LOGE("effects: scene image %dx%d: vkAllocateMemory %s", w, h, vk_result_name(r)); destroy_scene_image(); return -1; }
|
||||
g_vk.BindImageMemory(g_dev, g_scene.img, g_scene.mem, 0);
|
||||
g_scene.w = w; g_scene.h = h;
|
||||
banner_log("effects", "scene image %dx%d for the effect chain", w, h);
|
||||
droiddeck_log("effects", "scene image %dx%d for the effect chain", w, h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1150,6 +1190,7 @@ static int ensure_scene_image(int w, int h) {
|
||||
static int ensure_img(struct vkp_img_slot *s, int w, int h, VkFormat fmt, VkImageUsageFlags usage, const char *what) {
|
||||
if (s->img && s->w == w && s->h == h && s->fmt == fmt) return 0;
|
||||
if (s->img) hdrc_forget_image(s->img);
|
||||
if (s->img) blendp_forget_image(s->img);
|
||||
if (s->img) g_vk.DestroyImage(g_dev, s->img, NULL);
|
||||
if (s->mem) g_vk.FreeMemory(g_dev, s->mem, NULL);
|
||||
memset(s, 0, sizeof(*s));
|
||||
@@ -1177,7 +1218,7 @@ static int ensure_img(struct vkp_img_slot *s, int w, int h, VkFormat fmt, VkImag
|
||||
s->w = w; s->h = h; s->fmt = fmt;
|
||||
char fname[24];
|
||||
vk_format_short(fmt, fname, sizeof(fname));
|
||||
banner_log("color", "%s image %dx%d (%s)", what, w, h, fname);
|
||||
droiddeck_log("color", "%s image %dx%d (%s)", what, w, h, fname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1190,7 +1231,8 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n,
|
||||
int scene_w, int scene_h, VkImage out, VkFormat out_fmt, enum hdrc_mode mode) {
|
||||
if (!hf || !hf->is_hdr || n <= 0) return -1;
|
||||
if (ensure_img(&g_mixed, scene_w, scene_h, HDR_FMT,
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
|
||||
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
|
||||
"HDR composition (mixed)") != 0)
|
||||
return -1;
|
||||
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
@@ -1207,16 +1249,14 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n,
|
||||
.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT};
|
||||
g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &mb, 0, NULL, 0, NULL);
|
||||
|
||||
struct hdrc_params p = {.mode = mode, .sdr_white_nits = banner_color_sdr_white(),
|
||||
struct hdrc_params p = {.mode = mode, .sdr_white_nits = droiddeck_color_sdr_white(),
|
||||
.peak_nits = hf->peak_nits > 0.0f ? hf->peak_nits : 1000.0f};
|
||||
int lowest_hdr = -1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
VkImageBlit blit;
|
||||
if (!draws[i].img || !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit)) continue;
|
||||
/* A blit converts UNORM to UNORM by value: 8-bit sRGB and 10-bit PQ both land unchanged. */
|
||||
g_vk.CmdBlitImage(cmd, draws[i].img->image,
|
||||
draws[i].img->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
g_mixed.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
|
||||
record_draw(cmd, &draws[i], &blit, g_mixed.img, HDR_FMT, scene_w, scene_h, VK_FILTER_LINEAR);
|
||||
if (hf->is_hdr[i] && lowest_hdr < 0) lowest_hdr = i;
|
||||
}
|
||||
static int truncated_said;
|
||||
@@ -1232,7 +1272,7 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n,
|
||||
}
|
||||
if (i >= lowest_hdr && !truncated_said) {
|
||||
truncated_said = 1;
|
||||
banner_log("color", "HDR composition: more than %d windows are stacked over the HDR game - the lowest "
|
||||
droiddeck_log("color", "HDR composition: more than %d windows are stacked over the HDR game - the lowest "
|
||||
"ones are treated as SDR", HDRC_MAX_RECTS);
|
||||
}
|
||||
}
|
||||
@@ -1247,8 +1287,9 @@ static void device_lost(const char *where) {
|
||||
g_dev_state = -2;
|
||||
g_base_black = 0;
|
||||
vkp_framegen_device_lost();
|
||||
banner_log("error", "GPU device lost (VK_ERROR_DEVICE_LOST in %s): the compositor has stopped presenting; "
|
||||
droiddeck_log("error", "GPU device lost (VK_ERROR_DEVICE_LOST in %s): the compositor has stopped presenting; "
|
||||
"restart the session", where);
|
||||
for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]);
|
||||
if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL);
|
||||
g_swapchain = VK_NULL_HANDLE;
|
||||
if (g_surface) g_vk.DestroySurfaceKHR(g_inst, g_surface, NULL);
|
||||
@@ -1344,12 +1385,12 @@ static int acquire_image(int k, int first, uint32_t *img, VkResult *ar_out) {
|
||||
if (ar == VK_SUCCESS || ar == VK_SUBOPTIMAL_KHR) { *ar_out = ar; return 0; }
|
||||
if (ar == VK_ERROR_DEVICE_LOST) { device_lost("acquire"); return -1; }
|
||||
if (ar == VK_ERROR_OUT_OF_DATE_KHR || ar == VK_ERROR_SURFACE_LOST_KHR) {
|
||||
banner_log("gpu", "screen surface %s on acquire: rebuilding the swapchain", vk_result_name(ar));
|
||||
droiddeck_log("gpu", "screen surface %s on acquire: rebuilding the swapchain", vk_result_name(ar));
|
||||
destroy_swapchain();
|
||||
if (first && attempt == 0) continue;
|
||||
return -1;
|
||||
}
|
||||
banner_log("error", "present: acquire %d failed (%s %d)", k, vk_result_name(ar), (int)ar);
|
||||
droiddeck_log("error", "present: acquire %d failed (%s %d)", k, vk_result_name(ar), (int)ar);
|
||||
if (ar == VK_TIMEOUT || ar == VK_NOT_READY) return -1;
|
||||
destroy_swapchain(); /* anything else: start over next frame */
|
||||
return -1;
|
||||
@@ -1381,7 +1422,7 @@ static void check_surface_changed(void) {
|
||||
if (caps.currentExtent.width == g_caps_extent.width && caps.currentExtent.height == g_caps_extent.height &&
|
||||
caps.currentTransform == g_surface_transform)
|
||||
return;
|
||||
banner_log("gpu", "screen surface changed (%ux%u -> %ux%u, transform %d -> %d): rebuilding the swapchain",
|
||||
droiddeck_log("gpu", "screen surface changed (%ux%u -> %ux%u, transform %d -> %d): rebuilding the swapchain",
|
||||
g_caps_extent.width, g_caps_extent.height, caps.currentExtent.width, caps.currentExtent.height,
|
||||
(int)g_surface_transform, (int)caps.currentTransform);
|
||||
destroy_swapchain();
|
||||
@@ -1423,7 +1464,7 @@ static int take_wait_fd(int fd) {
|
||||
static int said;
|
||||
if (!said) {
|
||||
said = 1;
|
||||
banner_log("perf", "layer buffers: importing a release fence as a GPU wait failed; waiting for it on the CPU");
|
||||
droiddeck_log("perf", "layer buffers: importing a release fence as a GPU wait failed; waiting for it on the CPU");
|
||||
}
|
||||
}
|
||||
struct pollfd p = {.fd = fd, .events = POLLIN};
|
||||
@@ -1435,18 +1476,18 @@ static int take_wait_fd(int fd) {
|
||||
|
||||
/* The HDR10 swapchain's metadata: the game's SMPTE 2086 / CTA-861.3 values (its image description) via
|
||||
* VK_EXT_hdr_metadata, once per swapchain and description. A game that gave none gets none. */
|
||||
static void send_hdr_metadata(const struct banner_color *c) {
|
||||
static void send_hdr_metadata(const struct droiddeck_color *c) {
|
||||
static int said_none, said_nodesc;
|
||||
if (!c) return;
|
||||
if (!g_set_hdr_metadata) {
|
||||
if (!said_none) { said_none = 1; banner_log("color", "HDR10 swapchain without the game's metadata: this device "
|
||||
if (!said_none) { said_none = 1; droiddeck_log("color", "HDR10 swapchain without the game's metadata: this device "
|
||||
"has no VK_EXT_hdr_metadata (the display uses its defaults)"); }
|
||||
return;
|
||||
}
|
||||
if (!c->has_st2086 && !c->has_cta861) {
|
||||
if (said_nodesc != (int)c->identity) {
|
||||
said_nodesc = (int)c->identity;
|
||||
banner_log("color", "HDR10 swapchain: image description #%u carries no HDR metadata (the game sent none), "
|
||||
droiddeck_log("color", "HDR10 swapchain: image description #%u carries no HDR metadata (the game sent none), "
|
||||
"so none is set - the display uses its defaults", c->identity);
|
||||
}
|
||||
return;
|
||||
@@ -1465,7 +1506,7 @@ static void send_hdr_metadata(const struct banner_color *c) {
|
||||
md.maxFrameAverageLightLevel = c->max_fall;
|
||||
}
|
||||
g_set_hdr_metadata(g_dev, 1, &g_swapchain, &md);
|
||||
banner_log("color", "HDR10 swapchain: the game's metadata set via VK_EXT_hdr_metadata (image description #%u: %s)",
|
||||
droiddeck_log("color", "HDR10 swapchain: the game's metadata set via VK_EXT_hdr_metadata (image description #%u: %s)",
|
||||
c->identity, c->text);
|
||||
}
|
||||
|
||||
@@ -1481,7 +1522,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
/* Frame generation queues several presents per scene frame and needs swapchain images for
|
||||
* them: rebuild when that changes (armed, disarmed, another multiplier). */
|
||||
if (g_swapchain && vkp_framegen_extra_images() != g_swap_extra) {
|
||||
banner_log("gpu", "frame generation now wants %d present%s per frame: rebuilding the swapchain",
|
||||
droiddeck_log("gpu", "frame generation now wants %d present%s per frame: rebuilding the swapchain",
|
||||
1 + vkp_framegen_extra_images(), vkp_framegen_extra_images() ? "s" : "");
|
||||
destroy_swapchain();
|
||||
}
|
||||
@@ -1490,7 +1531,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
* swapchain (only in HDR sessions: g_colorspace_ext). */
|
||||
const int want_hdr = hf && !hf->tonemap && g_colorspace_ext && !g_swap_hdr_unavailable;
|
||||
if (g_swapchain && want_hdr != g_swap_is_hdr) {
|
||||
banner_log("color", want_hdr ? "HDR frames with frame generation: rebuilding the screen swapchain as HDR10"
|
||||
droiddeck_log("color", want_hdr ? "HDR frames with frame generation: rebuilding the screen swapchain as HDR10"
|
||||
: hf ? "HDR output switched off: rebuilding the screen swapchain as SDR (frames tone-mapped)"
|
||||
: "no HDR frames through the screen swapchain any more: rebuilding it as SDR");
|
||||
destroy_swapchain();
|
||||
@@ -1529,10 +1570,10 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
char sw[24];
|
||||
vk_format_short(g_swap_fmt, sw, sizeof(sw));
|
||||
if (deep)
|
||||
banner_log("color", "frame generation runs on the HDR picture in FP16 (RGBA16F): more than 8 bits of PQ "
|
||||
droiddeck_log("color", "frame generation runs on the HDR picture in FP16 (RGBA16F): more than 8 bits of PQ "
|
||||
"through interpolation, into the HDR10 swapchain (%s)", sw);
|
||||
else
|
||||
banner_log("color", "frame generation runs on the HDR picture in 8 bits (the engine cannot take FP16 here), "
|
||||
droiddeck_log("color", "frame generation runs on the HDR picture in 8 bits (the engine cannot take FP16 here), "
|
||||
"into the HDR10 swapchain (%s): banding possible in smooth gradients", sw);
|
||||
}
|
||||
}
|
||||
@@ -1547,6 +1588,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
g_vk.BeginCommandBuffer(cmd, &bi);
|
||||
blendp_begin_frame();
|
||||
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
|
||||
/* Source images: take dmabufs from the client's queue family, move shm images to
|
||||
@@ -1589,8 +1631,8 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
free(bars);
|
||||
|
||||
/* Clear the composition target to black (the letterbox on the direct path, the desktop's
|
||||
* background on the pass) before the draws land on it. A draw that covers the whole scene
|
||||
* overwrites every pixel of its rectangle (blits never blend), so then only what lies outside it
|
||||
* background on the pass) before the draws land on it. An opaque draw that covers the whole scene
|
||||
* overwrites every pixel of its rectangle, so then only what lies outside it
|
||||
* needs black: the letterbox bars on the direct path, nothing at all on the pass (the scene image
|
||||
* is exactly the scene). Anything else - no such draw, the HDR composition - clears the whole
|
||||
* target as before. */
|
||||
@@ -1600,7 +1642,8 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
if (!(hf && pass)) {
|
||||
for (int i = n - 1; i >= 0 && !covered; i--) {
|
||||
const struct vkp_draw *d = &draws[i];
|
||||
if (!d->img || d->dx > 0 || d->dy > 0 || d->dx + d->dw < scene_w || d->dy + d->dh < scene_h) continue;
|
||||
if (!d->img || d->blend || d->dx > 0 || d->dy > 0 || d->dx + d->dw < scene_w ||
|
||||
d->dy + d->dh < scene_h) continue;
|
||||
covered = pass ? draw_to_scene_blit(d, scene_w, scene_h, &cover_blit) : draw_to_blit(d, &cover_blit);
|
||||
}
|
||||
}
|
||||
@@ -1633,10 +1676,9 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
VkImageBlit blit;
|
||||
if (!draws[i].img) continue;
|
||||
if (pass ? !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit) : !draw_to_blit(&draws[i], &blit)) continue;
|
||||
g_vk.CmdBlitImage(cmd, draws[i].img->image,
|
||||
draws[i].img->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit,
|
||||
pass ? VK_FILTER_LINEAR : blit_filter);
|
||||
record_draw(cmd, &draws[i], &blit, target, pass ? scene_fmt : g_swap_fmt,
|
||||
pass ? scene_w : (int)g_extent.width, pass ? scene_h : (int)g_extent.height,
|
||||
pass ? VK_FILTER_LINEAR : blit_filter);
|
||||
drawn++;
|
||||
}
|
||||
|
||||
@@ -1704,7 +1746,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
if (k > 0) {
|
||||
VkResult ark;
|
||||
if (acquire_image(k, 0, &img, &ark) != 0) {
|
||||
banner_log("framegen", "present %d/%d: no swapchain image; the frame ends early", k + 1, presents);
|
||||
droiddeck_log("framegen", "present %d/%d: no swapchain image; the frame ends early", k + 1, presents);
|
||||
pr = VK_ERROR_OUT_OF_DATE_KHR;
|
||||
break;
|
||||
}
|
||||
@@ -1749,7 +1791,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
if (fr == VK_ERROR_DEVICE_LOST || pr == VK_ERROR_DEVICE_LOST) { device_lost("present"); return -1; }
|
||||
if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR) {
|
||||
/* The surface changed or went away under this frame: rebuild before the next one. */
|
||||
banner_log("gpu", "screen surface %s on present: rebuilding the swapchain", vk_result_name(pr));
|
||||
droiddeck_log("gpu", "screen surface %s on present: rebuilding the swapchain", vk_result_name(pr));
|
||||
destroy_swapchain();
|
||||
return -1;
|
||||
} else if (pr == VK_SUBOPTIMAL_KHR || ar == VK_SUBOPTIMAL_KHR) {
|
||||
@@ -1757,9 +1799,9 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
* swap_init), so a rotated panel reports SUBOPTIMAL on every frame. Rebuilding would change
|
||||
* nothing (and a rebuild per frame would be far worse), so present as is; say so once. */
|
||||
static int said;
|
||||
if (!said) { said = 1; banner_log("gpu", "surface reports SUBOPTIMAL (panel rotation); presenting as is"); }
|
||||
if (!said) { said = 1; droiddeck_log("gpu", "surface reports SUBOPTIMAL (panel rotation); presenting as is"); }
|
||||
} else if (pr != VK_SUCCESS) {
|
||||
banner_log("error", "present: vkQueuePresentKHR failed (%s %d)", vk_result_name(pr), (int)pr);
|
||||
droiddeck_log("error", "present: vkQueuePresentKHR failed (%s %d)", vk_result_name(pr), (int)pr);
|
||||
destroy_swapchain();
|
||||
return -1;
|
||||
}
|
||||
@@ -1773,7 +1815,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
* launch/preloader overlay can dismiss (wayland has no XServer window-content hook). */
|
||||
if (drawn && !g_first_frame_done) {
|
||||
g_first_frame_done = 1;
|
||||
banner_on_first_frame();
|
||||
droiddeck_on_first_frame();
|
||||
}
|
||||
if (how) *how = hdr_how;
|
||||
return 0;
|
||||
@@ -1814,7 +1856,7 @@ void vkp_reset_first_frame(void) {
|
||||
void vkp_signal_first_frame(void) {
|
||||
if (g_first_frame_done) return;
|
||||
g_first_frame_done = 1;
|
||||
banner_on_first_frame();
|
||||
droiddeck_on_first_frame();
|
||||
}
|
||||
|
||||
int vkp_update_map(int scene_w, int scene_h) {
|
||||
@@ -2073,6 +2115,7 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
g_vk.BeginCommandBuffer(cmd, &bi);
|
||||
blendp_begin_frame();
|
||||
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
|
||||
/* Same acquisition as vkp_render's compositor pass: the sources come from the client's queue
|
||||
@@ -2132,9 +2175,8 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
|
||||
for (int i = 0; i < n; i++) {
|
||||
VkImageBlit blit;
|
||||
if (!draws[i].img || !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit)) continue;
|
||||
g_vk.CmdBlitImage(cmd, draws[i].img->image,
|
||||
draws[i].img->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
g_scene.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
|
||||
record_draw(cmd, &draws[i], &blit, g_scene.img, SCENE_FMT, scene_w, scene_h,
|
||||
VK_FILTER_LINEAR);
|
||||
}
|
||||
if (vkp_effects_active()) vkp_effects_set_formats(VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ struct vkp_draw {
|
||||
struct vkp_image *img;
|
||||
float sx, sy, sw, sh;
|
||||
int dx, dy, dw, dh;
|
||||
int blend; /* translucent: composed over what is under it (blend_pass.h) */
|
||||
};
|
||||
|
||||
// The GPU the renderer runs on ("Adreno (TM) 750"), empty before the device is up.
|
||||
@@ -121,12 +122,12 @@ int vkp_can_wait_sync_fd(void);
|
||||
|
||||
/* ---- HDR composition (hdr_compose.h) ----
|
||||
* An HDR game that cannot be alone on its display layer: the scene is composed into ONE encoding. */
|
||||
struct banner_color;
|
||||
struct droiddeck_color;
|
||||
struct vkp_hdr_frame {
|
||||
const unsigned char *is_hdr; /* per draw: 1 = its image holds PQ BT.2020 (the surface's description) */
|
||||
float peak_nits; /* the HDR content's peak, for a tone-map (max CLL / mastering max; 0 = 1000) */
|
||||
int tonemap; /* 1 = the drawer's HDR output switch is off: tone-map to SDR, never PQ */
|
||||
const struct banner_color *color; /* the topmost HDR draw's description: an HDR10 swapchain's metadata */
|
||||
const struct droiddeck_color *color; /* the topmost HDR draw's description: an HDR10 swapchain's metadata */
|
||||
};
|
||||
/* vkp_render for a scene with HDR draws (frame generation): composed into PQ and presented through an
|
||||
* HDR10 swapchain where the surface offers one (and hf->tonemap is 0), else tone-mapped into the ordinary
|
||||
@@ -183,6 +184,6 @@ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd);
|
||||
void vkp_pass_abort(void);
|
||||
|
||||
// Session log (compositor.c): one line to Download/Wayland-logs and logcat.
|
||||
void banner_log(const char *tag, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
void droiddeck_log(const char *tag, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
#include <jni.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
@@ -15,19 +16,20 @@
|
||||
#include <android/log.h>
|
||||
#include <android/native_window_jni.h>
|
||||
#include "vk_present.h"
|
||||
#include "banner_ext.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "ahb_swapchain.h"
|
||||
#include "sc_layer.h"
|
||||
#include "effects_chain.h"
|
||||
#include "banner_color.h"
|
||||
#include "droiddeck_color.h"
|
||||
|
||||
extern int banner_wayland_run(void);
|
||||
extern void banner_wayland_send_pointer(int action, int x, int y);
|
||||
extern void banner_wayland_send_touch(int action, int pointer_id, int x, int y);
|
||||
extern int banner_cursor_snapshot(int *out, int cap);
|
||||
extern void banner_wayland_send_key(int evdev, int state);
|
||||
extern void banner_wayland_send_scene_input(int type, int a, int b);
|
||||
extern void banner_wayland_vsync(int64_t frame_time_ns);
|
||||
extern int droiddeck_wayland_run(void);
|
||||
extern void droiddeck_wayland_send_pointer(int action, int x, int y);
|
||||
extern void droiddeck_wayland_send_touch(int action, int pointer_id, int x, int y);
|
||||
extern int droiddeck_cursor_snapshot(int *out, int cap);
|
||||
extern int droiddeck_cursor_serial(void);
|
||||
extern void droiddeck_wayland_send_key(int evdev, int state);
|
||||
extern void droiddeck_wayland_send_scene_input(int type, int a, int b);
|
||||
extern void droiddeck_wayland_vsync(int64_t frame_time_ns);
|
||||
extern volatile int g_fps_limit;
|
||||
extern volatile int g_hide_shell;
|
||||
extern volatile int g_zero_copy;
|
||||
@@ -37,7 +39,7 @@ extern volatile int g_no_render_node;
|
||||
extern volatile int g_output_refresh_mhz;
|
||||
extern volatile int g_output_w, g_output_h;
|
||||
|
||||
#define TAG "BannerWayland"
|
||||
#define TAG "DroidDeckWayland"
|
||||
|
||||
static JavaVM *g_jvm;
|
||||
static volatile pid_t g_comp_tid; /* the compositor thread, for the app's ADPF hint session */
|
||||
@@ -78,7 +80,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||
/* Called from vk_present.c on the compositor thread when the first client frame is
|
||||
* presented. Attaches to the JVM (this thread is a bare pthread) and calls back into
|
||||
* Java so the launch overlay can dismiss. Fires exactly once. */
|
||||
void banner_on_first_frame(void) {
|
||||
void droiddeck_on_first_frame(void) {
|
||||
if (!g_jvm || !g_compositor_cls || !g_on_first_frame) return;
|
||||
JNIEnv *env = NULL;
|
||||
int attached = 0;
|
||||
@@ -109,7 +111,7 @@ static JNIEnv *thread_env(void) {
|
||||
}
|
||||
|
||||
/* A window started presenting GPU frames (window = its description), or NULL when it closed. */
|
||||
void banner_on_game_surface(const char *window, const char *gpu) {
|
||||
void droiddeck_on_game_surface(const char *window, const char *gpu) {
|
||||
JNIEnv *env;
|
||||
if (!g_compositor_cls || !g_on_game_surface || !(env = thread_env())) return;
|
||||
jstring jw = window ? (*env)->NewStringUTF(env, window) : NULL;
|
||||
@@ -122,7 +124,7 @@ void banner_on_game_surface(const char *window, const char *gpu) {
|
||||
|
||||
/* The program behind the game window that just started presenting: its Linux pid and executable name
|
||||
* ("" = unknown). The app arms its launch-time CPU affinity on it. Compositor thread. */
|
||||
void banner_on_game_program(int pid, const char *program) {
|
||||
void droiddeck_on_game_program(int pid, const char *program) {
|
||||
JNIEnv *env;
|
||||
if (!g_compositor_cls || !g_on_game_program || !(env = thread_env())) return;
|
||||
jstring jp = (*env)->NewStringUTF(env, program ? program : "");
|
||||
@@ -132,7 +134,7 @@ void banner_on_game_program(int pid, const char *program) {
|
||||
}
|
||||
|
||||
/* One GPU frame from that window. */
|
||||
void banner_on_game_frame(void) {
|
||||
void droiddeck_on_game_frame(void) {
|
||||
JNIEnv *env;
|
||||
if (!g_compositor_cls || !g_on_game_frame || !(env = thread_env())) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_compositor_cls, g_on_game_frame);
|
||||
@@ -142,7 +144,7 @@ void banner_on_game_frame(void) {
|
||||
/* A program locked the pointer (locked = 1; the app's input path switches to deltas) or the lock
|
||||
* ended (locked = 0; x,y = where the pointer is now, in scene coordinates, for the app to re-sync
|
||||
* its own pointer to). Compositor thread. */
|
||||
void banner_on_pointer_lock(int locked, int x, int y) {
|
||||
void droiddeck_on_pointer_lock(int locked, int x, int y) {
|
||||
JNIEnv *env;
|
||||
if (!g_compositor_cls || !g_on_pointer_lock || !(env = thread_env())) return;
|
||||
(*env)->CallStaticVoidMethod(env, g_compositor_cls, g_on_pointer_lock, (jboolean)(locked != 0), (jint)x, (jint)y);
|
||||
@@ -151,7 +153,7 @@ void banner_on_pointer_lock(int locked, int x, int y) {
|
||||
|
||||
/* 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) {
|
||||
void droiddeck_on_clipboard_text(const char *utf8, int len) {
|
||||
JNIEnv *env;
|
||||
if (!g_compositor_cls || !g_on_clipboard || !(env = thread_env())) return;
|
||||
jbyteArray arr = (*env)->NewByteArray(env, len);
|
||||
@@ -164,7 +166,7 @@ void banner_on_clipboard_text(const char *utf8, int len) {
|
||||
|
||||
/* A program started (enabled) or stopped accepting IME text; x,y,w,h = its caret rectangle in
|
||||
* scene pixels (all 0 = unknown). Compositor thread. */
|
||||
void banner_on_text_input(int enabled, const char *program, int x, int y, int w, int h) {
|
||||
void droiddeck_on_text_input(int enabled, const char *program, int x, int y, int w, int h) {
|
||||
JNIEnv *env;
|
||||
if (!g_compositor_cls || !g_on_text_input || !(env = thread_env())) return;
|
||||
jstring jp = (*env)->NewStringUTF(env, program ? program : "");
|
||||
@@ -193,11 +195,11 @@ static void *comp_thread(void *arg) {
|
||||
if (before > COMPOSITOR_NICE && setpriority(PRIO_PROCESS, tid, COMPOSITOR_NICE) != 0) refused = errno ? errno : -1;
|
||||
const int after = getpriority(PRIO_PROCESS, tid);
|
||||
if (refused)
|
||||
banner_log("perf", "compositor thread %d \"wl-compositor\": stays at nice %d, a higher priority was refused (%s)",
|
||||
droiddeck_log("perf", "compositor thread %d \"wl-compositor\": stays at nice %d, a higher priority was refused (%s)",
|
||||
(int)tid, after, refused > 0 ? strerror(refused) : "?");
|
||||
else
|
||||
banner_log("perf", "compositor thread %d \"wl-compositor\": nice %d -> %d", (int)tid, before, after);
|
||||
banner_wayland_run();
|
||||
droiddeck_log("perf", "compositor thread %d \"wl-compositor\": nice %d -> %d", (int)tid, before, after);
|
||||
droiddeck_wayland_run();
|
||||
__android_log_print(ANDROID_LOG_INFO, TAG, "compositor thread exited");
|
||||
if (t_attached) (*g_jvm)->DetachCurrentThread(g_jvm);
|
||||
t_env = NULL; t_attached = 0;
|
||||
@@ -270,13 +272,13 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStartWithSurface(
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendPointer(
|
||||
JNIEnv *env, jclass clazz, jint action, jint x, jint y) {
|
||||
banner_wayland_send_pointer(action, x, y);
|
||||
droiddeck_wayland_send_pointer(action, x, y);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendTouch(
|
||||
JNIEnv *env, jclass clazz, jint action, jint pointer_id, jint x, jint y) {
|
||||
banner_wayland_send_touch(action, pointer_id, x, y);
|
||||
droiddeck_wayland_send_touch(action, pointer_id, x, y);
|
||||
}
|
||||
|
||||
/* The client's pointer image and whether it wants one at all (wl_pointer.set_cursor).
|
||||
@@ -289,29 +291,36 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot(
|
||||
jint cap = (*env)->GetArrayLength(env, out);
|
||||
jint *buf = (*env)->GetIntArrayElements(env, out, NULL);
|
||||
if (!buf) return 0;
|
||||
int n = banner_cursor_snapshot((int *)buf, (int)cap);
|
||||
int n = droiddeck_cursor_snapshot((int *)buf, (int)cap);
|
||||
(*env)->ReleaseIntArrayElements(env, out, buf, 0);
|
||||
return n;
|
||||
}
|
||||
|
||||
/* The serial nativeCursorSnapshot would return, without copying anything. */
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSerial(JNIEnv *env, jclass clazz) {
|
||||
(void)env; (void)clazz;
|
||||
return droiddeck_cursor_serial();
|
||||
}
|
||||
|
||||
/* Inject a key event. evdev = Linux input keycode (KEY_A=30…); state 1=down 0=up. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendKey(
|
||||
JNIEnv *env, jclass clazz, jint evdev, jint state) {
|
||||
banner_wayland_send_key(evdev, state);
|
||||
droiddeck_wayland_send_key(evdev, state);
|
||||
}
|
||||
|
||||
/* App X-server input in scene (virtual desktop) coordinates; see banner_wayland_send_scene_input. */
|
||||
/* App X-server input in scene (virtual desktop) coordinates; see droiddeck_wayland_send_scene_input. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendSceneInput(
|
||||
JNIEnv *env, jclass clazz, jint type, jint a, jint b) {
|
||||
banner_wayland_send_scene_input(type, a, b);
|
||||
droiddeck_wayland_send_scene_input(type, a, b);
|
||||
}
|
||||
|
||||
/* One screen refresh (Choreographer frame callback, UI thread): the compositor draws once. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeVsync(JNIEnv *env, jclass clazz, jlong frameTimeNanos) {
|
||||
banner_wayland_vsync((int64_t)frameTimeNanos);
|
||||
droiddeck_wayland_vsync((int64_t)frameTimeNanos);
|
||||
}
|
||||
|
||||
/* Shortcut launches: don't draw explorer's windows (desktop, taskbar), like X11's unviewable classes. */
|
||||
@@ -320,15 +329,15 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHideShell(JNIEnv
|
||||
g_hide_shell = hide ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Zero-copy layer mode (BANNER_WAYLAND_ZERO_COPY=1 at launch, the drawer's switch live): one fullscreen
|
||||
/* Zero-copy layer mode (DROIDDECK_WAYLAND_ZERO_COPY=1 at launch, the drawer's switch live): one fullscreen
|
||||
* window on its own Android layer instead of the swapchain blit (sc_layer.c / ahb_swapchain.c). Any
|
||||
* thread, any time: before the compositor starts it is the launch default; afterwards the compositor
|
||||
* thread flips the state, tells the bound games to rebuild their swapchains for it and redraws. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetZeroCopy(JNIEnv *env, jclass clazz, jboolean on) {
|
||||
int live = banner_get_display() != NULL;
|
||||
int live = droiddeck_get_display() != NULL;
|
||||
if (!live) g_zero_copy = on ? 1 : 0; /* read by ahb_swapchain_init before the queue drains */
|
||||
banner_host_zero_copy(on ? 1 : 0, live);
|
||||
droiddeck_host_zero_copy(on ? 1 : 0, live);
|
||||
__android_log_print(ANDROID_LOG_INFO, TAG, "zero-copy layer mode %s%s", on ? "on" : "off", live ? " (live)" : "");
|
||||
}
|
||||
|
||||
@@ -346,7 +355,7 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeZeroCopyFrames(JNIEn
|
||||
}
|
||||
|
||||
/* Compressed (UBWC) game buffers: advertise DRM_FORMAT_MOD_QCOM_COMPRESSED on zwp_linux_dmabuf_v1 when
|
||||
* the renderer's driver imports it (default on; BANNER_WAYLAND_UBWC=0 = off). Set before the compositor
|
||||
* the renderer's driver imports it (default on; DROIDDECK_WAYLAND_UBWC=0 = off). Set before the compositor
|
||||
* starts. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUbwc(JNIEnv *env, jclass clazz, jboolean on) {
|
||||
@@ -354,7 +363,7 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUbwc(JNIEnv *env,
|
||||
__android_log_print(ANDROID_LOG_INFO, TAG, "compressed (UBWC) game buffers %s", on ? "on" : "off");
|
||||
}
|
||||
|
||||
/* Debug: name no DRM device in the dma-buf feedback (BANNER_WAYLAND_NO_RENDER_NODE=1), the way a
|
||||
/* Debug: name no DRM device in the dma-buf feedback (DROIDDECK_WAYLAND_NO_RENDER_NODE=1), the way a
|
||||
* phone that exposes no /dev/dri node to apps does. Set before the compositor starts. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetNoRenderNode(JNIEnv *env, jclass clazz, jboolean on) {
|
||||
@@ -443,12 +452,12 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetScreenEffects(JNI
|
||||
}
|
||||
|
||||
/* One "display" line in the session log, written from Java: facts that live in the Android
|
||||
* framework (the panel's HDR capability) rather than in the compositor. banner_log() mirrors to
|
||||
* framework (the panel's HDR capability) rather than in the compositor. droiddeck_log() mirrors to
|
||||
* logcat and appends to the session file when one is open, so this is safe at any point. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogDisplay(JNIEnv *env, jclass clazz, jstring message) {
|
||||
char *s = dup_jstr(env, message);
|
||||
if (s) banner_log("display", "%s", s);
|
||||
if (s) droiddeck_log("display", "%s", s);
|
||||
free(s);
|
||||
}
|
||||
|
||||
@@ -457,18 +466,18 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogDisplay(JNIEnv *e
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogPerf(JNIEnv *env, jclass clazz, jstring message) {
|
||||
char *s = dup_jstr(env, message);
|
||||
if (s) banner_log("perf", "%s", s);
|
||||
if (s) droiddeck_log("perf", "%s", s);
|
||||
free(s);
|
||||
}
|
||||
|
||||
/* ---- HDR10 output, round 1 (banner_color.h / wl_color_mgmt.c) ---- */
|
||||
/* ---- HDR10 output, round 1 (droiddeck_color.h / wl_color_mgmt.c) ---- */
|
||||
|
||||
/* The opt-in: mode 0 off, 1 BANNER_WAYLAND_HDR=1, 2 =force (testing). Before the compositor starts. */
|
||||
/* The opt-in: mode 0 off, 1 DROIDDECK_WAYLAND_HDR=1, 2 =force (testing). Before the compositor starts. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrRequest(JNIEnv *env, jclass clazz, jint mode,
|
||||
jstring source, jboolean dxvkHdr, jboolean zeroCopyForced) {
|
||||
char *s = dup_jstr(env, source);
|
||||
banner_color_set_request((int)mode, s, dxvkHdr ? 1 : 0, zeroCopyForced ? 1 : 0);
|
||||
droiddeck_color_set_request((int)mode, s, dxvkHdr ? 1 : 0, zeroCopyForced ? 1 : 0);
|
||||
free(s);
|
||||
}
|
||||
|
||||
@@ -478,7 +487,7 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrDisplay(JNIEnv
|
||||
jstring formats, jboolean hdr10, jfloat maxLum, jfloat maxAvg, jfloat minLum, jboolean ratioAvailable,
|
||||
jfloat ratio, jint api) {
|
||||
char *n = dup_jstr(env, name), *f = dup_jstr(env, formats);
|
||||
banner_color_set_display((int)id, n, f, hdr10 ? 1 : 0, (float)maxLum, (float)maxAvg, (float)minLum,
|
||||
droiddeck_color_set_display((int)id, n, f, hdr10 ? 1 : 0, (float)maxLum, (float)maxAvg, (float)minLum,
|
||||
ratioAvailable ? 1 : 0, (float)ratio, (int)api);
|
||||
free(n); free(f);
|
||||
}
|
||||
@@ -487,38 +496,38 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrDisplay(JNIEnv
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrSdrRatioSample(JNIEnv *env, jclass clazz, jfloat ratio,
|
||||
jboolean listener) {
|
||||
banner_color_ratio_sample((float)ratio, listener ? 1 : 0);
|
||||
droiddeck_color_ratio_sample((float)ratio, listener ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ms since an HDR frame last reached a display layer, -1 = never this session. Any thread. */
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrLastFrameAgeMs(JNIEnv *env, jclass clazz) {
|
||||
return (jint)banner_color_last_frame_age_ms();
|
||||
return (jint)droiddeck_color_last_frame_age_ms();
|
||||
}
|
||||
|
||||
/* -1 = not decided yet, 0 = closed, 1 = open. Any thread. */
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrGateState(JNIEnv *env, jclass clazz) {
|
||||
return (jint)banner_color_gate_state();
|
||||
return (jint)droiddeck_color_gate_state();
|
||||
}
|
||||
|
||||
/* The session is ending: the "HDR on screen: ..." summary line. Any thread, once. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrSessionEnd(JNIEnv *env, jclass clazz) {
|
||||
banner_color_session_end();
|
||||
droiddeck_color_session_end();
|
||||
}
|
||||
|
||||
/* HDR frames really on screen right now (the HUD badge). Any thread. */
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrOnScreen(JNIEnv *env, jclass clazz) {
|
||||
return banner_color_hdr_on_screen() ? JNI_TRUE : JNI_FALSE;
|
||||
return droiddeck_color_hdr_on_screen() ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
/* One "color" line in the session log from Java. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogColor(JNIEnv *env, jclass clazz, jstring message) {
|
||||
char *s = dup_jstr(env, message);
|
||||
if (s) banner_log("color", "%s", s);
|
||||
if (s) droiddeck_log("color", "%s", s);
|
||||
free(s);
|
||||
}
|
||||
|
||||
@@ -527,7 +536,7 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogColor(JNIEnv *env
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLog(JNIEnv *env, jclass clazz, jstring area, jstring message) {
|
||||
char *a = dup_jstr(env, area), *s = dup_jstr(env, message);
|
||||
if (s) banner_log(a && a[0] ? a : "app", "%s", s);
|
||||
if (s) droiddeck_log(a && a[0] ? a : "app", "%s", s);
|
||||
free(a);
|
||||
free(s);
|
||||
}
|
||||
@@ -535,65 +544,65 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLog(JNIEnv *env, jcl
|
||||
/* SDR content's level inside an HDR picture, in nits (default 203). */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrSdrWhite(JNIEnv *env, jclass clazz, jfloat nits) {
|
||||
banner_color_set_sdr_white((float)nits);
|
||||
droiddeck_color_set_sdr_white((float)nits);
|
||||
}
|
||||
|
||||
/* The drawer's live HDR output switch: on = HDR frames as HDR, off = the same frames tone-mapped to SDR.
|
||||
* Applied on the compositor thread (logged there, with a redraw). */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrOutput(JNIEnv *env, jclass clazz, jboolean on) {
|
||||
if (banner_get_display()) banner_host_hdr_output(on ? 1 : 0);
|
||||
else banner_color_set_output(on ? 1 : 0); /* no compositor thread yet: nothing is drawing either */
|
||||
if (droiddeck_get_display()) droiddeck_host_hdr_output(on ? 1 : 0);
|
||||
else droiddeck_color_set_output(on ? 1 : 0); /* no compositor thread yet: nothing is drawing either */
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrOutput(JNIEnv *env, jclass clazz) {
|
||||
return banner_color_output() ? JNI_TRUE : JNI_FALSE;
|
||||
return droiddeck_color_output() ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
/* Device evidence for the HDR lines: thermal status + headroom, brightness + mode. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrEnvSample(JNIEnv *env, jclass clazz, jint thermal,
|
||||
jfloat headroom, jint brightness, jint mode) {
|
||||
banner_color_env_sample((int)thermal, (float)headroom != (float)headroom ? -1.0f : (float)headroom,
|
||||
droiddeck_color_env_sample((int)thermal, (float)headroom != (float)headroom ? -1.0f : (float)headroom,
|
||||
(int)brightness, (int)mode);
|
||||
}
|
||||
|
||||
/* Display.getHighestHdrSdrRatio() (Android 16+), <= 0 = not reported. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrHighestRatio(JNIEnv *env, jclass clazz, jfloat ratio) {
|
||||
banner_color_set_highest_ratio((float)ratio == (float)ratio ? (float)ratio : -1.0f);
|
||||
droiddeck_color_set_highest_ratio((float)ratio == (float)ratio ? (float)ratio : -1.0f);
|
||||
}
|
||||
|
||||
/* The HDR headroom the screen surface should ask for (HDR10 swapchain frames in the last 1.5 s), 0 = none. */
|
||||
JNIEXPORT jfloat JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroom(JNIEnv *env, jclass clazz) {
|
||||
return (jfloat)banner_color_screen_headroom(NULL, 0);
|
||||
return (jfloat)droiddeck_color_screen_headroom(NULL, 0);
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroomWhy(JNIEnv *env, jclass clazz) {
|
||||
char why[200];
|
||||
banner_color_screen_headroom(why, sizeof(why));
|
||||
droiddeck_color_screen_headroom(why, sizeof(why));
|
||||
return (*env)->NewStringUTF(env, why);
|
||||
}
|
||||
|
||||
/* The app's screen-surface request, for the no-headroom lines: > 0 asked, 0 cleared, -1 not possible. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrNoteHeadroomRequest(JNIEnv *env, jclass clazz, jfloat ratio) {
|
||||
banner_color_note_headroom_request((float)ratio);
|
||||
droiddeck_color_note_headroom_request((float)ratio);
|
||||
}
|
||||
|
||||
/* 0 none, 1 HDR frames on screen with headroom, 2 HDR frames on screen without headroom for 5 s+. */
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrState(JNIEnv *env, jclass clazz) {
|
||||
return (jint)banner_color_hdr_state();
|
||||
return (jint)droiddeck_color_hdr_state();
|
||||
}
|
||||
|
||||
/* An HDR game's frames were shown tone-mapped to SDR in the last 1.5 s (the drawer's status line). */
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrToneMappedOnScreen(JNIEnv *env, jclass clazz) {
|
||||
return banner_color_tonemapped_on_screen() ? JNI_TRUE : JNI_FALSE;
|
||||
return droiddeck_color_tonemapped_on_screen() ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
/* The Look the controls currently match (null = Custom) - only named in the session log. */
|
||||
@@ -642,7 +651,7 @@ JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNIEnv *env, jclass clazz, jbyteArray utf8) {
|
||||
int len;
|
||||
char *buf = dup_bytes(env, utf8, &len);
|
||||
banner_host_clipboard_text(buf ? buf : "", len);
|
||||
droiddeck_host_clipboard_text(buf ? buf : "", len);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
@@ -651,7 +660,7 @@ JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputCommit(JNIEnv *env, jclass clazz, jbyteArray utf8) {
|
||||
int len;
|
||||
char *buf = dup_bytes(env, utf8, &len);
|
||||
if (buf && len) banner_host_text_commit(buf, len);
|
||||
if (buf && len) droiddeck_host_text_commit(buf, len);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
@@ -661,12 +670,38 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputPreedit(JNI
|
||||
jint cursorBegin, jint cursorEnd) {
|
||||
int len;
|
||||
char *buf = dup_bytes(env, utf8, &len);
|
||||
banner_host_text_preedit(buf ? buf : "", len, cursorBegin, cursorEnd);
|
||||
droiddeck_host_text_preedit(buf ? buf : "", len, cursorBegin, cursorEnd);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* The IME deleted characters around the caret. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputDelete(JNIEnv *env, jclass clazz, jint before, jint after) {
|
||||
banner_host_text_delete(before, after);
|
||||
droiddeck_host_text_delete(before, after);
|
||||
}
|
||||
|
||||
/* KGSL's power control, off (the GPU held at its top clock) or back on. The property is the
|
||||
* device's, not this process's: it outlives the fd and the app, so the caller clears it at stop
|
||||
* and again at every app start. No root; the kernel's thermal limits still apply. */
|
||||
extern void adrenotools_set_turbo(bool turbo);
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTurbo(JNIEnv *env, jclass clazz, jboolean on) {
|
||||
(void)env; (void)clazz;
|
||||
adrenotools_set_turbo(on == JNI_TRUE);
|
||||
}
|
||||
|
||||
/* Raises one of our own processes or threads to `nice` with a bare setpriority(), never lowering
|
||||
* it. android.os.Process.setThreadPriority is not used for this: it also moves the thread between
|
||||
* scheduling groups by priority, and a guest process must stay where Android put the app. Returns
|
||||
* the nice value it is left at, or 100 when it could not be read. */
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeRaisePriority(JNIEnv *env, jclass clazz, jint tid, jint nice) {
|
||||
(void)env; (void)clazz;
|
||||
errno = 0;
|
||||
int before = getpriority(PRIO_PROCESS, (id_t)tid);
|
||||
if (before == -1 && errno != 0) return 100;
|
||||
if (before > nice) setpriority(PRIO_PROCESS, (id_t)tid, nice);
|
||||
errno = 0;
|
||||
int after = getpriority(PRIO_PROCESS, (id_t)tid);
|
||||
return after == -1 && errno != 0 ? 100 : after;
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
*
|
||||
* Guest → Android: when a program sets a selection with a text mime, the compositor asks for it
|
||||
* once over a pipe on the event loop (non-blocking, 1 MiB cap, 3 s timeout) and hands the text
|
||||
* to the app (banner_on_clipboard_text → ClipboardManager). Android → guest: the app's text
|
||||
* to the app (droiddeck_on_clipboard_text → ClipboardManager). Android → guest: the app's text
|
||||
* becomes the HOST selection; receive requests are answered with a non-blocking writer.
|
||||
*/
|
||||
#define _GNU_SOURCE 1
|
||||
@@ -25,7 +25,7 @@
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include "banner_ext.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "wlr-data-control-unstable-v1-server-protocol.h"
|
||||
|
||||
#define CLIP_MAX_BYTES (1024 * 1024)
|
||||
@@ -130,7 +130,7 @@ static int on_writer_writable(int fd, uint32_t mask, void *data) {
|
||||
|
||||
static int on_writer_timeout(void *data) {
|
||||
struct writer *w = data;
|
||||
banner_log("clipboard", "a program never read the clipboard text it asked for; giving up");
|
||||
droiddeck_log("clipboard", "a program never read the clipboard text it asked for; giving up");
|
||||
writer_free(w);
|
||||
return 0;
|
||||
}
|
||||
@@ -179,11 +179,11 @@ static void reader_finish(struct reader *r) {
|
||||
free(g_last_guest_text);
|
||||
g_last_guest_text = copy;
|
||||
g_last_guest_len = r->len;
|
||||
banner_log("clipboard", "guest copied %zu bytes (%s)", r->len, r->mime);
|
||||
banner_on_clipboard_text(copy, (int)r->len);
|
||||
droiddeck_log("clipboard", "guest copied %zu bytes (%s)", r->len, r->mime);
|
||||
droiddeck_on_clipboard_text(copy, (int)r->len);
|
||||
}
|
||||
} else {
|
||||
banner_log("clipboard", "guest copied an empty text; Android clipboard left alone");
|
||||
droiddeck_log("clipboard", "guest copied an empty text; Android clipboard left alone");
|
||||
}
|
||||
reader_free(r);
|
||||
}
|
||||
@@ -192,7 +192,7 @@ static int on_reader_readable(int fd, uint32_t mask, void *data) {
|
||||
struct reader *r = data;
|
||||
for (;;) {
|
||||
if (r->len >= CLIP_MAX_BYTES) {
|
||||
banner_log("clipboard", "guest text exceeds %d bytes; dropped", CLIP_MAX_BYTES);
|
||||
droiddeck_log("clipboard", "guest text exceeds %d bytes; dropped", CLIP_MAX_BYTES);
|
||||
reader_free(r);
|
||||
return 0;
|
||||
}
|
||||
@@ -215,7 +215,7 @@ static int on_reader_readable(int fd, uint32_t mask, void *data) {
|
||||
|
||||
static int on_reader_timeout(void *data) {
|
||||
struct reader *r = data;
|
||||
banner_log("clipboard", "timed out reading the guest's clipboard text (%zu bytes so far)", r->len);
|
||||
droiddeck_log("clipboard", "timed out reading the guest's clipboard text (%zu bytes so far)", r->len);
|
||||
reader_free(r);
|
||||
return 0;
|
||||
}
|
||||
@@ -251,7 +251,7 @@ static void offer_receive(struct wl_client *c, struct wl_resource *r, const char
|
||||
if (!o || o->gen != g_gen) { close(fd); return; } /* stale offer */
|
||||
if (g_owner == OWNER_HOST) {
|
||||
if (is_text_mime(mime)) {
|
||||
banner_log("clipboard", "Android clipboard → guest (%zu bytes) for %s", g_host_len, banner_client_name(c));
|
||||
droiddeck_log("clipboard", "Android clipboard → guest (%zu bytes) for %s", g_host_len, droiddeck_client_name(c));
|
||||
writer_start(fd, g_host_text, g_host_len);
|
||||
} else {
|
||||
close(fd);
|
||||
@@ -327,7 +327,7 @@ static void broadcast_selection(void) {
|
||||
wl_display_flush_clients(g_disp);
|
||||
}
|
||||
|
||||
void banner_clipboard_keyboard_focus(struct wl_client *client) {
|
||||
void droiddeck_clipboard_keyboard_focus(struct wl_client *client) {
|
||||
struct device *d;
|
||||
if (client == g_kb_client) return;
|
||||
g_kb_client = client;
|
||||
@@ -345,7 +345,7 @@ static void source_res_destroy(struct wl_resource *r) {
|
||||
g_src = NULL;
|
||||
g_owner = OWNER_NONE;
|
||||
if (g_reader) reader_free(g_reader);
|
||||
banner_log("clipboard", "%s withdrew its clipboard contents", banner_client_name(wl_resource_get_client(r)));
|
||||
droiddeck_log("clipboard", "%s withdrew its clipboard contents", droiddeck_client_name(wl_resource_get_client(r)));
|
||||
broadcast_selection();
|
||||
}
|
||||
for (int i = 0; i < s->nmimes; i++) free(s->mimes[i]);
|
||||
@@ -396,9 +396,9 @@ static void set_selection(struct wl_client *c, struct wl_resource *source_res) {
|
||||
if (*list) strncat(list, ", ", sizeof(list) - strlen(list) - 1);
|
||||
strncat(list, s->mimes[i], sizeof(list) - strlen(list) - 1);
|
||||
}
|
||||
banner_log("clipboard", "selection set by %s (%s)", banner_client_name(c), *list ? list : "no mime types");
|
||||
droiddeck_log("clipboard", "selection set by %s (%s)", droiddeck_client_name(c), *list ? list : "no mime types");
|
||||
} else {
|
||||
banner_log("clipboard", "%s cleared the clipboard", banner_client_name(c));
|
||||
droiddeck_log("clipboard", "%s cleared the clipboard", droiddeck_client_name(c));
|
||||
}
|
||||
broadcast_selection();
|
||||
read_client_text();
|
||||
@@ -415,7 +415,7 @@ static void device_res_destroy(struct wl_resource *r) {
|
||||
|
||||
static void device_start_drag(struct wl_client *c, struct wl_resource *r, struct wl_resource *source,
|
||||
struct wl_resource *origin, struct wl_resource *icon, uint32_t serial) {
|
||||
banner_log("clipboard", "%s started a drag and drop; not supported, cancelled", banner_client_name(c));
|
||||
droiddeck_log("clipboard", "%s started a drag and drop; not supported, cancelled", droiddeck_client_name(c));
|
||||
if (source) {
|
||||
struct source *s = wl_resource_get_user_data(source);
|
||||
if (s && s != g_src) source_send_cancelled(s);
|
||||
@@ -512,7 +512,7 @@ void clipboard_host_text(char *utf8, size_t len) {
|
||||
if (!len) free(utf8);
|
||||
g_host_len = len;
|
||||
g_owner = len ? OWNER_HOST : OWNER_NONE;
|
||||
banner_log("clipboard", len ? "Android clipboard → guest (%zu bytes)" : "Android clipboard cleared (%zu bytes)", len);
|
||||
droiddeck_log("clipboard", len ? "Android clipboard → guest (%zu bytes)" : "Android clipboard cleared (%zu bytes)", len);
|
||||
broadcast_selection();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* 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.
|
||||
* HDR evidence. See droiddeck_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
|
||||
* errors are only raised where the protocol leaves no choice and a conforming client can never hit
|
||||
@@ -12,8 +12,8 @@
|
||||
* for the same surface replaces the first, a request on an inert object is ignored.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "banner_color.h"
|
||||
#include "banner_ext.h"
|
||||
#include "droiddeck_color.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "ahb_swapchain.h"
|
||||
#include "sc_layer.h"
|
||||
#include "color-management-v1-server-protocol.h"
|
||||
@@ -30,7 +30,7 @@ extern volatile int g_zero_copy;
|
||||
#define TAG "color"
|
||||
|
||||
/* AHARDWAREBUFFER_FORMAT_* (android/hardware_buffer.h), for the log only. */
|
||||
const char *banner_ahb_format_name(uint32_t f) {
|
||||
const char *droiddeck_ahb_format_name(uint32_t f) {
|
||||
static char other[32];
|
||||
switch (f) {
|
||||
case 1: return "RGBA8888 (8-bit)";
|
||||
@@ -73,9 +73,9 @@ static const char *tf_name(uint32_t tf) {
|
||||
|
||||
static const char *dataspace_name(int32_t ds) {
|
||||
switch (ds) {
|
||||
case BANNER_ADATASPACE_BT2020_PQ: return "BT2020_PQ";
|
||||
case BANNER_ADATASPACE_BT2020_HLG: return "BT2020_HLG";
|
||||
case BANNER_ADATASPACE_UNKNOWN: return "UNKNOWN (sRGB)";
|
||||
case DROIDDECK_ADATASPACE_BT2020_PQ: return "BT2020_PQ";
|
||||
case DROIDDECK_ADATASPACE_BT2020_HLG: return "BT2020_HLG";
|
||||
case DROIDDECK_ADATASPACE_UNKNOWN: return "UNKNOWN (sRGB)";
|
||||
default: return "other";
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ static _Atomic int g_gate = -1; /* -1 undecided, 0 closed, 1 open */
|
||||
static char g_gate_why[320]; /* the closed gate's reason (for the summary) */
|
||||
static _Atomic int g_output_on = 1; /* the drawer's HDR output switch (per session, starts on) */
|
||||
|
||||
void banner_color_set_request(int mode, const char *source, int dxvk_hdr, int zero_copy_forced) {
|
||||
void droiddeck_color_set_request(int mode, const char *source, int dxvk_hdr, int zero_copy_forced) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_req.mode = mode < 0 ? 0 : mode > 2 ? 2 : mode;
|
||||
snprintf(g_req.source, sizeof(g_req.source), "%s", source ? source : "environment");
|
||||
@@ -109,7 +109,7 @@ void banner_color_set_request(int mode, const char *source, int dxvk_hdr, int ze
|
||||
atomic_store(&g_output_on, 1); /* the drawer's switch is per session and starts on */
|
||||
}
|
||||
|
||||
void banner_color_set_display(int id, const char *name, const char *formats, int hdr10, float max_lum,
|
||||
void droiddeck_color_set_display(int id, const char *name, const char *formats, int hdr10, float max_lum,
|
||||
float max_avg, float min_lum, int ratio_available, float ratio, int api) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int was_known = g_req.display_known, was_hdr10 = g_req.hdr10, was_id = g_req.display_id;
|
||||
@@ -127,17 +127,17 @@ void banner_color_set_display(int id, const char *name, const char *formats, int
|
||||
* session is recorded, and what it means is said out loud. */
|
||||
if (was_known && atomic_load(&g_gate) >= 0 && (was_hdr10 != (hdr10 ? 1 : 0) || was_id != id)) {
|
||||
if (atomic_load(&g_gate) == 1 && !hdr10)
|
||||
banner_log(TAG, "the game is now on \"%s\" (display %d), which reports no HDR10: HDR frames stay tagged "
|
||||
droiddeck_log(TAG, "the game is now on \"%s\" (display %d), which reports no HDR10: HDR frames stay tagged "
|
||||
"BT2020_PQ and SurfaceFlinger tone-maps them for this display (the offer to games is fixed "
|
||||
"for the session)", name ? name : "?", id);
|
||||
else if (atomic_load(&g_gate) == 0 && hdr10)
|
||||
banner_log(TAG, "the game is now on \"%s\" (display %d), which reports HDR10 - HDR stays off for this "
|
||||
droiddeck_log(TAG, "the game is now on \"%s\" (display %d), which reports HDR10 - HDR stays off for this "
|
||||
"session (the gate is decided when the compositor starts); relaunch to use it", name ? name : "?", id);
|
||||
}
|
||||
}
|
||||
|
||||
int banner_color_gate_state(void) { return atomic_load(&g_gate); }
|
||||
int banner_color_hdr_open(void) { return atomic_load(&g_gate) == 1; }
|
||||
int droiddeck_color_gate_state(void) { return atomic_load(&g_gate); }
|
||||
int droiddeck_color_hdr_open(void) { return atomic_load(&g_gate) == 1; }
|
||||
|
||||
/* ---------------------------------------------------------------- HDR evidence (summary) */
|
||||
|
||||
@@ -170,7 +170,7 @@ static struct {
|
||||
static _Atomic int64_t g_last_frame_ns;
|
||||
static _Atomic int64_t g_last_tm_ns; /* the last HDR frame shown tone-mapped to SDR */
|
||||
|
||||
/* What the device says beside the headroom (the app, non-root APIs; banner_color_env_*). The Fold showed
|
||||
/* What the device says beside the headroom (the app, non-root APIs; droiddeck_color_env_*). The Fold showed
|
||||
* three ways to lose HDR headroom with HDR frames on screen - a screen recording, heat under load, and
|
||||
* (plausibly) the brightness slider at maximum - so every no-headroom line carries this. Under g_mu. */
|
||||
static struct {
|
||||
@@ -241,20 +241,20 @@ static _Atomic int g_screen_hr_x1000; /* the screen surface's wanted headr
|
||||
static _Atomic int64_t g_screen_hr_ns; /* the last HDR10-swapchain frame */
|
||||
static char g_screen_hr_why[200]; /* under g_mu */
|
||||
|
||||
static float peak_of(const struct banner_color *c, float disp_max, const char **src) {
|
||||
static float peak_of(const struct droiddeck_color *c, float disp_max, const char **src) {
|
||||
if (c->max_cll > 0.0f) { *src = "max CLL"; return c->max_cll; }
|
||||
if (c->has_st2086 && c->max_lum > 0.0f) { *src = "mastering max"; return c->max_lum; }
|
||||
if (disp_max > 0.0f) { *src = "the display's peak"; return disp_max; }
|
||||
*src = "assumed"; return 1000.0f;
|
||||
}
|
||||
|
||||
float banner_color_desired_headroom(const struct banner_color *c, char *why, size_t n) {
|
||||
float droiddeck_color_desired_headroom(const struct droiddeck_color *c, char *why, size_t n) {
|
||||
if (!c || !c->dataspace) { if (why && n) why[0] = 0; return 0.0f; }
|
||||
pthread_mutex_lock(&g_mu);
|
||||
const float disp_max = g_req.max_lum, highest = g_req.highest_ratio;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
const char *src;
|
||||
const float peak = peak_of(c, disp_max, &src), white = banner_color_sdr_white();
|
||||
const float peak = peak_of(c, disp_max, &src), white = droiddeck_color_sdr_white();
|
||||
if (highest > 0.0f && highest <= 1.01f) {
|
||||
if (why && n) snprintf(why, n, "the display reports its highest HDR/SDR ratio as %.2f: no app can get a "
|
||||
"boost here, so nothing is asked", highest);
|
||||
@@ -273,24 +273,24 @@ float banner_color_desired_headroom(const struct banner_color *c, char *why, siz
|
||||
return r;
|
||||
}
|
||||
|
||||
void banner_color_note_headroom_request(float ratio) {
|
||||
void droiddeck_color_note_headroom_request(float ratio) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_env.requested = ratio;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
|
||||
void banner_color_set_highest_ratio(float ratio) {
|
||||
void droiddeck_color_set_highest_ratio(float ratio) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
const float was = g_req.highest_ratio;
|
||||
g_req.highest_ratio = ratio > 0.0f ? ratio : -1.0f;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (ratio > 0.0f && (was < 0.0f || was != ratio) && atomic_load(&g_gate) == 1)
|
||||
banner_log(TAG, "the display's highest HDR/SDR ratio is %.2f%s", ratio,
|
||||
droiddeck_log(TAG, "the display's highest HDR/SDR ratio is %.2f%s", ratio,
|
||||
ratio <= 1.01f ? " - it reports no HDR boost at all: no app can raise HDR highlights above SDR white here"
|
||||
: " (the most HDR headroom it can give)");
|
||||
}
|
||||
|
||||
float banner_color_screen_headroom(char *why, size_t n) {
|
||||
float droiddeck_color_screen_headroom(char *why, size_t n) {
|
||||
const int64_t t = atomic_load(&g_screen_hr_ns);
|
||||
if (!t || now_ns() - t > 1500000000LL) { if (why && n) why[0] = 0; return 0.0f; }
|
||||
if (why && n) {
|
||||
@@ -301,7 +301,7 @@ float banner_color_screen_headroom(char *why, size_t n) {
|
||||
return atomic_load(&g_screen_hr_x1000) / 1000.0f;
|
||||
}
|
||||
|
||||
int banner_color_last_frame_age_ms(void) {
|
||||
int droiddeck_color_last_frame_age_ms(void) {
|
||||
int64_t t = atomic_load(&g_last_frame_ns);
|
||||
if (!t) return -1;
|
||||
int64_t age = (now_ns() - t) / 1000000LL;
|
||||
@@ -408,34 +408,34 @@ static void log_verdict(int force) {
|
||||
int changed = strcmp(v, g_hdr.verdict) != 0;
|
||||
if (changed) snprintf(g_hdr.verdict, sizeof(g_hdr.verdict), "%s", v);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (changed || force) banner_log(TAG, "HDR on screen: %s", v);
|
||||
if (changed || force) droiddeck_log(TAG, "HDR on screen: %s", v);
|
||||
}
|
||||
|
||||
void banner_color_frame_shown(const struct banner_color *c, int path, uint32_t ahb_format) {
|
||||
void droiddeck_color_frame_shown(const struct droiddeck_color *c, int path, uint32_t ahb_format) {
|
||||
if (!c || !c->dataspace) return;
|
||||
int first8 = 0;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
switch (path) {
|
||||
case BANNER_HDR_ZERO_COPY:
|
||||
case DROIDDECK_HDR_ZERO_COPY:
|
||||
if (ahb_format == 0x2b) g_hdr.layer_10bit++;
|
||||
g_hdr.win_zc++;
|
||||
snprintf(g_hdr.layer_fmt, sizeof(g_hdr.layer_fmt), "%s", banner_ahb_format_name(ahb_format));
|
||||
snprintf(g_hdr.layer_fmt, sizeof(g_hdr.layer_fmt), "%s", droiddeck_ahb_format_name(ahb_format));
|
||||
break;
|
||||
case BANNER_HDR_LAYER_COPY:
|
||||
case DROIDDECK_HDR_LAYER_COPY:
|
||||
if (!g_hdr.layer_copy8) first8 = 1;
|
||||
g_hdr.layer_copy8++; g_hdr.win_copy8++;
|
||||
snprintf(g_hdr.layer_fmt, sizeof(g_hdr.layer_fmt), "RGBA8888 layer copy");
|
||||
break;
|
||||
case BANNER_HDR_COMPOSED:
|
||||
case DROIDDECK_HDR_COMPOSED:
|
||||
g_hdr.composed++; g_hdr.win_composed++;
|
||||
snprintf(g_hdr.layer_fmt, sizeof(g_hdr.layer_fmt), "composed %s", banner_ahb_format_name(ahb_format));
|
||||
snprintf(g_hdr.layer_fmt, sizeof(g_hdr.layer_fmt), "composed %s", droiddeck_ahb_format_name(ahb_format));
|
||||
break;
|
||||
case BANNER_HDR_SWAPCHAIN:
|
||||
case DROIDDECK_HDR_SWAPCHAIN:
|
||||
g_hdr.swapchain++; g_hdr.win_swapchain++;
|
||||
snprintf(g_hdr.layer_fmt, sizeof(g_hdr.layer_fmt), "HDR10 swapchain");
|
||||
atomic_store(&g_screen_hr_ns, now_ns());
|
||||
break;
|
||||
default: /* BANNER_HDR_TONEMAPPED: shown, but not as HDR */
|
||||
default: /* DROIDDECK_HDR_TONEMAPPED: shown, but not as HDR */
|
||||
g_hdr.tonemapped++; g_hdr.win_tonemapped++;
|
||||
if (!atomic_load(&g_output_on)) g_hdr.tm_off++;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
@@ -446,7 +446,7 @@ void banner_color_frame_shown(const struct banner_color *c, int path, uint32_t a
|
||||
g_hdr.win_layer++;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
atomic_store(&g_last_frame_ns, now_ns());
|
||||
if (path == BANNER_HDR_SWAPCHAIN) {
|
||||
if (path == DROIDDECK_HDR_SWAPCHAIN) {
|
||||
/* The screen surface's headroom request (the app applies it): worked out again when the
|
||||
* description changes, else once a second. */
|
||||
static uint32_t ident;
|
||||
@@ -454,7 +454,7 @@ void banner_color_frame_shown(const struct banner_color *c, int path, uint32_t a
|
||||
const int64_t t = now_ns();
|
||||
if (c->identity != ident || t - calc_ns > 1000000000LL) {
|
||||
char why[200];
|
||||
const float r = banner_color_desired_headroom(c, why, sizeof(why));
|
||||
const float r = droiddeck_color_desired_headroom(c, why, sizeof(why));
|
||||
ident = c->identity; calc_ns = t;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
snprintf(g_screen_hr_why, sizeof(g_screen_hr_why), "%s", why);
|
||||
@@ -463,12 +463,12 @@ void banner_color_frame_shown(const struct banner_color *c, int path, uint32_t a
|
||||
}
|
||||
}
|
||||
if (first8)
|
||||
banner_log(TAG, "HDR frames are reaching the display layer through the compositor's 8-bit layer copy (the game's "
|
||||
droiddeck_log(TAG, "HDR frames are reaching the display layer through the compositor's 8-bit layer copy (the game's "
|
||||
"buffer is not a gralloc buffer this frame): colours stay correct (the frame keeps its BT2020_PQ tag), "
|
||||
"precision drops to 8 bits (banding possible)");
|
||||
}
|
||||
|
||||
void banner_color_frame_copied(const char *who, const char *reason) {
|
||||
void droiddeck_color_frame_copied(const char *who, const char *reason) {
|
||||
int first = 0;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_hdr.copy_frames++;
|
||||
@@ -479,14 +479,14 @@ void banner_color_frame_copied(const char *who, const char *reason) {
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (first)
|
||||
banner_log(TAG, "HDR frames of %s go through the compositor's 8-bit SDR copy now, because %s: shown WITHOUT "
|
||||
droiddeck_log(TAG, "HDR frames of %s go through the compositor's 8-bit SDR copy now, because %s: shown WITHOUT "
|
||||
"tone mapping (washed out) until that changes", who ? who : "a window", reason ? reason : "?");
|
||||
}
|
||||
|
||||
void banner_color_ratio_sample(float ratio, int listener) {
|
||||
void droiddeck_color_ratio_sample(float ratio, int listener) {
|
||||
if (atomic_load(&g_gate) != 1) return;
|
||||
int64_t t = now_ns();
|
||||
int age = banner_color_last_frame_age_ms();
|
||||
int age = droiddeck_color_last_frame_age_ms();
|
||||
int live = age >= 0 && age < 1500;
|
||||
int log_it = 0, periodic = 0, nohead_now = 0, head_back = 0;
|
||||
float was;
|
||||
@@ -536,28 +536,28 @@ void banner_color_ratio_sample(float ratio, int listener) {
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (nohead_now)
|
||||
banner_log(TAG, "no HDR headroom for 5 s while HDR frames are on screen (display HDR/SDR ratio %.2f): %s - Android "
|
||||
droiddeck_log(TAG, "no HDR headroom for 5 s while HDR frames are on screen (display HDR/SDR ratio %.2f): %s - Android "
|
||||
"drops HDR headroom while the screen is recorded and when the device is hot, and some phones never "
|
||||
"boost HDR for apps; HDR highlights look no brighter until that ends [%s]", ratio, causes, env);
|
||||
if (head_back)
|
||||
banner_log(TAG, "HDR headroom is back: display HDR/SDR ratio %.2f with HDR frames on screen", ratio);
|
||||
droiddeck_log(TAG, "HDR headroom is back: display HDR/SDR ratio %.2f with HDR frames on screen", ratio);
|
||||
if (!log_it) return;
|
||||
char when[64];
|
||||
if (age < 0) snprintf(when, sizeof(when), "none yet this session");
|
||||
else snprintf(when, sizeof(when), "%s, last one %d ms ago", live ? "yes" : "no", age);
|
||||
if (periodic)
|
||||
banner_log(TAG, "display HDR/SDR ratio %.2f (steady; HDR frames on screen: %s)%s%s%s%s%s", ratio, when,
|
||||
droiddeck_log(TAG, "display HDR/SDR ratio %.2f (steady; HDR frames on screen: %s)%s%s%s%s%s", ratio, when,
|
||||
live && ratio <= 1.01f ? " - no HDR headroom: " : "", live && ratio <= 1.01f ? causes : "",
|
||||
env[0] ? " [" : "", env, env[0] ? "]" : "");
|
||||
else if (was > 0.0f)
|
||||
banner_log(TAG, "display HDR/SDR ratio %.2f (was %.2f; HDR frames on screen: %s) [%s]", ratio, was, when,
|
||||
droiddeck_log(TAG, "display HDR/SDR ratio %.2f (was %.2f; HDR frames on screen: %s) [%s]", ratio, was, when,
|
||||
listener ? "display listener" : "sampler");
|
||||
else
|
||||
banner_log(TAG, "display HDR/SDR ratio %.2f (first reading; HDR frames on screen: %s) [%s]", ratio, when,
|
||||
droiddeck_log(TAG, "display HDR/SDR ratio %.2f (first reading; HDR frames on screen: %s) [%s]", ratio, when,
|
||||
listener ? "display listener" : "sampler");
|
||||
}
|
||||
|
||||
void banner_color_stats_tick(void) {
|
||||
void droiddeck_color_stats_tick(void) {
|
||||
if (atomic_load(&g_gate) != 1) return;
|
||||
char line[512];
|
||||
int any;
|
||||
@@ -582,13 +582,13 @@ void banner_color_stats_tick(void) {
|
||||
g_hdr.win_zc = g_hdr.win_composed = g_hdr.win_swapchain = g_hdr.win_tonemapped = 0;
|
||||
g_hdr.win_ratio_n = 0;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (any) banner_log(TAG, "%s", line);
|
||||
if (any) droiddeck_log(TAG, "%s", line);
|
||||
log_verdict(0);
|
||||
}
|
||||
|
||||
int banner_color_hdr_state(void) {
|
||||
int droiddeck_color_hdr_state(void) {
|
||||
if (atomic_load(&g_gate) != 1) return 0;
|
||||
int age = banner_color_last_frame_age_ms();
|
||||
int age = droiddeck_color_last_frame_age_ms();
|
||||
if (age < 0 || age >= 1500) return 0;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int confirmed = !g_hdr.ratio_n || g_hdr.ratio_last > 1.01f; /* no ratio on this display: the tag is all there is */
|
||||
@@ -597,26 +597,26 @@ int banner_color_hdr_state(void) {
|
||||
return confirmed ? 1 : nohead ? 2 : 0;
|
||||
}
|
||||
|
||||
int banner_color_hdr_on_screen(void) { return banner_color_hdr_state() == 1; }
|
||||
int droiddeck_color_hdr_on_screen(void) { return droiddeck_color_hdr_state() == 1; }
|
||||
|
||||
static _Atomic int g_sdr_white_x100 = 20300;
|
||||
void banner_color_set_sdr_white(float nits) {
|
||||
void droiddeck_color_set_sdr_white(float nits) {
|
||||
if (!(nits >= 10.0f && nits <= 2000.0f)) return;
|
||||
atomic_store(&g_sdr_white_x100, (int)(nits * 100.0f + 0.5f));
|
||||
banner_log(TAG, "SDR content inside an HDR picture is placed at %.0f nits (BANNER_WAYLAND_HDR_SDR_NITS)", nits);
|
||||
droiddeck_log(TAG, "SDR content inside an HDR picture is placed at %.0f nits (DROIDDECK_WAYLAND_HDR_SDR_NITS)", nits);
|
||||
}
|
||||
float banner_color_sdr_white(void) { return atomic_load(&g_sdr_white_x100) / 100.0f; }
|
||||
float droiddeck_color_sdr_white(void) { return atomic_load(&g_sdr_white_x100) / 100.0f; }
|
||||
|
||||
int banner_color_requested(void) {
|
||||
int droiddeck_color_requested(void) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int m = g_req.mode;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return m != 0;
|
||||
}
|
||||
|
||||
int banner_color_output(void) { return atomic_load(&g_output_on); }
|
||||
int droiddeck_color_output(void) { return atomic_load(&g_output_on); }
|
||||
|
||||
void banner_color_env_sample(int thermal, float headroom, int brightness, int bmode) {
|
||||
void droiddeck_color_env_sample(int thermal, float headroom, int brightness, int bmode) {
|
||||
if (atomic_load(&g_gate) != 1) return; /* evidence for the HDR lines only */
|
||||
char msg[2][200];
|
||||
int nmsg = 0;
|
||||
@@ -644,42 +644,42 @@ void banner_color_env_sample(int thermal, float headroom, int brightness, int bm
|
||||
"brightness changes are logged as they happen)", e);
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
for (int i = 0; i < nmsg; i++) banner_log(TAG, "%s", msg[i]);
|
||||
for (int i = 0; i < nmsg; i++) droiddeck_log(TAG, "%s", msg[i]);
|
||||
}
|
||||
|
||||
int banner_color_tonemapped_on_screen(void) {
|
||||
int droiddeck_color_tonemapped_on_screen(void) {
|
||||
if (atomic_load(&g_gate) != 1) return 0;
|
||||
int64_t t = atomic_load(&g_last_tm_ns);
|
||||
return t && now_ns() - t < 1500000000LL;
|
||||
}
|
||||
|
||||
void banner_color_set_output(int on) {
|
||||
void droiddeck_color_set_output(int on) {
|
||||
on = on ? 1 : 0;
|
||||
if (atomic_load(&g_gate) != 1) {
|
||||
/* The drawer only shows the switch in sessions whose gate is open; say so if it ever gets here. */
|
||||
banner_log(TAG, "HDR output switch (%s) ignored: HDR is not open in this session", on ? "on" : "off");
|
||||
droiddeck_log(TAG, "HDR output switch (%s) ignored: HDR is not open in this session", on ? "on" : "off");
|
||||
return;
|
||||
}
|
||||
if (atomic_exchange(&g_output_on, on) == on) return;
|
||||
char who[160];
|
||||
int age = banner_color_last_frame_age_ms(), tm = banner_color_tonemapped_on_screen();
|
||||
int age = droiddeck_color_last_frame_age_ms(), tm = droiddeck_color_tonemapped_on_screen();
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (!on) g_hdr.output_offs++;
|
||||
snprintf(who, sizeof(who), "%s", g_hdr.applied_who[0] ? g_hdr.applied_who : "no HDR program yet");
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (on)
|
||||
banner_log(TAG, "HDR output switched ON in the drawer: HDR frames go to the display as HDR again from the next "
|
||||
droiddeck_log(TAG, "HDR output switched ON in the drawer: HDR frames go to the display as HDR again from the next "
|
||||
"frame (%s; %s)", who, tm ? "they were being tone-mapped to SDR until now" : "none were on screen just now");
|
||||
else
|
||||
banner_log(TAG, "HDR output switched OFF in the drawer: HDR frames are tone-mapped to SDR from the next frame "
|
||||
droiddeck_log(TAG, "HDR output switched OFF in the drawer: HDR frames are tone-mapped to SDR from the next frame "
|
||||
"(%s; %s). The game is not told - it keeps rendering HDR; its own HDR setting and DXVK_HDR are "
|
||||
"untouched", who, age >= 0 && age < 1500 ? "HDR frames were on screen" : "no HDR frames on screen just now");
|
||||
banner_request_redraw(); /* a paused game commits nothing: show the change now */
|
||||
droiddeck_request_redraw(); /* a paused game commits nothing: show the change now */
|
||||
}
|
||||
|
||||
static _Atomic int g_session_ended;
|
||||
|
||||
void banner_color_session_end(void) {
|
||||
void droiddeck_color_session_end(void) {
|
||||
if (atomic_exchange(&g_session_ended, 1)) return;
|
||||
if (atomic_load(&g_gate) < 0) return;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
@@ -692,7 +692,7 @@ void banner_color_session_end(void) {
|
||||
|
||||
struct cm_desc {
|
||||
int refs; /* protocol objects + surfaces (pending / current) */
|
||||
struct banner_color c;
|
||||
struct droiddeck_color c;
|
||||
/* Set on the output / preferred descriptions the compositor serves (never on one a game built
|
||||
* from parameters): get_information is answered rather than refused, with these as the target. */
|
||||
int informative;
|
||||
@@ -727,7 +727,7 @@ static void image_description_get_information(struct wl_client *c, struct wl_res
|
||||
wl_resource_get_version(r), id);
|
||||
if (!info) { wl_client_post_no_memory(c); return; }
|
||||
wl_resource_set_implementation(info, NULL, NULL, NULL); /* events only */
|
||||
const struct banner_color *col = &d->c;
|
||||
const struct droiddeck_color *col = &d->c;
|
||||
int32_t xy[8] = { (int32_t)(col->red[0] * 1e6f + 0.5f), (int32_t)(col->red[1] * 1e6f + 0.5f),
|
||||
(int32_t)(col->green[0] * 1e6f + 0.5f), (int32_t)(col->green[1] * 1e6f + 0.5f),
|
||||
(int32_t)(col->blue[0] * 1e6f + 0.5f), (int32_t)(col->blue[1] * 1e6f + 0.5f),
|
||||
@@ -765,12 +765,12 @@ static void image_description_resource_destroy(struct wl_resource *r) {
|
||||
static void make_output_description(struct wl_client *c, struct wl_resource *parent, uint32_t id, const char *what) {
|
||||
struct cm_desc *d = calloc(1, sizeof(*d));
|
||||
if (!d) { wl_client_post_no_memory(c); return; }
|
||||
struct banner_color *col = &d->c;
|
||||
struct droiddeck_color *col = &d->c;
|
||||
col->identity = g_next_identity++;
|
||||
if (!g_next_identity) g_next_identity = 1;
|
||||
col->primaries = WP_COLOR_MANAGER_V1_PRIMARIES_BT2020;
|
||||
col->tf = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ;
|
||||
col->dataspace = BANNER_ADATASPACE_BT2020_PQ;
|
||||
col->dataspace = DROIDDECK_ADATASPACE_BT2020_PQ;
|
||||
col->has_st2086 = 1;
|
||||
col->red[0] = 0.708f; col->red[1] = 0.292f; col->green[0] = 0.170f; col->green[1] = 0.797f;
|
||||
col->blue[0] = 0.131f; col->blue[1] = 0.046f; col->white[0] = 0.3127f; col->white[1] = 0.3290f;
|
||||
@@ -788,8 +788,8 @@ static void make_output_description(struct wl_client *c, struct wl_resource *par
|
||||
d->refs = 1;
|
||||
wl_resource_set_implementation(r, &image_description_impl, d, image_description_resource_destroy);
|
||||
wp_image_description_v1_send_ready(r, col->identity);
|
||||
banner_log(TAG, "%s asked for %s: answered the display's HDR10 volume (%s), ready and open to get_information",
|
||||
banner_client_name(c), what, col->text);
|
||||
droiddeck_log(TAG, "%s asked for %s: answered the display's HDR10 volume (%s), ready and open to get_information",
|
||||
droiddeck_client_name(c), what, col->text);
|
||||
}
|
||||
|
||||
static void params_create(struct wl_client *client, struct wl_resource *r, uint32_t id) {
|
||||
@@ -801,15 +801,15 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3
|
||||
}
|
||||
struct cm_desc *d = calloc(1, sizeof(*d));
|
||||
if (!d) { wl_client_post_no_memory(client); return; }
|
||||
struct banner_color *c = &d->c;
|
||||
struct droiddeck_color *c = &d->c;
|
||||
c->identity = g_next_identity++;
|
||||
if (!g_next_identity) g_next_identity = 1;
|
||||
c->primaries = p->primaries;
|
||||
c->tf = p->tf;
|
||||
if (p->primaries == WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 && p->tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ)
|
||||
c->dataspace = BANNER_ADATASPACE_BT2020_PQ;
|
||||
c->dataspace = DROIDDECK_ADATASPACE_BT2020_PQ;
|
||||
else if (p->primaries == WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 && p->tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG)
|
||||
c->dataspace = BANNER_ADATASPACE_BT2020_HLG;
|
||||
c->dataspace = DROIDDECK_ADATASPACE_BT2020_HLG;
|
||||
|
||||
/* HDR metadata, kept only when it makes sense. The PQ defaults stand in for what was not given
|
||||
* (0.005 - 10000 cd/m², the target volume = the primary one); CTA-861.3 treats 0 as unknown. */
|
||||
@@ -870,12 +870,12 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3
|
||||
g_hdr.descs++;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
banner_log(TAG, "image description #%u from %s: %s -> ready (on a display layer: dataspace %s)", c->identity,
|
||||
banner_client_name(client), c->text, dataspace_name(c->dataspace));
|
||||
droiddeck_log(TAG, "image description #%u from %s: %s -> ready (on a display layer: dataspace %s)", c->identity,
|
||||
droiddeck_client_name(client), c->text, dataspace_name(c->dataspace));
|
||||
if (dropped[0]) {
|
||||
size_t l = strlen(dropped);
|
||||
if (l && dropped[l - 1] == ',') dropped[l - 1] = 0;
|
||||
banner_log(TAG, "image description #%u: dropped HDR metadata that breaks the protocol's rules:%s "
|
||||
droiddeck_log(TAG, "image description #%u: dropped HDR metadata that breaks the protocol's rules:%s "
|
||||
"(Android gets the rest)", c->identity, dropped);
|
||||
}
|
||||
wl_resource_destroy(r); /* create is the creator's destructor */
|
||||
@@ -982,7 +982,7 @@ static struct cm_surf *surf_of(struct wl_resource *surface) {
|
||||
|
||||
static void describe_surface(struct wl_resource *surface, char *out, size_t size) {
|
||||
struct surface *s = surface ? wl_resource_get_user_data(surface) : NULL;
|
||||
if (s) banner_surface_describe(s, out, size);
|
||||
if (s) droiddeck_surface_describe(s, out, size);
|
||||
else snprintf(out, size, "a closed window");
|
||||
}
|
||||
|
||||
@@ -1002,8 +1002,8 @@ static void cm_surface_set_image_description(struct wl_client *c, struct wl_reso
|
||||
struct cm_surf *cs = wl_resource_get_user_data(r);
|
||||
struct cm_desc *d = desc_res ? wl_resource_get_user_data(desc_res) : NULL;
|
||||
if (!cs) {
|
||||
banner_log(TAG, "%s set an image description on a colour-management object whose surface is gone: ignored",
|
||||
banner_client_name(c));
|
||||
droiddeck_log(TAG, "%s set an image description on a colour-management object whose surface is gone: ignored",
|
||||
droiddeck_client_name(c));
|
||||
return;
|
||||
}
|
||||
if (!d) {
|
||||
@@ -1045,7 +1045,7 @@ static void cm_surface_resource_destroy(struct wl_resource *r) {
|
||||
cs->pending_dirty = 1;
|
||||
}
|
||||
|
||||
void banner_color_commit(struct wl_resource *surface) {
|
||||
void droiddeck_color_commit(struct wl_resource *surface) {
|
||||
if (atomic_load(&g_gate) != 1) return;
|
||||
struct cm_surf *cs = surf_of(surface);
|
||||
if (!cs || !cs->pending_dirty) return;
|
||||
@@ -1056,7 +1056,7 @@ void banner_color_commit(struct wl_resource *surface) {
|
||||
char who[160];
|
||||
describe_surface(surface, who, sizeof(who));
|
||||
if (cs->current) {
|
||||
banner_log(TAG, "%s: image description #%u now applies to its frames (%s; perceptual intent)", who,
|
||||
droiddeck_log(TAG, "%s: image description #%u now applies to its frames (%s; perceptual intent)", who,
|
||||
cs->current->c.identity, cs->current->c.text);
|
||||
if (cs->current->c.dataspace) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
@@ -1064,16 +1064,16 @@ void banner_color_commit(struct wl_resource *surface) {
|
||||
snprintf(g_hdr.applied_who, sizeof(g_hdr.applied_who), "%s", who);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (!g_zero_copy)
|
||||
banner_log(TAG, "%s: zero-copy presentation is OFF, so its HDR frames are composed into the HDR picture "
|
||||
droiddeck_log(TAG, "%s: zero-copy presentation is OFF, so its HDR frames are composed into the HDR picture "
|
||||
"(one extra pass per frame) - switch Zero-copy presentation on for the direct path", who);
|
||||
}
|
||||
} else if (old) {
|
||||
banner_log(TAG, "%s: image description #%u removed - its frames are sRGB again", who, old->c.identity);
|
||||
droiddeck_log(TAG, "%s: image description #%u removed - its frames are sRGB again", who, old->c.identity);
|
||||
}
|
||||
desc_unref(old);
|
||||
}
|
||||
|
||||
const struct banner_color *banner_color_of(struct wl_resource *surface) {
|
||||
const struct droiddeck_color *droiddeck_color_of(struct wl_resource *surface) {
|
||||
if (atomic_load(&g_gate) != 1) return NULL;
|
||||
struct cm_surf *cs = surf_of(surface);
|
||||
return cs && cs->current ? &cs->current->c : NULL;
|
||||
@@ -1125,14 +1125,14 @@ static void cm_get_surface(struct wl_client *c, struct wl_resource *r, uint32_t
|
||||
describe_surface(surface, who, sizeof(who));
|
||||
if (cs->owner) {
|
||||
/* The protocol says surface_exists; a game would be disconnected for it. The newer object wins. */
|
||||
banner_log(TAG, "%s: a second colour-management object for the same surface - the newer one takes over "
|
||||
droiddeck_log(TAG, "%s: a second colour-management object for the same surface - the newer one takes over "
|
||||
"(protocol error surface_exists not raised)", who);
|
||||
wl_resource_set_user_data(cs->owner, NULL);
|
||||
}
|
||||
cs->owner = o;
|
||||
wl_resource_set_implementation(o, &cm_surface_impl, cs, cm_surface_resource_destroy);
|
||||
banner_log(TAG, "%s: colour-management surface created by %s (its Vulkan swapchain asks for a colour space "
|
||||
"other than sRGB)", who, banner_client_name(c));
|
||||
droiddeck_log(TAG, "%s: colour-management surface created by %s (its Vulkan swapchain asks for a colour space "
|
||||
"other than sRGB)", who, droiddeck_client_name(c));
|
||||
}
|
||||
|
||||
static void cm_get_surface_feedback(struct wl_client *c, struct wl_resource *r, uint32_t id, struct wl_resource *surface) {
|
||||
@@ -1186,16 +1186,16 @@ static void bind_cm(struct wl_client *c, void *data, uint32_t ver, uint32_t id)
|
||||
/* One line per program: Mesa binds again for every surface-format query. */
|
||||
if (last_named != c) {
|
||||
last_named = c;
|
||||
banner_log(TAG, "%s bound wp_color_manager_v1 version %u (offered: perceptual intent; parametric descriptions "
|
||||
droiddeck_log(TAG, "%s bound wp_color_manager_v1 version %u (offered: perceptual intent; parametric descriptions "
|
||||
"with mastering metadata; BT.2020 primaries; ST 2084 PQ) - its Vulkan driver can now list "
|
||||
"VK_COLOR_SPACE_HDR10_ST2084_EXT", banner_client_name(c), ver > 1 ? 1u : ver);
|
||||
"VK_COLOR_SPACE_HDR10_ST2084_EXT", droiddeck_client_name(c), ver > 1 ? 1u : ver);
|
||||
}
|
||||
}
|
||||
|
||||
void banner_color_client_gone(struct wl_client *client) {
|
||||
void droiddeck_color_client_gone(struct wl_client *client) {
|
||||
if (atomic_load(&g_gate) != 1 || atomic_load(&g_session_ended)) return;
|
||||
/* A program that presented HDR has left: say where the session stands now, while it is fresh. */
|
||||
const char *name = banner_client_name(client);
|
||||
const char *name = droiddeck_client_name(client);
|
||||
int mine;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
mine = g_hdr.applied_who[0] && name && strstr(g_hdr.applied_who, name) != NULL;
|
||||
@@ -1205,7 +1205,7 @@ void banner_color_client_gone(struct wl_client *client) {
|
||||
|
||||
/* ---------------------------------------------------------------- the gate */
|
||||
|
||||
void banner_color_init(struct wl_display *display) {
|
||||
void droiddeck_color_init(struct wl_display *display) {
|
||||
char why[320] = "";
|
||||
int mode, dxvk_hdr, zc_forced, known, hdr10, id, ratio_avail, api;
|
||||
char name[96], formats[96], source[64];
|
||||
@@ -1229,14 +1229,14 @@ void banner_color_init(struct wl_display *display) {
|
||||
|
||||
/* What asked for HDR, in the words the user knows it by: the editors' setting, or the env override. */
|
||||
char asked[128];
|
||||
if (mode == 2) snprintf(asked, sizeof(asked), "BANNER_WAYLAND_HDR=force (%s)", source);
|
||||
else if (strstr(source, "env")) snprintf(asked, sizeof(asked), "BANNER_WAYLAND_HDR=1 (%s)", source);
|
||||
if (mode == 2) snprintf(asked, sizeof(asked), "DROIDDECK_WAYLAND_HDR=force (%s)", source);
|
||||
else if (strstr(source, "env")) snprintf(asked, sizeof(asked), "DROIDDECK_WAYLAND_HDR=1 (%s)", source);
|
||||
else snprintf(asked, sizeof(asked), "HDR output is on (%s)", source);
|
||||
|
||||
int layer_ok = sc_layer_can_tag_hdr();
|
||||
int ahb_ok = ahb_swapchain_advertised();
|
||||
char off_why[128];
|
||||
if (strstr(source, "env")) snprintf(off_why, sizeof(off_why), "BANNER_WAYLAND_HDR=0 in the %s overrides the setting", source);
|
||||
if (strstr(source, "env")) snprintf(off_why, sizeof(off_why), "DROIDDECK_WAYLAND_HDR=0 in the %s overrides the setting", source);
|
||||
else snprintf(off_why, sizeof(off_why), "the HDR output setting is off");
|
||||
if (mode == 0)
|
||||
snprintf(why, sizeof(why), "HDR output is off (%s)", off_why);
|
||||
@@ -1255,16 +1255,16 @@ void banner_color_init(struct wl_display *display) {
|
||||
snprintf(g_gate_why, sizeof(g_gate_why), "%s", why);
|
||||
atomic_store(&g_gate, 0);
|
||||
if (mode) {
|
||||
banner_log(TAG, "HDR gate CLOSED: %s. Nothing is advertised (no wp_color_manager_v1, no 10-bit dma-buf "
|
||||
droiddeck_log(TAG, "HDR gate CLOSED: %s. Nothing is advertised (no wp_color_manager_v1, no 10-bit dma-buf "
|
||||
"formats): games see an SDR display, exactly as without the switch", why);
|
||||
if (dxvk_hdr)
|
||||
banner_log(TAG, "DXVK_HDR=1 is set: DXGI will still claim an HDR display that no swapchain can get here "
|
||||
droiddeck_log(TAG, "DXVK_HDR=1 is set: DXGI will still claim an HDR display that no swapchain can get here "
|
||||
"(games that check fall back to SDR; some show washed-out colours) - remove it on this display");
|
||||
log_verdict(1);
|
||||
} else if (dxvk_hdr || (known && hdr10)) {
|
||||
/* Off, and silent unless it is worth a line: a display that could show HDR10, or a DXVK
|
||||
* switch that promises games an HDR display they cannot get. */
|
||||
banner_log(TAG, "HDR output off for this session (%s)%s%s", off_why,
|
||||
droiddeck_log(TAG, "HDR output off for this session (%s)%s%s", off_why,
|
||||
(known && hdr10) ? " - this display lists HDR10, so switching HDR output on for this game would "
|
||||
"offer it to games" : "",
|
||||
dxvk_hdr ? " - but DXVK_HDR=1 is set, so DXGI claims an HDR display the game cannot get a "
|
||||
@@ -1276,25 +1276,25 @@ void banner_color_init(struct wl_display *display) {
|
||||
if (!wl_global_create(display, &wp_color_manager_v1_interface, 1, NULL, bind_cm)) {
|
||||
snprintf(g_gate_why, sizeof(g_gate_why), "creating the wp_color_manager_v1 global failed");
|
||||
atomic_store(&g_gate, 0);
|
||||
banner_log("error", "color: wp_color_manager_v1 global creation failed - HDR gate closed");
|
||||
droiddeck_log("error", "color: wp_color_manager_v1 global creation failed - HDR gate closed");
|
||||
log_verdict(1);
|
||||
return;
|
||||
}
|
||||
atomic_store(&g_gate, 1);
|
||||
char syms[160];
|
||||
sc_layer_hdr_symbols(syms, sizeof(syms));
|
||||
banner_log(TAG, "HDR gate %s: %s and %s. Offering games HDR10: wp_color_manager_v1 version 1 (BT.2020 primaries "
|
||||
droiddeck_log(TAG, "HDR gate %s: %s and %s. Offering games HDR10: wp_color_manager_v1 version 1 (BT.2020 primaries "
|
||||
"+ ST 2084 PQ, parametric descriptions with mastering metadata) and 10-bit AB30/XB30 dma-buf formats; HDR "
|
||||
"frames go on the game's own display layer as BT2020_PQ (%s)",
|
||||
mode == 2 ? "FORCED OPEN (testing)" : "OPEN", asked, disp, syms);
|
||||
if (mode == 2 && !hdr10)
|
||||
banner_log(TAG, "BANNER_WAYLAND_HDR=force on a display without HDR10 - testing only: SurfaceFlinger will tone-map "
|
||||
droiddeck_log(TAG, "DROIDDECK_WAYLAND_HDR=force on a display without HDR10 - testing only: SurfaceFlinger will tone-map "
|
||||
"the game layer for this panel (expect GPU/CLIENT composition for it), nothing looks HDR");
|
||||
if (zc_forced)
|
||||
banner_log(TAG, "zero-copy presentation turned on for this session: the HDR game's own 10-bit frames go straight "
|
||||
droiddeck_log(TAG, "zero-copy presentation turned on for this session: the HDR game's own 10-bit frames go straight "
|
||||
"to its display layer (the best path; anything that needs the compositor gets the composed HDR picture)");
|
||||
if (!dxvk_hdr)
|
||||
banner_log(TAG, "DXVK_HDR=1 is not in the game's environment: DXVK games will not see an HDR display in DXGI "
|
||||
droiddeck_log(TAG, "DXVK_HDR=1 is not in the game's environment: DXVK games will not see an HDR display in DXGI "
|
||||
"(dxgi.enableHDR in a dxvk.conf does the same) - add DXVK_HDR=1 to the container's or shortcut's "
|
||||
"environment variables");
|
||||
(void)max_avg; (void)min_lum;
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
#include "linux-dmabuf-v1-server-protocol.h"
|
||||
#include "vk_present.h"
|
||||
#include "banner_ext.h"
|
||||
#include "banner_color.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "droiddeck_color.h"
|
||||
#include "compositor_internal.h"
|
||||
|
||||
/* ------------------------------------------------------------ zwp_linux_dmabuf_v1 */
|
||||
@@ -112,7 +112,7 @@ static const struct zwp_linux_dmabuf_v1_interface dmabuf_impl = {
|
||||
/* The advertised format/modifier table, built at the first bind from what the renderer's driver
|
||||
* can import (vkp_dmabuf_modifiers). INVALID is always offered too (Mesa drops it; other clients
|
||||
* may use it for the implicit path). Without a renderer the list is LINEAR + INVALID, as before. */
|
||||
/* The last two rows are HDR10's (banner_color.h): A2B10G10R10 as AB30 (alpha) + XB30 (opaque) - Mesa
|
||||
/* The last two rows are HDR10's (droiddeck_color.h): A2B10G10R10 as AB30 (alpha) + XB30 (opaque) - Mesa
|
||||
* lists a VkFormat only when both are advertised, and it is the one 10-bit layout our zero-copy WSI
|
||||
* can put in a gralloc buffer (AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM). They are advertised ONLY
|
||||
* while the HDR gate is open, and after the 8-bit rows, so every other session - and every client that
|
||||
@@ -132,7 +132,7 @@ static void dmabuf_build_formats(void) {
|
||||
char line[320];
|
||||
int pos = 0, compressed = 0;
|
||||
g_dmabuf_fmts_ready = 1;
|
||||
g_dmabuf_nfmt = banner_color_hdr_open() ? DMABUF_NFMT_MAX : DMABUF_NFMT_SDR;
|
||||
g_dmabuf_nfmt = droiddeck_color_hdr_open() ? DMABUF_NFMT_MAX : DMABUF_NFMT_SDR;
|
||||
for (int f = 0; f < g_dmabuf_nfmt; f++) {
|
||||
uint64_t got[DMABUF_NMOD];
|
||||
int n = vkp_dmabuf_modifiers(g_dmabuf_fmts[f].fmt, got, DMABUF_NMOD), k = 0;
|
||||
@@ -155,10 +155,10 @@ static void dmabuf_build_formats(void) {
|
||||
vkp_modifier_name(g_dmabuf_fmts[f].mods[i]));
|
||||
if (pos >= (int)sizeof(line)) pos = (int)sizeof(line) - 1;
|
||||
}
|
||||
banner_log("dmabuf", "formats: %s%s", line,
|
||||
!g_ubwc ? " (BANNER_WAYLAND_UBWC=0: qcom_compressed not advertised)" : "");
|
||||
droiddeck_log("dmabuf", "formats: %s%s", line,
|
||||
!g_ubwc ? " (DROIDDECK_WAYLAND_UBWC=0: qcom_compressed not advertised)" : "");
|
||||
if (g_ubwc && !compressed)
|
||||
banner_log("dmabuf", "the compositor's driver (%s) reports no importable qcom_compressed layout: "
|
||||
droiddeck_log("dmabuf", "the compositor's driver (%s) reports no importable qcom_compressed layout: "
|
||||
"game swapchains stay linear", vkp_gpu_name());
|
||||
if (g_dmabuf_nfmt > DMABUF_NFMT_SDR) {
|
||||
/* The rows above always carry LINEAR; say what the compositor's own driver can really import
|
||||
@@ -166,7 +166,7 @@ static void dmabuf_build_formats(void) {
|
||||
uint64_t got[DMABUF_NMOD];
|
||||
int n = vkp_dmabuf_modifiers(DRM_XBGR2101010, got, DMABUF_NMOD), ubwc10 = 0;
|
||||
for (int i = 0; i < n; i++) if (got[i] == VKP_MOD_QCOM_COMPRESSED) ubwc10 = 1;
|
||||
banner_log("color", "10-bit dma-buf formats AB30/XB30 advertised for HDR10; the compositor's driver (%s) "
|
||||
droiddeck_log("color", "10-bit dma-buf formats AB30/XB30 advertised for HDR10; the compositor's driver (%s) "
|
||||
"imports XB30 %s", vkp_gpu_name(),
|
||||
n == 0 ? "with no layout it reports (copy path unlikely; display layer only)"
|
||||
: ubwc10 ? "linear and UBWC" : "linear only (UBWC 10-bit frames: display layer only)");
|
||||
@@ -237,7 +237,7 @@ static void dmabuf_build_feedback(void) {
|
||||
}
|
||||
if (!n) return;
|
||||
|
||||
int fd = (int)syscall(__NR_memfd_create, "banner-dmabuf-formats",
|
||||
int fd = (int)syscall(__NR_memfd_create, "droiddeck-dmabuf-formats",
|
||||
MFD_CLOEXEC | MFD_ALLOW_SEALING);
|
||||
if (fd < 0) { WLOGE("dmabuf feedback: memfd_create failed (%s)", strerror(errno)); return; }
|
||||
size_t size = (size_t)n * sizeof(entries[0]);
|
||||
@@ -251,14 +251,14 @@ static void dmabuf_build_feedback(void) {
|
||||
g_fmt_table_fd = fd;
|
||||
g_fmt_table_size = size;
|
||||
g_fmt_table_n = (uint16_t)n;
|
||||
banner_log("dmabuf", "feedback ready: %d format/modifier pairs, main device %u:%u",
|
||||
droiddeck_log("dmabuf", "feedback ready: %d format/modifier pairs, main device %u:%u",
|
||||
n, (unsigned)major(g_main_device), (unsigned)minor(g_main_device));
|
||||
/* Vulkan games never need the node. Mesa's EGL did until Wayland layer versionCode 9: without
|
||||
* one it fell back to a software path that draws nothing here (black window, sound plays). */
|
||||
if (!g_main_device)
|
||||
banner_log("dmabuf", "%s: OpenGL games need Wayland layer versionCode 9 or newer, "
|
||||
droiddeck_log("dmabuf", "%s: OpenGL games need Wayland layer versionCode 9 or newer, "
|
||||
"which runs OpenGL on the GPU without a DRM node; older layers show a black window",
|
||||
g_no_render_node ? "no DRM device named (forced by BANNER_WAYLAND_NO_RENDER_NODE=1)"
|
||||
g_no_render_node ? "no DRM device named (forced by DROIDDECK_WAYLAND_NO_RENDER_NODE=1)"
|
||||
: "this device gives apps no display (DRM) device (/dev/dri)");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 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
|
||||
* 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():
|
||||
* desktop owner's process), so text input can't follow it; it follows droiddeck_ime_target():
|
||||
* the program window last clicked, else the topmost program window. Clicking into notepad
|
||||
* moves the text-input focus to notepad's process, which is where the edit control lives.
|
||||
*
|
||||
@@ -13,7 +13,7 @@
|
||||
* them (enable resets the state as the protocol says). We answer with `done(serial)` only when
|
||||
* we sent events (preedit/commit strings), never for every commit.
|
||||
*
|
||||
* Host side. banner_on_text_input(enabled, program, cursor rect in scene pixels) tells the app
|
||||
* Host side. droiddeck_on_text_input(enabled, program, cursor rect in scene pixels) tells the app
|
||||
* when a program starts/stops accepting IME text or moves its caret (Wine reports the rect only
|
||||
* when an edit control positions its IME composition window, via ImmSetCompositionWindow).
|
||||
* Typed text arrives through text_input_host_*: preedit_string / commit_string + done.
|
||||
@@ -23,7 +23,7 @@
|
||||
#define _GNU_SOURCE 1
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "banner_ext.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "text-input-unstable-v3-server-protocol.h"
|
||||
|
||||
#ifndef KEY_BACKSPACE
|
||||
@@ -58,7 +58,7 @@ static struct wl_resource *g_target; /* surface text input is focused
|
||||
static int g_told_enabled, g_told_rect[4];
|
||||
static struct text_input *g_told_ti;
|
||||
|
||||
static const char *ti_program(struct text_input *ti) { return banner_client_name(ti->client); }
|
||||
static const char *ti_program(struct text_input *ti) { return droiddeck_client_name(ti->client); }
|
||||
|
||||
/* The enabled text input focused on the current target, if any. */
|
||||
static struct text_input *active_input(void) {
|
||||
@@ -74,22 +74,22 @@ static void tell_app(void) {
|
||||
int enabled = ti != NULL, rect[4] = {0, 0, 0, 0};
|
||||
if (ti) {
|
||||
int ox = 0, oy = 0;
|
||||
banner_surface_scene_origin(ti->entered, &ox, &oy);
|
||||
droiddeck_surface_scene_origin(ti->entered, &ox, &oy);
|
||||
rect[0] = ti->rect[0] + ox; rect[1] = ti->rect[1] + oy;
|
||||
rect[2] = ti->rect[2]; rect[3] = ti->rect[3];
|
||||
}
|
||||
if (enabled == g_told_enabled && ti == g_told_ti && !memcmp(rect, g_told_rect, sizeof(rect))) return;
|
||||
if (enabled && (!g_told_enabled || ti != g_told_ti))
|
||||
banner_log("text-input", "text input enabled by %s (cursor rect %d,%d %dx%d)", ti_program(ti),
|
||||
droiddeck_log("text-input", "text input enabled by %s (cursor rect %d,%d %dx%d)", ti_program(ti),
|
||||
rect[0], rect[1], rect[2], rect[3]);
|
||||
else if (enabled)
|
||||
banner_log("text-input", "cursor rect from %s: %d,%d %dx%d", ti_program(ti), rect[0], rect[1], rect[2], rect[3]);
|
||||
droiddeck_log("text-input", "cursor rect from %s: %d,%d %dx%d", ti_program(ti), rect[0], rect[1], rect[2], rect[3]);
|
||||
else
|
||||
banner_log("text-input", "text input disabled%s%s", g_told_ti ? " by " : "", g_told_ti ? ti_program(g_told_ti) : "");
|
||||
droiddeck_log("text-input", "text input disabled%s%s", g_told_ti ? " by " : "", g_told_ti ? ti_program(g_told_ti) : "");
|
||||
g_told_enabled = enabled;
|
||||
g_told_ti = ti;
|
||||
memcpy(g_told_rect, rect, sizeof(rect));
|
||||
banner_on_text_input(enabled, enabled ? ti_program(ti) : "", rect[0], rect[1], rect[2], rect[3]);
|
||||
droiddeck_on_text_input(enabled, enabled ? ti_program(ti) : "", rect[0], rect[1], rect[2], rect[3]);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ focus */
|
||||
@@ -102,8 +102,8 @@ static void send_leave(struct text_input *ti) {
|
||||
ti->has_preedit = 0;
|
||||
}
|
||||
|
||||
void banner_text_input_refocus(void) {
|
||||
struct wl_resource *t = banner_ime_target();
|
||||
void droiddeck_text_input_refocus(void) {
|
||||
struct wl_resource *t = droiddeck_ime_target();
|
||||
struct text_input *ti;
|
||||
if (t == g_target) return;
|
||||
wl_list_for_each(ti, &g_inputs, link)
|
||||
@@ -121,7 +121,7 @@ void banner_text_input_refocus(void) {
|
||||
wl_display_flush_clients(g_disp);
|
||||
}
|
||||
|
||||
void banner_text_input_surface_gone(struct wl_resource *surface) {
|
||||
void droiddeck_text_input_surface_gone(struct wl_resource *surface) {
|
||||
struct text_input *ti;
|
||||
wl_list_for_each(ti, &g_inputs, link)
|
||||
if (ti->entered == surface) { ti->entered = NULL; ti->enabled = 0; ti->has_preedit = 0; }
|
||||
@@ -136,7 +136,7 @@ static void ti_res_destroy(struct wl_resource *r) {
|
||||
if (!ti) return;
|
||||
wl_list_remove(&ti->link);
|
||||
if (g_told_ti == ti) { g_told_ti = NULL; g_told_enabled = 0; memset(g_told_rect, 0, sizeof(g_told_rect));
|
||||
banner_on_text_input(0, "", 0, 0, 0, 0); }
|
||||
droiddeck_on_text_input(0, "", 0, 0, 0, 0); }
|
||||
free(ti);
|
||||
}
|
||||
|
||||
@@ -235,13 +235,13 @@ static int32_t utf8_offset(const char *s, size_t len, int chars) {
|
||||
void text_input_host_commit(const char *utf8, size_t len) {
|
||||
struct text_input *ti = active_input();
|
||||
if (!ti) {
|
||||
banner_log("text-input", "typed text (%zu chars) dropped: no program accepts text input", utf8_chars(utf8, len));
|
||||
droiddeck_log("text-input", "typed text (%zu chars) dropped: no program accepts text input", utf8_chars(utf8, len));
|
||||
return;
|
||||
}
|
||||
if (ti->has_preedit) { zwp_text_input_v3_send_preedit_string(ti->res, "", 0, 0); ti->has_preedit = 0; }
|
||||
zwp_text_input_v3_send_commit_string(ti->res, utf8);
|
||||
zwp_text_input_v3_send_done(ti->res, ti->serial);
|
||||
banner_log("text-input", "committed %zu chars to %s", utf8_chars(utf8, len), ti_program(ti));
|
||||
droiddeck_log("text-input", "committed %zu chars to %s", utf8_chars(utf8, len), ti_program(ti));
|
||||
}
|
||||
|
||||
void text_input_host_preedit(const char *utf8, size_t len, int cursor_begin, int cursor_end) {
|
||||
@@ -264,9 +264,9 @@ void text_input_host_delete(int before, int after) {
|
||||
zwp_text_input_v3_send_done(ti->res, ti->serial);
|
||||
ti->has_preedit = 0;
|
||||
}
|
||||
for (int i = 0; i < before; i++) { banner_inject_key(KEY_BACKSPACE, 1); banner_inject_key(KEY_BACKSPACE, 0); }
|
||||
for (int i = 0; i < after; i++) { banner_inject_key(KEY_DELETE, 1); banner_inject_key(KEY_DELETE, 0); }
|
||||
banner_log("text-input", "deleted %d before / %d after the caret (as key presses)", before, after);
|
||||
for (int i = 0; i < before; i++) { droiddeck_inject_key(KEY_BACKSPACE, 1); droiddeck_inject_key(KEY_BACKSPACE, 0); }
|
||||
for (int i = 0; i < after; i++) { droiddeck_inject_key(KEY_DELETE, 1); droiddeck_inject_key(KEY_DELETE, 0); }
|
||||
droiddeck_log("text-input", "deleted %d before / %d after the caret (as key presses)", before, after);
|
||||
}
|
||||
|
||||
void text_input_init(struct wl_display *display) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* missing. We send no icon_size (any size is fine) and `done` at bind, as the protocol asks.
|
||||
*/
|
||||
#include <stdlib.h>
|
||||
#include "banner_ext.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "xdg-toplevel-icon-v1-server-protocol.h"
|
||||
|
||||
static void icon_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
package com.droiddeck.launcher
|
||||
|
||||
import android.app.Application
|
||||
import com.droiddeck.launcher.runtime.BwrapSpawner
|
||||
import com.droiddeck.launcher.session.CrashHandler
|
||||
import com.droiddeck.launcher.session.GpuClockPin
|
||||
|
||||
/** Process-wide setup: the crash handler, so a session's folder is finished even when we die. */
|
||||
/**
|
||||
* Process-wide setup: the crash handler, so a session's folder is finished even when we die, and
|
||||
* the GPU clock pin cleared in case a killed process left it set, and the socket Flatpak's
|
||||
* sandboxes are started through (BwrapSpawner).
|
||||
*/
|
||||
class App : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CrashHandler.install(this)
|
||||
// Before anything else touches the session prefs, so the first session already runs in Deck mode.
|
||||
com.droiddeck.launcher.session.SessionPrefs.settleDeckModeDefault(this)
|
||||
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
|
||||
com.droiddeck.launcher.update.AppUpdates.init(this)
|
||||
GpuClockPin.clearLeftover(this)
|
||||
BwrapSpawner.start(this)
|
||||
com.droiddeck.launcher.frontend.GameFileSync.start(this)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.droiddeck.launcher.session.ComponentsManager
|
||||
import com.droiddeck.launcher.session.ProtonDefault
|
||||
|
||||
/**
|
||||
* The Components page's state and work: the Protons and their DXVK/VKD3D sets, the Nightlies
|
||||
@@ -16,10 +17,11 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
var compCatalog by mutableStateOf<List<ComponentsManager.CatalogItem>>(emptyList())
|
||||
var compCatalogAt by mutableStateOf(0L)
|
||||
var compProton by mutableStateOf<String?>(null)
|
||||
var compComp by mutableStateOf("dxvk")
|
||||
var compComp by mutableStateOf(com.droiddeck.launcher.ui.GPU_TAB)
|
||||
var compChecking by mutableStateOf(false)
|
||||
var compBusy by mutableStateOf<String?>(null)
|
||||
var compDownloads by mutableStateOf<Map<String, Int>>(emptyMap())
|
||||
private var shownDefault: String? = null
|
||||
|
||||
/** Reads the Protons (and on first open saves their originals) off the UI thread; the Nightlies list comes from its cache. */
|
||||
fun refreshComponents(snapshotFirst: Boolean = false) {
|
||||
@@ -28,14 +30,35 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
runCatching { ComponentsManager.applyQueued(activity) }
|
||||
val snap = runCatching { ComponentsManager.snapshot(activity) }.onFailure { Log.w(TAG, "components", it) }.getOrNull()
|
||||
val cat = runCatching { ComponentsManager.catalog(activity, false) }.getOrNull()
|
||||
val chosen = snap?.let { s -> runCatching { ProtonDefault.selectedId(activity, s.protons.map { it.proton }) }.getOrNull() }
|
||||
ui.post {
|
||||
compSnapshot = snap ?: ComponentsManager.Snapshot(emptyList(), emptyList())
|
||||
if (cat != null && cat.fetchedAt > 0) { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
|
||||
if (compProton == null || snap?.protons?.none { it.proton.id == compProton } == true) compProton = snap?.protons?.firstOrNull()?.proton?.id
|
||||
if (chosen != null && chosen != shownDefault) {
|
||||
shownDefault = chosen
|
||||
compProton = chosen
|
||||
} else if (compProton == null || snap?.protons?.none { it.proton.id == compProton } == true) {
|
||||
compProton = chosen ?: snap?.protons?.firstOrNull()?.proton?.id
|
||||
}
|
||||
}
|
||||
}, "components").start()
|
||||
}
|
||||
|
||||
fun chooseProton(id: String) {
|
||||
val proton = compSnapshot?.protons?.firstOrNull { it.proton.id == id }?.proton ?: return
|
||||
compProton = id
|
||||
Thread({
|
||||
val message = runCatching {
|
||||
when (ProtonDefault.request(activity, proton)) {
|
||||
ProtonDefault.Outcome.LIVE -> activity.getString(R.string.comp_default_live, proton.name)
|
||||
ProtonDefault.Outcome.NEXT_START -> activity.getString(R.string.comp_default_next, proton.name)
|
||||
ProtonDefault.Outcome.NOT_RUNNABLE -> activity.getString(R.string.comp_default_not_runnable, proton.name)
|
||||
}
|
||||
}.getOrElse { e -> activity.getString(R.string.comp_default_failed, e.message ?: e.javaClass.simpleName) }
|
||||
ui.post { android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show() }
|
||||
}, "components-default").start()
|
||||
}
|
||||
|
||||
fun componentAction(label: String, work: () -> String) {
|
||||
if (compBusy != null) return
|
||||
compBusy = label
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.droiddeck.launcher.runtime.DeckyManager
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Decky Loader's state in the launcher: what is installed, the releases on offer and an install
|
||||
@@ -51,4 +52,21 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
}
|
||||
}, "install-decky").start()
|
||||
}
|
||||
|
||||
fun importPluginZip(archive: File) {
|
||||
if (deckyStage != null || SessionState.running || deckyInstalled == null) return
|
||||
deckyStage = "Starting plugin import…"; deckyPercent = -1
|
||||
Thread({
|
||||
val problem = runCatching {
|
||||
DeckyManager.installPluginZip(activity, archive) { label, value ->
|
||||
ui.post { deckyStage = label; deckyPercent = value }
|
||||
}
|
||||
}.getOrElse { error -> "Plugin import failed: ${error.message ?: error.javaClass.simpleName}" }
|
||||
ui.post {
|
||||
deckyStage = null; deckyPercent = -1
|
||||
val message = problem ?: "Plugin installed. Restart Steam to load it."
|
||||
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "import-decky-plugin").start()
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import com.droiddeck.launcher.gpu.DriverBundle
|
||||
import com.droiddeck.launcher.gpu.DriverPairs
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
@@ -17,11 +21,16 @@ import com.droiddeck.launcher.gpu.TurnipReleases
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.ui.DriverRow
|
||||
import com.droiddeck.launcher.ui.GpuDriversState
|
||||
import com.droiddeck.launcher.ui.PairRow
|
||||
import com.droiddeck.launcher.wayland.CompositorHost
|
||||
|
||||
/**
|
||||
* The Linux and Android driver menus: the drivers each can pick from, the release downloads the
|
||||
* last check found, and what each mode is set to. The launcher screen keeps one and hands its
|
||||
* rows to the mode settings dialog.
|
||||
* The GPU drivers: what this GPU is, the matched driver pairs the release repos offer for it, and
|
||||
* - in Auto, the default - keeping the recommended pair installed and set. Under Advanced, the
|
||||
* Linux (runtime) and Android (display) lists as before, each picked on its own - except a
|
||||
* bundle's two halves (DriverBundle), which are always picked, and deleted, together. The launcher
|
||||
* screen keeps one and hands it to the Components page's GPU drivers tab.
|
||||
*/
|
||||
internal class DriverMenus(private val activity: Activity, private val ui: Handler) {
|
||||
var linuxRows by mutableStateOf<List<DriverRow>>(emptyList())
|
||||
@@ -33,17 +42,42 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
var canRestoreBundled by mutableStateOf(false)
|
||||
/** Asset name -> download percent, while it downloads. */
|
||||
private val releaseProgress = HashMap<String, Int>()
|
||||
var linuxSteam by mutableStateOf("")
|
||||
var linuxDesktop by mutableStateOf("")
|
||||
var linuxSelected by mutableStateOf("")
|
||||
val gpu: GpuInfo = GpuInfo.detect()
|
||||
// Read in refreshDrivers/ensureAuto: the activity has no context yet while its fields are made.
|
||||
var mode by mutableStateOf(SessionPrefs.GPU_DRIVERS_AUTO)
|
||||
var pairRows by mutableStateOf<List<PairRow>>(emptyList())
|
||||
/** The pair being installed, and how far along. */
|
||||
var pairBusy by mutableStateOf<String?>(null)
|
||||
var pairPercent by mutableIntStateOf(-1)
|
||||
/** What Auto last did or found, for the line under the pair in use. */
|
||||
var autoStatus by mutableStateOf("")
|
||||
/** The bundle both drivers are set to, as "DD-Turnip 0.1.0", or null when they are not one. */
|
||||
var activeBundle by mutableStateOf<String?>(null)
|
||||
private var autoCheckedThisProcess = false
|
||||
|
||||
fun state() = GpuDriversState(
|
||||
gpuName = gpu.name, gpuFamily = gpu.family.label, soc = gpu.soc, supportText = gpu.supportText,
|
||||
supported = gpu.support == GpuInfo.Support.TESTED, unsupported = gpu.support == GpuInfo.Support.UNSUPPORTED,
|
||||
auto = mode == SessionPrefs.GPU_DRIVERS_AUTO, pairs = pairRows, busy = pairBusy, percent = pairPercent,
|
||||
autoStatus = autoStatus, releaseStatus = releaseStatus, checking = releaseChecking,
|
||||
linuxRows = linuxRows, linuxSelected = linuxSelected, androidRows = androidRows, androidSelected = androidSelected,
|
||||
linuxDownloads = linuxDownloads, androidDownloads = androidDownloads, canRestoreBundled = canRestoreBundled,
|
||||
activeBundle = activeBundle,
|
||||
)
|
||||
var androidRows by mutableStateOf<List<DriverRow>>(emptyList())
|
||||
var androidSelected by mutableStateOf("")
|
||||
|
||||
fun refreshDrivers() {
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
fun origin(id: String) = if (TurnipReleases.isDownloaded(activity, id)) DriverRow.DOWNLOADED else DriverRow.IMPORTED
|
||||
val bundles = DriverBundle.all(activity)
|
||||
fun bundleRow(id: String, linux: Boolean) = bundles.firstOrNull { (if (linux) it.linuxId else it.androidId) == id }?.let {
|
||||
DriverRow(id, it.label, "Android + Linux: sets the ${if (linux) "display" else "runtime"} driver with it", true, DriverRow.BUNDLE)
|
||||
}
|
||||
linuxRows = LinuxVulkanDriver.optionValues(activity).map { id ->
|
||||
if (id.isEmpty()) DriverRow("", "Runtime default", "the Turnip built into the runtime", false)
|
||||
else DriverRow(
|
||||
else bundleRow(id, linux = true) ?: DriverRow(
|
||||
id, lm.getDriverName(id),
|
||||
listOfNotNull(
|
||||
lm.getDriverVersion(id).takeIf { it.isNotEmpty() },
|
||||
@@ -52,8 +86,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
true, origin(id),
|
||||
)
|
||||
}
|
||||
linuxSteam = SessionPrefs.linuxDriver(activity, SessionService.MODE_STEAM)
|
||||
linuxDesktop = SessionPrefs.linuxDriver(activity, SessionService.MODE_DESKTOP)
|
||||
linuxSelected = SessionPrefs.linuxDriver(activity)
|
||||
mode = SessionPrefs.gpuDriverMode(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
val auto = td.autoId()
|
||||
androidRows = buildList {
|
||||
@@ -63,23 +97,211 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
false,
|
||||
))
|
||||
for (id in td.visibleBundled()) add(DriverRow(id, td.displayName(id), td.driverVersion(id), true, DriverRow.BUNDLED))
|
||||
for (id in td.enumerateImported()) add(DriverRow(id, td.displayName(id), td.driverVersion(id), true, origin(id)))
|
||||
for (id in td.enumerateImported()) add(bundleRow(id, linux = false) ?: DriverRow(id, td.displayName(id), td.driverVersion(id), true, origin(id)))
|
||||
}
|
||||
canRestoreBundled = td.hiddenBundled().isNotEmpty()
|
||||
androidSelected = SessionPrefs.androidDriver(activity)
|
||||
activeBundle = DriverBundle.active(activity)?.label
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
}
|
||||
|
||||
/** One line for the settings row that opens this: "Auto · WinNative · Balanced". */
|
||||
fun summary(): String {
|
||||
val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle
|
||||
return (if (mode == SessionPrefs.GPU_DRIVERS_AUTO) "Auto" else "Manual") + (active?.let { " · $it" } ?: "")
|
||||
}
|
||||
|
||||
/** The pairs the last check found, with what is installed and in use; this GPU's first. */
|
||||
fun refreshPairs() {
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
|
||||
val recommended = DriverPairs.recommendedKey(gpu, pairs)
|
||||
pairRows = pairs.map { p ->
|
||||
val bundle = p.bundle?.let { installedBundle(it) }
|
||||
val displayId = if (p.bundle != null) bundle?.androidId else p.display?.let { TurnipReleases.installedId(activity, it, td::isInstalled) }
|
||||
val linuxId = if (p.bundle != null) bundle?.linuxId else p.linux?.let { TurnipReleases.installedId(activity, it, lm::isInstalled) }
|
||||
val mb = p.assets.sumOf { it.size } / 1_048_576.0
|
||||
PairRow(
|
||||
key = p.key, name = p.name, version = p.version,
|
||||
detail = if (!p.complete) "Only one half is published right now" else "%.0f MB for both".format(mb),
|
||||
recommended = p.key == recommended, suits = p.suits(gpu), complete = p.complete,
|
||||
installed = displayId != null && linuxId != null,
|
||||
active = displayId != null && linuxId != null &&
|
||||
SessionPrefs.androidDriver(activity) == displayId && SessionPrefs.linuxDriver(activity) == linuxId,
|
||||
)
|
||||
}.sortedByDescending { it.recommended }
|
||||
}
|
||||
|
||||
fun setMode(auto: Boolean) {
|
||||
mode = if (auto) SessionPrefs.GPU_DRIVERS_AUTO else SessionPrefs.GPU_DRIVERS_MANUAL
|
||||
SessionPrefs.setGpuDriverMode(activity, mode)
|
||||
autoStatus = ""
|
||||
if (auto) ensureAuto(force = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto: make the recommended pair for this GPU the one installed and set. Looks online when
|
||||
* [force]d (refresh), or when the last check is a day old - Banners-Turnip rebuilds hourly, and
|
||||
* a new pair a day is plenty - otherwise works from the last check. Once per app start on its
|
||||
* own; the pair it replaces is removed, anything the user imported or picked is left alone.
|
||||
*/
|
||||
fun ensureAuto(force: Boolean) {
|
||||
mode = SessionPrefs.gpuDriverMode(activity)
|
||||
if (mode != SessionPrefs.GPU_DRIVERS_AUTO || pairBusy != null || releaseChecking) return
|
||||
if (!force && autoCheckedThisProcess) return
|
||||
autoCheckedThisProcess = true
|
||||
if (DriverPairs.recommendedKey(gpu, emptyList()) == null) {
|
||||
autoStatus = "No drivers to set: ${gpu.supportText.lowercase()}"
|
||||
return
|
||||
}
|
||||
releaseChecking = true
|
||||
autoStatus = "Checking for the latest drivers…"
|
||||
Thread({
|
||||
val cached = TurnipReleases.cached(activity)
|
||||
val stale = cached == null || System.currentTimeMillis() - cached.checkedAt > 24 * 3_600_000L ||
|
||||
cached.assets.none { it.pair.isNotEmpty() }
|
||||
val problem = if (force || stale) runCatching { TurnipReleases.refresh(activity) }.exceptionOrNull()?.message else null
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
|
||||
val key = DriverPairs.recommendedKey(gpu, pairs)
|
||||
val pair = pairs.firstOrNull { it.key == key }
|
||||
val row = pairRows.firstOrNull { it.key == key }
|
||||
when {
|
||||
row?.active == true -> autoStatus = "Up to date" + (problem?.let { " (couldn't check: $it)" } ?: "")
|
||||
pair == null || !pair.complete ->
|
||||
autoStatus = problem?.let { "Couldn't check: $it" } ?: "The recommended drivers aren't published right now"
|
||||
else -> installPair(pair, auto = true)
|
||||
}
|
||||
}
|
||||
}, "gpu-driver-auto").start()
|
||||
}
|
||||
|
||||
/** Manual: install (if need be) and set both halves of a pair. */
|
||||
fun selectPair(key: String) {
|
||||
if (pairBusy != null) return
|
||||
val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return
|
||||
installPair(pair, auto = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download what is missing of [pair], then set both halves - only once both are in, so a
|
||||
* failed download never leaves a half-changed pair. Auto's own downloads are recorded, and the
|
||||
* ones the new pair replaces are removed.
|
||||
*/
|
||||
private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean) {
|
||||
if (!pair.complete) return
|
||||
pairBusy = pair.key
|
||||
pairPercent = 0
|
||||
if (auto) autoStatus = "Downloading ${pair.name} ${pair.version}…"
|
||||
Thread({
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
var downloaded = false
|
||||
val result = runCatching {
|
||||
val zips = pair.assets
|
||||
val ids = zips.mapIndexed { i, asset ->
|
||||
installedIdOf(asset)
|
||||
?: installAsset(asset) { pct -> ui.post { pairPercent = (i * 100 + pct) / zips.size } }.also { downloaded = true }
|
||||
}
|
||||
if (pair.bundle != null) DriverBundle.get(activity, ids[0])!!.let { listOf(it.androidId, it.linuxId) } else ids
|
||||
}
|
||||
ui.post {
|
||||
pairBusy = null
|
||||
pairPercent = -1
|
||||
result.onSuccess { (displayId, linuxId) ->
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != displayId
|
||||
SessionPrefs.setAndroidDriver(activity, displayId)
|
||||
SessionPrefs.setLinuxDriver(activity, linuxId)
|
||||
val restart = if (displayChanged && CompositorHost.isStarted) " The display driver applies after DroidDeck restarts." else ""
|
||||
if (auto) {
|
||||
val previous = SessionPrefs.gpuAutoInstalled(activity)
|
||||
for (id in previous - setOf(displayId, linuxId)) {
|
||||
val bundle = DriverBundle.containing(activity, id, lm.isInstalled(id))
|
||||
when {
|
||||
bundle != null -> { DriverBundle.remove(activity, bundle); TurnipReleases.forget(activity, bundle.id) }
|
||||
lm.isInstalled(id) -> lm.removeDriver(id)
|
||||
else -> td.remove(id)
|
||||
}
|
||||
TurnipReleases.forget(activity, id)
|
||||
}
|
||||
SessionPrefs.setGpuAutoInstalled(activity, setOf(displayId, linuxId))
|
||||
autoStatus = (if (downloaded) "Updated to" else "Switched to") + " ${pair.name} ${pair.version}.$restart"
|
||||
} else {
|
||||
android.widget.Toast.makeText(activity, "Using ${pair.name} ${pair.version}.$restart", android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}.onFailure { e ->
|
||||
Log.w(TAG, "driver pair ${pair.key}", e)
|
||||
val why = if (e is IllegalArgumentException) e.message else "Download failed: ${e.message}"
|
||||
if (auto) autoStatus = why ?: "Download failed"
|
||||
else android.widget.Toast.makeText(activity, why, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
refreshDrivers()
|
||||
}
|
||||
}, "gpu-driver-pair").start()
|
||||
}
|
||||
|
||||
/** The bundle a release bundle was downloaded as, while both its halves are installed. */
|
||||
private fun installedBundle(asset: TurnipReleases.Asset): DriverBundle.Bundle? =
|
||||
TurnipReleases.installedId(activity, asset) { DriverBundle.get(activity, it) != null }?.let { DriverBundle.get(activity, it) }
|
||||
|
||||
/** The id a release asset was installed as (a bundle's id for a bundle), or null when it is not installed. */
|
||||
private fun installedIdOf(asset: TurnipReleases.Asset): String? = when {
|
||||
asset.bundle -> installedBundle(asset)?.id
|
||||
asset.linux -> TurnipReleases.installedId(activity, asset, LinuxVulkanDriverManager(activity)::isInstalled)
|
||||
else -> TurnipReleases.installedId(activity, asset, TurnipDriver(activity)::isInstalled)
|
||||
}
|
||||
|
||||
/**
|
||||
* One release asset, downloaded, checked and installed through the importer; returns its id,
|
||||
* a bundle's id for a bundle.
|
||||
*/
|
||||
private fun installAsset(asset: TurnipReleases.Asset, progress: (Int) -> Unit): String {
|
||||
var file: File? = null
|
||||
try {
|
||||
file = TurnipReleases.download(activity, asset, progress)
|
||||
val uri = Uri.fromFile(file)
|
||||
val id = when {
|
||||
asset.bundle -> DriverBundle.install(activity, uri).id
|
||||
asset.linux -> LinuxVulkanDriverManager(activity).installDriver(uri, asset.name)
|
||||
else -> TurnipDriver(activity).installFromZip(uri, asset.name)
|
||||
}
|
||||
TurnipReleases.recordDownload(activity, asset, id)
|
||||
return id
|
||||
} finally {
|
||||
file?.let { com.droiddeck.launcher.core.FileUtils.delete(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* library - then say what happened. A refusal's message is the user-facing reason. A bundle,
|
||||
* from any import, installs both halves and is set as both drivers at once. [linux] null (the
|
||||
* tab's own Import .zip) takes a single driver to whichever list its libc belongs in.
|
||||
*/
|
||||
fun importDriver(uri: Uri, linux: Boolean) {
|
||||
fun importDriver(uri: Uri, linux: Boolean?) {
|
||||
val name = activity.displayNameOf(uri)
|
||||
Thread({
|
||||
var bundle: DriverBundle.Bundle? = null
|
||||
val problem = try {
|
||||
if (linux) LinuxVulkanDriverManager(activity).installDriver(uri, name)
|
||||
else TurnipDriver(activity).installFromZip(uri, name)
|
||||
when {
|
||||
DriverBundle.isBundle(activity, uri) -> bundle = DriverBundle.install(activity, uri)
|
||||
linux == true -> LinuxVulkanDriverManager(activity).installDriver(uri, name)
|
||||
linux == false -> TurnipDriver(activity).installFromZip(uri, name)
|
||||
else -> try {
|
||||
TurnipDriver(activity).installFromZip(uri, name)
|
||||
} catch (display: IllegalArgumentException) {
|
||||
try {
|
||||
LinuxVulkanDriverManager(activity).installDriver(uri, name)
|
||||
} catch (runtime: IllegalArgumentException) {
|
||||
throw IllegalArgumentException("Not a driver zip: neither an AdrenoTools driver, a -Linux Turnip nor an Android + Linux bundle")
|
||||
}
|
||||
}
|
||||
}
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.message
|
||||
@@ -88,26 +310,57 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
"Import failed: ${e.message}"
|
||||
}
|
||||
ui.post {
|
||||
val done = bundle?.let { useBundle(it) } ?: "Imported ${name ?: "driver"}"
|
||||
android.widget.Toast.makeText(
|
||||
activity, problem ?: "Imported ${name ?: "driver"}",
|
||||
if (problem != null) android.widget.Toast.LENGTH_LONG else android.widget.Toast.LENGTH_SHORT,
|
||||
activity, problem ?: done,
|
||||
if (problem != null || bundle != null) android.widget.Toast.LENGTH_LONG else android.widget.Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
refreshDrivers()
|
||||
}
|
||||
}, "import-driver").start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick one driver from a list, which is Manual (Auto would put its pair back). A bundle's half
|
||||
* sets the bundle's other half in the other list as well.
|
||||
*/
|
||||
fun selectDriver(id: String, linux: Boolean) {
|
||||
val bundle = DriverBundle.containing(activity, id, linux)
|
||||
when {
|
||||
bundle != null -> useBundle(bundle)
|
||||
linux -> SessionPrefs.setLinuxDriver(activity, id)
|
||||
else -> SessionPrefs.setAndroidDriver(activity, id)
|
||||
}
|
||||
setMode(false)
|
||||
refreshDrivers()
|
||||
}
|
||||
|
||||
/** Set both halves of [bundle] as the drivers in use; returns what to tell the user. */
|
||||
private fun useBundle(bundle: DriverBundle.Bundle): String {
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != bundle.androidId
|
||||
DriverBundle.select(activity, bundle)
|
||||
if (mode == SessionPrefs.GPU_DRIVERS_AUTO) setMode(false)
|
||||
return "Using ${bundle.label} as both the runtime and display driver." +
|
||||
if (displayChanged && CompositorHost.isStarted) " The display driver applies after DroidDeck restarts." else ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an imported or downloaded driver. A mode still set to it goes back to its default, so a
|
||||
* session never starts on a driver that is gone; a release download is forgotten, so the menu
|
||||
* offers it again.
|
||||
*/
|
||||
fun deleteDriver(id: String, linux: Boolean) {
|
||||
val bundle = DriverBundle.containing(activity, id, linux)
|
||||
if (bundle != null) {
|
||||
DriverBundle.remove(activity, bundle)
|
||||
TurnipReleases.forget(activity, bundle.id)
|
||||
android.widget.Toast.makeText(activity, "Deleted ${bundle.label} (both drivers)", android.widget.Toast.LENGTH_SHORT).show()
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
if (linux) {
|
||||
LinuxVulkanDriverManager(activity).removeDriver(id)
|
||||
for (mode in listOf(SessionService.MODE_STEAM, SessionService.MODE_DESKTOP)) {
|
||||
if (SessionPrefs.linuxDriver(activity, mode) == id) SessionPrefs.setLinuxDriver(activity, mode, "")
|
||||
}
|
||||
if (SessionPrefs.linuxDriver(activity) == id) SessionPrefs.setLinuxDriver(activity, "")
|
||||
} else {
|
||||
val td = TurnipDriver(activity)
|
||||
if (id in TurnipDriver.BUNDLED) td.hideBundled(id) else td.remove(id)
|
||||
@@ -121,11 +374,9 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
/** The download entries and the refresh line, from what the last check found. */
|
||||
fun refreshReleaseRows() {
|
||||
val check = TurnipReleases.cached(activity)
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
fun rows(linux: Boolean) = check?.assets.orEmpty()
|
||||
.filter { it.linux == linux }
|
||||
.filter { a -> TurnipReleases.installedId(activity, a) { id -> if (linux) lm.isInstalled(id) else td.isInstalled(id) } == null }
|
||||
.filter { it.bundle || it.linux == linux }
|
||||
.filter { a -> installedIdOf(a) == null }
|
||||
.map { a ->
|
||||
val mb = "%.1f MB".format(a.size / 1_048_576.0)
|
||||
com.droiddeck.launcher.ui.DownloadRow(a.name, "${a.source} ${a.tag}", "${a.label} · $mb", releaseProgress[a.name])
|
||||
@@ -154,7 +405,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
fun checkLatestTurnip() {
|
||||
if (releaseChecking) return
|
||||
releaseChecking = true
|
||||
releaseStatus = "Checking Banners-Turnip and WinNative…"
|
||||
releaseStatus = "Checking Banners-Turnip, WinNative and DroidDeck…"
|
||||
Thread({
|
||||
val problem = try { TurnipReleases.refresh(activity); null } catch (e: Exception) {
|
||||
Log.w(TAG, "latest Turnip check", e); e.message ?: "check failed"
|
||||
@@ -162,6 +413,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
if (problem != null) releaseStatus = "Couldn't check: $problem"
|
||||
}
|
||||
}, "turnip-release-check").start()
|
||||
@@ -174,28 +426,20 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
releaseProgress[assetName] = 0
|
||||
refreshReleaseRows()
|
||||
Thread({
|
||||
var file: java.io.File? = null
|
||||
val problem = try {
|
||||
file = TurnipReleases.download(activity, asset) { pct ->
|
||||
ui.post { releaseProgress[assetName] = pct; refreshReleaseRows() }
|
||||
}
|
||||
val uri = Uri.fromFile(file)
|
||||
val id = if (asset.linux) LinuxVulkanDriverManager(activity).installDriver(uri, asset.name)
|
||||
else TurnipDriver(activity).installFromZip(uri, asset.name)
|
||||
TurnipReleases.recordDownload(activity, asset, id)
|
||||
installAsset(asset) { pct -> ui.post { releaseProgress[assetName] = pct; refreshReleaseRows() } }
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.message
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "release driver download", e)
|
||||
"Download failed: ${e.message}"
|
||||
} finally {
|
||||
file?.let { com.droiddeck.launcher.core.FileUtils.delete(it) }
|
||||
}
|
||||
ui.post {
|
||||
releaseProgress.remove(assetName)
|
||||
val pick = if (asset.bundle) "pick it in either list to set both drivers" else "pick it in the menu"
|
||||
android.widget.Toast.makeText(
|
||||
activity, problem ?: "Installed ${asset.name.removeSuffix(".zip")} - pick it in the menu",
|
||||
activity, problem ?: "Installed ${asset.name.removeSuffix(".zip")} - $pick",
|
||||
android.widget.Toast.LENGTH_LONG,
|
||||
).show()
|
||||
refreshDrivers()
|
||||
|
||||
@@ -97,13 +97,11 @@ object HomeApp {
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
.setComponent(ComponentName(app.packageName, app.className))
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
if (displayId == null) {
|
||||
context.startActivity(intent)
|
||||
return
|
||||
}
|
||||
// Keep the target app's task separate from DroidDeck. Without this, Android can put the
|
||||
// app on top of the caller's task and move the entire task to the chosen display.
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
val options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
|
||||
context.startActivity(intent, options.toBundle())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,142 @@
|
||||
package com.droiddeck.launcher
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import com.droiddeck.launcher.ui.UpdatesActions
|
||||
import com.droiddeck.launcher.ui.UpdatesState
|
||||
import com.droiddeck.launcher.update.AppUpdates
|
||||
import com.droiddeck.launcher.update.SelfInstaller
|
||||
|
||||
/**
|
||||
* The Updates page's state: the channels' latest builds, the one followed, and an update being
|
||||
* downloaded or installed. Checked when the launcher comes to the front - opening the app, and
|
||||
* coming back from a session - at most once a minute.
|
||||
*/
|
||||
internal class UpdatesMenu(private val activity: Activity, private val ui: Handler) {
|
||||
// Read in start(): the activity has no context yet while its fields are made.
|
||||
var catalog by mutableStateOf<AppUpdates.Catalog?>(null)
|
||||
var follow by mutableStateOf(AppUpdates.Follow(AppUpdates.Channel.STABLE))
|
||||
var checking by mutableStateOf(false)
|
||||
var error by mutableStateOf<String?>(null)
|
||||
var stage by mutableStateOf<String?>(null)
|
||||
var percent by mutableIntStateOf(-1)
|
||||
var askPermission by mutableStateOf(false)
|
||||
// An install waiting for the user to allow "Install unknown apps"; it goes on when they come back.
|
||||
private var pending: AppUpdates.Release? = null
|
||||
private var lastCheck = 0L
|
||||
private var request = 0
|
||||
|
||||
fun state() = UpdatesState(catalog, follow, checking, error, stage, percent, askPermission)
|
||||
|
||||
fun actions() = UpdatesActions(
|
||||
onCheck = { check(force = true) },
|
||||
onFollow = ::choose,
|
||||
onInstall = ::install,
|
||||
onAllowInstalls = {
|
||||
askPermission = false
|
||||
runCatching { activity.startActivity(SelfInstaller.permissionIntent(activity)) }
|
||||
.onFailure { error = "Couldn't open Android's \"Install unknown apps\" setting"; pending = null }
|
||||
},
|
||||
onDismissPermission = { askPermission = false; pending = null },
|
||||
)
|
||||
|
||||
val hasUpdate: Boolean get() = AppUpdates.hasUpdate(catalog, follow)
|
||||
|
||||
/** On every return to the launcher: go on with an install the user just allowed, and check if due. */
|
||||
fun onResume() {
|
||||
pending?.let { r ->
|
||||
if (SelfInstaller.canInstall(activity)) { pending = null; install(r) }
|
||||
}
|
||||
if (!SessionState.running) check(force = false)
|
||||
}
|
||||
|
||||
fun check(force: Boolean) {
|
||||
if (checking || stage != null) return
|
||||
val now = System.currentTimeMillis()
|
||||
if (!force && now - lastCheck < 60_000) return
|
||||
lastCheck = now
|
||||
val id = ++request
|
||||
checking = true
|
||||
Thread({
|
||||
val result = runCatching { AppUpdates.refresh(activity) }
|
||||
ui.post {
|
||||
if (id != request) return@post
|
||||
checking = false
|
||||
result.onSuccess {
|
||||
catalog = it
|
||||
follow = AppUpdates.follow(activity, it)
|
||||
error = null
|
||||
}.onFailure { if (force) error = "Couldn't check: ${it.message ?: it.javaClass.simpleName}" }
|
||||
}
|
||||
}, "app-updates").start()
|
||||
}
|
||||
|
||||
private fun choose(f: AppUpdates.Follow) {
|
||||
AppUpdates.setFollow(activity, f)
|
||||
follow = f
|
||||
error = null
|
||||
}
|
||||
|
||||
private fun install(r: AppUpdates.Release) {
|
||||
if (stage != null) return
|
||||
val apk = r.apk ?: run { error = "This build has no download for this copy of DroidDeck"; return }
|
||||
AppUpdates.installBlock(r)?.let { error = it; return }
|
||||
if (SessionState.running) { error = "Stop the running session first, then update"; return }
|
||||
if (!SelfInstaller.canInstall(activity)) { pending = r; askPermission = true; return }
|
||||
error = null
|
||||
stage = "Downloading"
|
||||
percent = 0
|
||||
Thread({
|
||||
val result = runCatching {
|
||||
val file = SelfInstaller.download(activity, apk) { pct -> ui.post { percent = pct } }
|
||||
ui.post { stage = "Installing"; percent = -1 }
|
||||
SelfInstaller.install(activity, file)
|
||||
}
|
||||
result.onFailure { e -> ui.post { stage = null; error = e.message ?: "The update failed" } }
|
||||
}, "app-update-install").start()
|
||||
}
|
||||
|
||||
/** PackageInstaller's answer: Android's confirm prompt to show, or why it failed. Success restarts the app. */
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirm = if (Build.VERSION.SDK_INT >= 33) intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
else @Suppress("DEPRECATION") intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
if (confirm == null) { stage = null; error = "Android didn't offer to install the update"; return }
|
||||
stage = "Waiting for you to confirm"
|
||||
activity.startActivity(confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS -> stage = null
|
||||
else -> {
|
||||
stage = null
|
||||
error = SelfInstaller.describeFailure(status, intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** From onCreate: the last check, the channel followed, and PackageInstaller's answers. */
|
||||
fun start() {
|
||||
catalog = AppUpdates.cached(activity)
|
||||
follow = AppUpdates.follow(activity, catalog)
|
||||
val filter = IntentFilter(SelfInstaller.ACTION_STATUS)
|
||||
if (Build.VERSION.SDK_INT >= 33) activity.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||
else activity.registerReceiver(receiver, filter)
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
runCatching { activity.unregisterReceiver(receiver) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object AppUiPrefs {
|
||||
const val DEFAULT_SCALE = 100
|
||||
val scales = listOf(75, 85, 100, 115, 125, 150)
|
||||
|
||||
internal fun normalizeScale(percent: Int): Int = percent.takeIf { it in scales } ?: DEFAULT_SCALE
|
||||
|
||||
fun scale(context: Context): Int = normalizeScale(
|
||||
context.getSharedPreferences("app-ui", Context.MODE_PRIVATE).getInt("scale", DEFAULT_SCALE),
|
||||
)
|
||||
|
||||
fun setScale(context: Context, percent: Int) {
|
||||
context.getSharedPreferences("app-ui", Context.MODE_PRIVATE).edit()
|
||||
.putInt("scale", normalizeScale(percent)).apply()
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import com.droiddeck.launcher.runtime.DesktopCatalog
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntimeInstaller
|
||||
import com.droiddeck.launcher.session.EsyncPacks
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
@@ -107,25 +108,30 @@ object DeviceReport {
|
||||
h("Display")
|
||||
k("Session output", SessionState.outputSize?.let { "${it.first}x${it.second}" })
|
||||
k("Session refresh", String.format(Locale.US, "%.2f Hz", SessionState.refreshHz))
|
||||
k("Shape setting", SessionPrefs.shapeMode(context))
|
||||
k("Custom resolution (Steam)", SessionPrefs.customResolution(context, SessionService.MODE_STEAM)?.let { "${it.first}x${it.second}" } ?: "off")
|
||||
k("Custom resolution (Desktop)", SessionPrefs.customResolution(context, SessionService.MODE_DESKTOP)?.let { "${it.first}x${it.second}" } ?: "off")
|
||||
val panelSize = com.droiddeck.launcher.session.SessionDisplay.panelSize(context)
|
||||
k("Resolution (Steam)", SessionPrefs.resolutionChoice(context, SessionService.MODE_STEAM, panelSize))
|
||||
k("Resolution (Desktop)", SessionPrefs.resolutionChoice(context, SessionService.MODE_DESKTOP, panelSize))
|
||||
k("Foldable", context.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle"))
|
||||
|
||||
h("Drivers")
|
||||
val gpu = com.droiddeck.launcher.gpu.GpuInfo.detect()
|
||||
k("GPU", "${gpu.name} · ${gpu.family.label} · ${gpu.supportText}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "")
|
||||
k("Driver mode", SessionPrefs.gpuDriverMode(context) +
|
||||
(com.droiddeck.launcher.gpu.DriverPairs.recommendedKey(gpu, com.droiddeck.launcher.gpu.DriverPairs.from(
|
||||
com.droiddeck.launcher.gpu.TurnipReleases.cached(context)))?.let { " (recommended pair: $it)" } ?: ""))
|
||||
val turnip = TurnipDriver(context)
|
||||
val androidChoice = SessionPrefs.androidDriver(context)
|
||||
k("Display driver (chosen)", if (androidChoice.isEmpty()) "Auto -> ${turnip.autoId()}" else androidChoice)
|
||||
k(" name / version", "${turnip.displayName(if (androidChoice.isEmpty()) turnip.autoId() else androidChoice)} ${turnip.driverVersion(if (androidChoice.isEmpty()) turnip.autoId() else androidChoice)}".trim())
|
||||
k(" imported available", turnip.enumerateImported().ifEmpty { listOf("none") }.joinToString(", "))
|
||||
val lm = LinuxVulkanDriverManager(context)
|
||||
for (m in listOf(SessionService.MODE_STEAM, SessionService.MODE_DESKTOP)) {
|
||||
val id = SessionPrefs.linuxDriver(context, m)
|
||||
run {
|
||||
val id = SessionPrefs.linuxDriver(context)
|
||||
val label = if (id.isEmpty()) "runtime default (the Turnip built into the runtime)"
|
||||
else "${lm.getDriverName(id)} ${lm.getDriverVersion(id)}".trim() +
|
||||
lm.getMinGlibc(id).let { if (it.isEmpty()) "" else " (glibc $it+)" } +
|
||||
if (lm.isInstalled(id)) "" else " [MISSING - falls back to the runtime's own]"
|
||||
k("Linux driver ($m)", label)
|
||||
k("Linux driver", label)
|
||||
}
|
||||
k("Runtime's own ICD", LinuxRuntime.vulkanIcd(context)?.path)
|
||||
|
||||
@@ -144,11 +150,21 @@ object DeviceReport {
|
||||
k("Threaded GL (glthread)", SessionPrefs.glThread(context))
|
||||
k("No GL error checks", SessionPrefs.noGlError(context))
|
||||
k("Steam Deck mode", SessionPrefs.steamDeckMode(context))
|
||||
k("Steam controller", SessionPrefs.steamController(context))
|
||||
k("Upscaler", (SessionPrefs.upscalerChoices.firstOrNull { it.first == SessionPrefs.upscaler(context) }?.second ?: "Off") +
|
||||
" (sharpness ${SessionPrefs.upscaleSharpness(context)}%)")
|
||||
k("FEX preset", SessionPrefs.fexPreset(context).ifEmpty { "FEX defaults" })
|
||||
k("Skip xalia", SessionPrefs.noXalia(context))
|
||||
k("Wine sync", SessionPrefs.syncBackend(context))
|
||||
EsyncPacks.status(LinuxRuntime.rootDir(context)).let { k("droiddeck-esync packs", "${it.installed} installed, ${it.wanted} wanted") }
|
||||
k("Linux x86 (FEX)", if (com.droiddeck.launcher.runtime.LinuxFex.ready(context)) "ready" else "not set up")
|
||||
k("gamescope realtime", SessionPrefs.gamescopeRealtime(context))
|
||||
k("proot without seccomp", SessionPrefs.prootNoSeccomp(context))
|
||||
k("proot fast path", SessionPrefs.prootFastPath(context))
|
||||
k("Guest host name", SessionPrefs.guestHostname(context))
|
||||
k("DirectAudio for games", SessionPrefs.directAudio(context))
|
||||
k("Stretch games to fill", SessionPrefs.forceFullscreen(context))
|
||||
k("Stretch 16:9 to panel", SessionPrefs.stretch16x9(context))
|
||||
k("Client audio", if (SessionPrefs.clientDirectAudio(context)) "DirectAudio" else "classic")
|
||||
k("Microphone", SessionPrefs.micEnabled(context))
|
||||
k("On-screen controls", SessionPrefs.oscMode(context))
|
||||
@@ -161,11 +177,11 @@ object DeviceReport {
|
||||
})
|
||||
|
||||
h("Android process limits")
|
||||
k("Phantom proc monitor", PhantomProcessLimit.reportValue(PhantomProcessLimit.read(context.contentResolver)))
|
||||
k("Phantom proc monitor", PhantomProcessLimit.reportValue(PhantomProcessLimit.read(context)))
|
||||
|
||||
h("Device switch files in Download")
|
||||
for (name in listOf("droiddeck-env", "droiddeck-tu-debug", "droiddeck-driver",
|
||||
"droiddeck-osc", "droiddeck-no-pad", "droiddeck-pad-log",
|
||||
"droiddeck-osc", "droiddeck-no-pad",
|
||||
"droiddeck-no-hud", "droiddeck-wlr-renderer")) {
|
||||
val f = File(Environment.getExternalStorageDirectory(), "Download/$name")
|
||||
if (f.isFile) k(name, FileUtils.readString(f)?.trim()?.replace('\n', ' ')?.ifEmpty { "(present, empty)" } ?: "(present)")
|
||||
|
||||
@@ -1,42 +1,33 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
/**
|
||||
* FEXCore presets for the x86 games the Steam client launches, Bannerlator's table carried over
|
||||
* so a preset means the same thing in both apps. Each is a set of FEX_* variables put into the
|
||||
* session's environment; the games under FEX inherit them from the client. The default is FEX's
|
||||
* own defaults - no variables at all - which is what every session has run on so far.
|
||||
*
|
||||
* TSO = x86's total store ordering emulated on ARM: safest, slowest. MULTIBLOCK compiles more per
|
||||
* block. X87REDUCEDPRECISION trades x87 exactness for speed. SMCCHECKS=none is the risky one -
|
||||
* anything that writes its own code at runtime (JIT, .NET, DRM) can misbehave. DENUVO keeps the
|
||||
* checks at full and hides the hypervisor bit, which its anti-tamper looks for.
|
||||
*/
|
||||
import com.droiddeck.launcher.R
|
||||
|
||||
object FexPreset {
|
||||
class Preset(val id: String, val label: String, val detail: String, val env: List<String>)
|
||||
class Preset(val id: String, val label: Int, val detail: Int, val env: List<String>)
|
||||
|
||||
private fun tso(main: Int, vector: Int, memcpy: Int, halfBarrier: Int) = listOf(
|
||||
"FEX_TSOENABLED=$main", "FEX_VECTORTSOENABLED=$vector", "FEX_MEMCPYSETTSOENABLED=$memcpy", "FEX_HALFBARRIERTSOENABLED=$halfBarrier",
|
||||
)
|
||||
|
||||
val all: List<Preset> = listOf(
|
||||
Preset("", "FEX defaults", "Uses FEX defaults.", emptyList()),
|
||||
Preset("STABILITY", "Stability", "Safest, slowest settings.", tso(1, 1, 1, 1) + listOf("FEX_X87REDUCEDPRECISION=0", "FEX_MULTIBLOCK=0")),
|
||||
Preset("COMPATIBILITY", "Compatibility", "Full TSO with multiblock.", tso(1, 1, 1, 1) + listOf("FEX_X87REDUCEDPRECISION=0", "FEX_MULTIBLOCK=1")),
|
||||
Preset("INTERMEDIATE", "Intermediate", "Balanced TSO with reduced x87 precision.", tso(1, 0, 0, 1) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("PERFORMANCE", "Performance", "All TSO disabled; multiblock.", tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("PERFORMANCE_TSO", "Performance + TSO", "Performance settings with main TSO enabled.", tso(1, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("EXTREME", "Extreme", "Disables self-modifying-code checks. JIT games and DRM may break.",
|
||||
Preset("", R.string.fex_default_label, R.string.fex_default_detail, emptyList()),
|
||||
Preset("STABILITY", R.string.fex_stability_label, R.string.fex_stability_detail, tso(1, 1, 1, 1) + listOf("FEX_X87REDUCEDPRECISION=0", "FEX_MULTIBLOCK=0")),
|
||||
Preset("COMPATIBILITY", R.string.fex_compatibility_label, R.string.fex_compatibility_detail, tso(1, 1, 1, 1) + listOf("FEX_X87REDUCEDPRECISION=0", "FEX_MULTIBLOCK=1")),
|
||||
Preset("INTERMEDIATE", R.string.fex_intermediate_label, R.string.fex_intermediate_detail, tso(1, 0, 0, 1) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("PERFORMANCE", R.string.fex_performance_label, R.string.fex_performance_detail, tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("PERFORMANCE_TSO", R.string.fex_performance_tso_label, R.string.fex_performance_tso_detail, tso(1, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1")),
|
||||
Preset("EXTREME", R.string.fex_extreme_label, R.string.fex_extreme_detail,
|
||||
tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1", "FEX_SMCCHECKS=none", "FEX_DISABLEL2CACHE=1", "FEX_DYNAMICL1CACHE=1", "FEX_DYNAMICL1CACHEINCREASECOUNTHEURISTIC=250", "FEX_DYNAMICL1CACHEDECREASECOUNTHEURISTIC=50")),
|
||||
Preset("EXTREME_TSO", "Extreme + TSO", "Extreme settings with main TSO enabled.",
|
||||
Preset("EXTREME_TSO", R.string.fex_extreme_tso_label, R.string.fex_extreme_tso_detail,
|
||||
tso(1, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1", "FEX_SMCCHECKS=none", "FEX_DISABLEL2CACHE=1", "FEX_DYNAMICL1CACHE=1", "FEX_DYNAMICL1CACHEINCREASECOUNTHEURISTIC=250", "FEX_DYNAMICL1CACHEDECREASECOUNTHEURISTIC=50")),
|
||||
Preset("EXTREME_GN", "Extreme-gn", "Performance with TSC and metadata hints. Try before Extreme.",
|
||||
Preset("EXTREME_GN", R.string.fex_extreme_gn_label, R.string.fex_extreme_gn_detail,
|
||||
tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1", "FEX_SMALLTSCSCALE=1", "FEX_VOLATILEMETADATA=1")),
|
||||
Preset("DENUVO", "Denuvo", "Performance with full code checks and the hypervisor bit hidden.",
|
||||
Preset("DENUVO", R.string.fex_denuvo_label, R.string.fex_denuvo_detail,
|
||||
tso(0, 0, 0, 0) + listOf("FEX_X87REDUCEDPRECISION=1", "FEX_MULTIBLOCK=1", "FEX_SMCCHECKS=full", "FEX_HIDEHYPERVISORBIT=1")),
|
||||
)
|
||||
|
||||
fun byId(id: String): Preset = all.firstOrNull { it.id == id } ?: all[0]
|
||||
|
||||
/** The variables for the session's environment, in KEY=VALUE form; empty for the default. */
|
||||
/** Game-launch variables in KEY=VALUE form. */
|
||||
fun env(id: String): List<String> = byId(id).env
|
||||
}
|
||||
@@ -104,6 +104,7 @@ public final class FileUtils {
|
||||
public static String sizeToString(long bytes) {
|
||||
if (bytes >= 1L << 30) return String.format(java.util.Locale.US, "%.1f GB", bytes / (float) (1L << 30));
|
||||
if (bytes >= 1L << 20) return String.format(java.util.Locale.US, "%.0f MB", bytes / (float) (1L << 20));
|
||||
if (bytes >= 1L << 10) return String.format(java.util.Locale.US, "%.0f KB", bytes / (float) (1L << 10));
|
||||
return bytes + " B";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
object GameEnvironment {
|
||||
data class Config(
|
||||
val shared: Map<String, String?> = emptyMap(),
|
||||
val games: Map<String, Map<String, String?>> = emptyMap(),
|
||||
) {
|
||||
fun entries(scope: String) = if (scope.isEmpty()) shared else games[scope].orEmpty()
|
||||
|
||||
fun withEntries(scope: String, entries: Map<String, String?>): Config =
|
||||
if (scope.isEmpty()) copy(shared = entries.toMap())
|
||||
else copy(games = if (entries.isEmpty()) games - scope else games + (scope to entries.toMap()))
|
||||
}
|
||||
|
||||
private val namePattern = Regex("[A-Za-z_][A-Za-z0-9_]*")
|
||||
private val unused = setOf(
|
||||
"WINENTSYNC", "WINE_FAST_YIELD", "WINE_DO_NOT_CREATE_DXGI_DEVICE_MANAGER",
|
||||
"WINE_NEW_MEDIASOURCE", "WRAPPER_MAX_IMAGE_COUNT", "WRAPPER_DMAHEAP_CACHED",
|
||||
"ALSA_LATENCY_MS", "ALSA_VOLUME", "ALSA_BASS_BOOST", "ALSA_PERFORMANCE_MODE",
|
||||
"DXVK_ASYNC", "DXVK_GPLASYNCCACHE",
|
||||
)
|
||||
|
||||
fun validName(name: String) = namePattern.matches(name)
|
||||
fun supported(name: String) = name !in unused && !name.startsWith("BOX64_") && !name.startsWith("BOX86_")
|
||||
fun validValue(value: String) = '\u0000' !in value && value.length <= 8192
|
||||
fun validScope(scope: String) = scope.isEmpty() || (Regex("[1-9][0-9]*").matches(scope) && scope.toLongOrNull()?.let { it in 1..4294967295L } == true)
|
||||
|
||||
fun defaults(preset: String): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
"MESA_SHADER_CACHE_DISABLE" to "false",
|
||||
"VKD3D_FEATURE_LEVEL" to "12_2",
|
||||
"VKD3D_SHADER_MODEL" to "6_9",
|
||||
// .NET 7 and later reserve 256 GB of address space for the GC heap at startup, more than an
|
||||
// Android process has free, and the runtime fails to start. 32 GB is plenty for a game.
|
||||
"DOTNET_GCRegionRange" to "0x800000000",
|
||||
// .NET's W^X writes JIT code through a second mapping that FEX does not watch, so FEX keeps
|
||||
// running the stale translation and the game hangs at startup.
|
||||
"DOTNET_EnableWriteXorExecute" to "0",
|
||||
).apply {
|
||||
FexPreset.env(preset).forEach { put(it.substringBefore('='), it.substringAfter('=')) }
|
||||
}
|
||||
|
||||
fun effective(config: Config, preset: String, scope: String): Map<String, String?> =
|
||||
defaults(preset) + config.shared + if (scope.isEmpty()) emptyMap() else config.games[scope].orEmpty()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import com.droiddeck.launcher.R
|
||||
|
||||
object GameEnvironmentOptions {
|
||||
enum class Type { TOGGLE, CHOICE, MULTIPLE, NUMBER, TEXT }
|
||||
data class Option(val name: String, val value: String, val detail: Int, val type: Type, val choices: List<String> = emptyList())
|
||||
|
||||
val all = listOf(
|
||||
Option("MESA_SHADER_CACHE_DISABLE", "false", R.string.game_env_cache, Type.TOGGLE, listOf("false", "true")),
|
||||
Option("VKD3D_FEATURE_LEVEL", "12_2", R.string.game_env_feature, Type.CHOICE, listOf("12_2", "12_1", "12_0", "11_1", "11_0")),
|
||||
Option("VKD3D_SHADER_MODEL", "6_9", R.string.game_env_shader, Type.CHOICE, listOf("6_9", "6_8", "6_7", "6_6", "6_5", "6_4", "6_3", "6_2", "6_1", "6_0", "5_1")),
|
||||
Option("VKD3D_CONFIG", "nodxr", R.string.game_env_vkd3d_config, Type.MULTIPLE, listOf("nodxr", "single_queue", "no_upload_hvv")),
|
||||
Option("MESA_SHADER_CACHE_MAX_SIZE", "1G", R.string.game_env_cache_size, Type.CHOICE, listOf("512M", "1G", "2G", "4G")),
|
||||
Option("PROTON_LOG", "1", R.string.game_env_proton_log, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("PROTON_USE_WINED3D", "1", R.string.game_env_wined3d, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("PROTON_USE_XALIA", "0", R.string.game_env_xalia, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("PROTON_NO_NTSYNC", "1", R.string.game_env_no_ntsync, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("PROTON_NO_ESYNC", "1", R.string.game_env_no_esync, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("PROTON_NO_FSYNC", "1", R.string.game_env_no_fsync, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("BL_FSYNC_FIRST", "1", R.string.game_env_fsync_first, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("DXVK_HUD", "fps", R.string.game_env_hud, Type.MULTIPLE, listOf("scale=0.5", "scale=0.7", "opacity=0.5", "opacity=0.7", "devinfo", "fps", "frametimes", "submissions", "drawcalls", "pipelines", "descriptors", "memory", "gpuload", "version", "api", "cs", "compiler", "samplers")),
|
||||
Option("DXVK_CONFIG", "dxvk.maxFrameRate = 60", R.string.game_env_dxvk_config, Type.TEXT),
|
||||
Option("VKD3D_FRAME_RATE", "60", R.string.game_env_vkd3d_limit, Type.NUMBER),
|
||||
Option("mesa_glthread", "true", R.string.game_env_glthread, Type.TOGGLE, listOf("false", "true")),
|
||||
Option("ZINK_DESCRIPTORS", "auto", 0, Type.CHOICE, listOf("auto", "lazy", "cached", "notemplates")),
|
||||
Option("ZINK_DEBUG", "nir", 0, Type.MULTIPLE, listOf("nir", "spirv", "tgsi", "validation", "sync", "compact", "noreorder")),
|
||||
Option("SteamDeck", "0", 0, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("FD_DEV_FEATURES", "enable_tp_ubwc_flag_hint=1", 0, Type.MULTIPLE, listOf("enable_tp_ubwc_flag_hint=1", "storage_8bit=1")),
|
||||
Option("TU_DEBUG", "forcecb", 0, Type.MULTIPLE, listOf("forcecb", "nocb", "startup", "deck_emu", "nir", "nobin", "sysmem", "gmem", "forcebin", "layout", "noubwc", "nomultipos", "nolrz", "nolrzfc", "perf", "perfc", "flushall", "syncdraw", "push_consts_per_stage", "rast_order", "unaligned_store", "log_skip_gmem_ops", "dynamic", "bos", "3d_load", "fdm", "noconform", "rd")),
|
||||
Option("IR3_SHADER_DEBUG", "nouboopt", 0, Type.MULTIPLE, listOf("nouboopt", "nopreamble", "noearlypreamble")),
|
||||
Option("MESA_EXTENSION_MAX_YEAR", "", 0, Type.TEXT),
|
||||
Option("MESA_GL_VERSION_OVERRIDE", "", 0, Type.TEXT),
|
||||
Option("PULSE_LATENCY_MSEC", "", 0, Type.NUMBER),
|
||||
Option("WINE_LARGE_ADDRESS_AWARE", "0", 0, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("WINEDLLOVERRIDES", "", 0, Type.TEXT),
|
||||
Option("GALLIUM_HUD", "simple", 0, Type.MULTIPLE, listOf("simple", "fps", "frametime")),
|
||||
)
|
||||
|
||||
fun find(name: String): Option? = all.firstOrNull { it.name == name }
|
||||
|
||||
fun toggle(value: String, choice: String): String {
|
||||
val selected = value.split(',').filter { it.isNotEmpty() }.toMutableSet()
|
||||
if (!selected.remove(choice)) selected.add(choice)
|
||||
return selected.joinToString(",")
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,8 @@ object HostProcess {
|
||||
}
|
||||
}
|
||||
|
||||
private fun pidOf(process: Process): Int = try {
|
||||
@JvmStatic
|
||||
fun pidOf(process: Process): Int = try {
|
||||
val field = process.javaClass.getDeclaredField("pid")
|
||||
field.isAccessible = true
|
||||
val pid = field.getInt(process)
|
||||
|
||||
@@ -18,7 +18,7 @@ package com.droiddeck.launcher.core
|
||||
*
|
||||
* The device's OWN addresses are not kept: the client's IPv6 check logs "external address <ours>"
|
||||
* about twenty times a session, and a public IPv6 address is the user's. [learnOwnAddresses] reads
|
||||
* the link the session was given (etc/bannerlator-net) and every address on the same /64 - privacy
|
||||
* the link the session was given (etc/droiddeck-net) and every address on the same /64 - privacy
|
||||
* addresses rotate inside it - or an own public IPv4 is replaced wherever it appears. Likewise every
|
||||
* Steam account and persona name on the device ([learnAccounts]), for users who sign in with an
|
||||
* account name rather than an email.
|
||||
@@ -95,7 +95,7 @@ object LogRedactor {
|
||||
|
||||
/** Everything the session's runtime can tell about whose logs these are: addresses and accounts. */
|
||||
fun learnFromRuntime(root: java.io.File) {
|
||||
learnOwnAddresses(java.io.File(root, "etc/bannerlator-net"))
|
||||
learnOwnAddresses(java.io.File(root, "etc/droiddeck-net"))
|
||||
learnAccounts(java.io.File(root, "root/.local/share/Steam/config/loginusers.vdf"))
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,8 @@ import java.net.Inet6Address
|
||||
* `network.txt`: what the session was given to work with, for the reports that say "the client
|
||||
* thinks WiFi is off" or "Firefox cannot resolve anything".
|
||||
*
|
||||
* The runtime has no NetworkManager, so the client's own network panel is blind by design and says
|
||||
* so in ways that read like a fault. What actually matters is further down: whether the phone had
|
||||
* a validated link, which DNS servers the app wrote into the runtime's `resolv.conf`, and whether
|
||||
* the session can reach a name and an address at all.
|
||||
* The runtime's NetworkManager bridge reports Android's active link. Include whether the phone
|
||||
* had a validated link and which DNS servers the app wrote into the runtime's `resolv.conf`.
|
||||
*
|
||||
* **No network names.** The SSID is identifying and is deliberately not collected; the transport
|
||||
* type, the addresses' families and the DNS servers are what a diagnosis needs.
|
||||
@@ -93,14 +91,14 @@ object NetworkReport {
|
||||
} else {
|
||||
append("etc/resolv.conf does not exist yet - written when the session's link is published.\n")
|
||||
}
|
||||
val netdev = File(LinuxRuntime.rootDir(context), "etc/bannerlator-net")
|
||||
val netdev = File(LinuxRuntime.rootDir(context), "etc/droiddeck-net")
|
||||
if (netdev.isFile) {
|
||||
append("\netc/bannerlator-net (the link as the session sees it):\n")
|
||||
append("\netc/droiddeck-net (the link as the session sees it):\n")
|
||||
FileUtils.readString(netdev)?.lines()?.forEach { append(" ").append(it).append('\n') }
|
||||
}
|
||||
append("\nproot makes no network namespace, so the session uses the phone's connection\n")
|
||||
append("directly: IPv4 and IPv6 both pass through, and there is nothing to forward.\n")
|
||||
append("The Steam client's own network panel drives NetworkManager over D-Bus, which this\n")
|
||||
append("runtime does not have - \"wifi is off\" there is cosmetic and not evidence.\n")
|
||||
append("Steam reads Android's transport, connectivity and metering through the runtime's\n")
|
||||
append("NetworkManager D-Bus bridge. Android settings manage Wi-Fi and cellular connections.\n")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
|
||||
@@ -14,17 +14,53 @@ enum class PhantomProcessStatus {
|
||||
}
|
||||
|
||||
object PhantomProcessLimit {
|
||||
const val ADB_COMMAND = "adb shell settings put global settings_enable_monitor_phantom_procs false"
|
||||
const val SHELL_COMMAND = "settings put global settings_enable_monitor_phantom_procs false"
|
||||
private const val SETTING = "settings_enable_monitor_phantom_procs"
|
||||
|
||||
fun adbCommand(enabled: Boolean): String =
|
||||
"adb shell settings put global $SETTING ${if (enabled) "true" else "false"}"
|
||||
private const val MAX_PHANTOM = "2147483647"
|
||||
private const val PREFS = "phantom-process-limit"
|
||||
private const val PREF_ANDROID_12_OFF = "android12-off"
|
||||
|
||||
fun read(resolver: ContentResolver, sdk: Int = Build.VERSION.SDK_INT): PhantomProcessStatus {
|
||||
fun usesDeviceConfig(sdk: Int = Build.VERSION.SDK_INT): Boolean = sdk == Build.VERSION_CODES.S
|
||||
|
||||
fun shellCommands(enabled: Boolean, sdk: Int = Build.VERSION.SDK_INT): List<String> = when {
|
||||
usesDeviceConfig(sdk) && enabled -> listOf(
|
||||
"device_config delete activity_manager max_phantom_processes",
|
||||
"device_config set_sync_disabled_for_tests none",
|
||||
)
|
||||
usesDeviceConfig(sdk) -> listOf(
|
||||
"device_config set_sync_disabled_for_tests persistent",
|
||||
"device_config put activity_manager max_phantom_processes $MAX_PHANTOM",
|
||||
)
|
||||
else -> listOf("settings put global $SETTING ${if (enabled) "true" else "false"}")
|
||||
}
|
||||
|
||||
fun verifyCommand(sdk: Int = Build.VERSION.SDK_INT): String =
|
||||
if (usesDeviceConfig(sdk)) "device_config get activity_manager max_phantom_processes"
|
||||
else "settings get global $SETTING"
|
||||
|
||||
fun verified(output: String, enabled: Boolean, sdk: Int = Build.VERSION.SDK_INT): Boolean {
|
||||
val value = output.trim()
|
||||
return if (usesDeviceConfig(sdk)) (value == MAX_PHANTOM) != enabled
|
||||
else value == if (enabled) "true" else "false"
|
||||
}
|
||||
|
||||
fun adbCommand(enabled: Boolean, sdk: Int = Build.VERSION.SDK_INT): String =
|
||||
shellCommands(enabled, sdk).joinToString(" && ") { "adb shell $it" }
|
||||
|
||||
fun adbCommand(): String = adbCommand(false)
|
||||
|
||||
fun rememberAndroid12(context: Context, off: Boolean) {
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putBoolean(PREF_ANDROID_12_OFF, off).apply()
|
||||
}
|
||||
|
||||
fun read(context: Context, sdk: Int = Build.VERSION.SDK_INT): PhantomProcessStatus {
|
||||
if (sdk < Build.VERSION_CODES.S) return PhantomProcessStatus.NOT_APPLICABLE
|
||||
if (usesDeviceConfig(sdk)) {
|
||||
val off = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(PREF_ANDROID_12_OFF, false)
|
||||
return if (off) PhantomProcessStatus.DISABLED else PhantomProcessStatus.UNREADABLE
|
||||
}
|
||||
val global = try {
|
||||
Settings.Global.getString(resolver, SETTING)
|
||||
Settings.Global.getString(context.contentResolver, SETTING)
|
||||
} catch (_: Exception) {
|
||||
return PhantomProcessStatus.UNREADABLE
|
||||
}
|
||||
@@ -57,6 +93,8 @@ object PhantomProcessLimit {
|
||||
null
|
||||
}
|
||||
|
||||
fun hasDeveloperToggle(sdk: Int = Build.VERSION.SDK_INT): Boolean = sdk >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
||||
|
||||
fun blocksSteam(status: PhantomProcessStatus): Boolean =
|
||||
status != PhantomProcessStatus.NOT_APPLICABLE && status != PhantomProcessStatus.DISABLED
|
||||
|
||||
@@ -69,20 +107,20 @@ object PhantomProcessLimit {
|
||||
}
|
||||
|
||||
fun instructions(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.ENABLED -> "Turn off “Restrict child processes” in Developer options. If you cannot find that option, connect this device to a computer with ADB and run the command below. Return here and check again."
|
||||
PhantomProcessStatus.UNSET -> "This setting is unset, so the ROM default applies and may enforce the limit. Set “Restrict child processes” to off in Developer options. If you cannot find that option, connect this device to a computer with ADB and run the command below. Return here and check again."
|
||||
PhantomProcessStatus.UNREADABLE -> "DroidDeck could not read the setting. Set “Restrict child processes” to off in Developer options. If you cannot find that option, connect this device to a computer with ADB and run the command below. Return here and check again."
|
||||
PhantomProcessStatus.ENABLED, PhantomProcessStatus.UNSET, PhantomProcessStatus.UNREADABLE -> fixSentence() + " If that is not possible, connect this device to a computer with ADB and run the command below. Return here and check again."
|
||||
PhantomProcessStatus.DISABLED -> "Steam sessions can start."
|
||||
PhantomProcessStatus.NOT_APPLICABLE -> "This Android version does not use this limit."
|
||||
}
|
||||
|
||||
fun gateInstructions(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.ENABLED -> "Turn off “Restrict child processes” in Developer options."
|
||||
PhantomProcessStatus.UNSET -> "The ROM default may enforce this limit. Explicitly turn off “Restrict child processes” in Developer options."
|
||||
PhantomProcessStatus.UNREADABLE -> "DroidDeck could not check this setting. Turn off “Restrict child processes” in Developer options."
|
||||
PhantomProcessStatus.ENABLED, PhantomProcessStatus.UNSET, PhantomProcessStatus.UNREADABLE -> fixSentence()
|
||||
else -> instructions(status)
|
||||
}
|
||||
|
||||
fun fixSentence(sdk: Int = Build.VERSION.SDK_INT): String =
|
||||
if (hasDeveloperToggle(sdk)) "Turn on “Disable child process restrictions” in Developer options, and keep Developer options on afterwards."
|
||||
else "This Android version has no switch for it. Use Wireless debugging to change it from this device."
|
||||
|
||||
fun reportValue(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.DISABLED -> "disabled (good - the OS will not kill the session's children)"
|
||||
PhantomProcessStatus.ENABLED -> "ENABLED (the OS may kill the session with no log; turn off Restrict child processes)"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import java.util.Locale
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Texture filtering for DirectX 9-11 games, as DXVK configuration options
|
||||
* (d3d9/d3d11.samplerAnisotropy and samplerLodBias). VKD3D-Proton and WineD3D do not read them.
|
||||
* Published into the game environment and read by DXVK when a game starts, so a change lands on
|
||||
* the next launch, not the running game.
|
||||
*/
|
||||
object TextureFiltering {
|
||||
/** 0 = the game's own filtering; otherwise that anisotropy on every linearly filtered sampler. */
|
||||
val ANISOTROPY = listOf(0, 2, 4, 8, 16)
|
||||
|
||||
/** Negative mip LOD bias added to the game's own: sharper textures at the cost of some shimmer. */
|
||||
const val LOD_BIAS_OFF = "0"
|
||||
/** Derived from how far the session is enlarged to the panel (see [autoLodBias]). */
|
||||
const val LOD_BIAS_AUTO = "auto"
|
||||
val LOD_BIAS = listOf(LOD_BIAS_OFF, LOD_BIAS_AUTO, "-0.25", "-0.5", "-0.75", "-1.0")
|
||||
|
||||
private const val LOD_BIAS_MIN = -2f
|
||||
|
||||
/**
|
||||
* The mip LOD bias for a picture enlarged by [scale] (panel size over session size, long side
|
||||
* to long side): -log2(scale), AMD's FSR 1 guidance (a 1.5x enlargement - 720p on a 1080p
|
||||
* panel - gives -0.58). 0 when the session is not enlarged or the scale is unknown.
|
||||
*/
|
||||
fun autoLodBias(scale: Float): Float {
|
||||
if (scale.isNaN() || scale <= 1f) return 0f
|
||||
return max((-ln(scale.toDouble()) / ln(2.0)).toFloat(), LOD_BIAS_MIN)
|
||||
}
|
||||
|
||||
fun lodBias(choice: String, autoBias: Float): Float = when (choice) {
|
||||
LOD_BIAS_OFF, "" -> 0f
|
||||
LOD_BIAS_AUTO -> autoBias
|
||||
else -> choice.toFloatOrNull()?.let { max(min(it, 0f), LOD_BIAS_MIN) } ?: 0f
|
||||
}
|
||||
|
||||
/** The DXVK_CONFIG options for the two settings, "" when both are left to the game. */
|
||||
fun dxvkOptions(anisotropy: Int, lodBiasChoice: String, autoBias: Float): String {
|
||||
val parts = ArrayList<String>(4)
|
||||
val aniso = if (anisotropy in ANISOTROPY) anisotropy else 0
|
||||
if (aniso > 0) {
|
||||
parts += "d3d9.samplerAnisotropy = $aniso"
|
||||
parts += "d3d11.samplerAnisotropy = $aniso"
|
||||
}
|
||||
val bias = lodBias(lodBiasChoice, autoBias)
|
||||
if (bias < 0f) {
|
||||
// Locale.US: DXVK parses "-0.58", never "-0,58".
|
||||
val b = String.format(Locale.US, "%.2f", bias)
|
||||
parts += "d3d9.samplerLodBias = $b"
|
||||
parts += "d3d11.samplerLodBias = $b"
|
||||
}
|
||||
return parts.joinToString("; ")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
|
||||
/** Location is used only for Android's Wi-Fi names and scan results, never coordinates. */
|
||||
object WifiDiscovery {
|
||||
val permissions = arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
|
||||
// targetSdk 28 accepts either grant for Wi-Fi scanning.
|
||||
fun permissionGranted(context: Context): Boolean = permissions.any {
|
||||
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun locationEnabled(context: Context): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
|
||||
(context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager)?.isLocationEnabled == true
|
||||
|
||||
fun available(context: Context): Boolean = SessionPrefs.wifiDiscoveryEnabled(context) &&
|
||||
permissionGranted(context) && locationEnabled(context)
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import android.net.nsd.NsdServiceInfo
|
||||
import com.flyfishxu.kadb.Kadb
|
||||
import com.flyfishxu.kadb.cert.KadbCert
|
||||
import java.io.File
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -19,6 +21,7 @@ object WirelessAdbFix {
|
||||
private const val KEY_FILE = "wireless-adb-key.pem"
|
||||
private const val HOST_FILE = "wireless-adb-host.txt"
|
||||
private const val CONNECTION_FILE = "wireless-adb-connection.txt"
|
||||
const val LOOPBACK = "127.0.0.1"
|
||||
|
||||
private data class SavedConnection(val host: String, val port: Int?)
|
||||
|
||||
@@ -76,9 +79,27 @@ object WirelessAdbFix {
|
||||
}
|
||||
|
||||
/** Finds this paired device's separate TLS connection port from Android's ADB mDNS record. */
|
||||
fun isLocalAddress(address: InetAddress): Boolean = runCatching {
|
||||
if (address.isLoopbackAddress) return true
|
||||
NetworkInterface.getNetworkInterfaces()?.toList().orEmpty().any { network ->
|
||||
network.inetAddresses.toList().any { it.address.contentEquals(address.address) }
|
||||
}
|
||||
}.getOrDefault(address is Inet4Address)
|
||||
|
||||
fun localConnectPort(context: Context): Int? =
|
||||
tlsPortProperty()
|
||||
?: findConnectPort(context, LOOPBACK)
|
||||
|
||||
private fun tlsPortProperty(): Int? = runCatching {
|
||||
Class.forName("android.os.SystemProperties").getMethod("get", String::class.java)
|
||||
.invoke(null, "service.adb.tls.port") as? String
|
||||
}.getOrNull()?.trim()?.toIntOrNull()?.takeIf { it in 1..65535 }
|
||||
|
||||
fun findConnectPort(context: Context, pairedHost: String, timeoutSeconds: Long = 12): Int? {
|
||||
val manager = context.applicationContext.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||
val expectedAddress = runCatching { InetAddress.getByName(pairedHost).address }.getOrNull() ?: return null
|
||||
val paired = runCatching { InetAddress.getByName(pairedHost) }.getOrNull() ?: return null
|
||||
val matches: (InetAddress) -> Boolean =
|
||||
if (paired.isLoopbackAddress) ::isLocalAddress else { address -> address.address.contentEquals(paired.address) }
|
||||
val resolvedPort = AtomicInteger(-1)
|
||||
val finished = AtomicBoolean(false)
|
||||
val latch = CountDownLatch(1)
|
||||
@@ -101,8 +122,8 @@ object WirelessAdbFix {
|
||||
}
|
||||
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||
val address = serviceInfo.host?.address
|
||||
if (address != null && address.contentEquals(expectedAddress) && finished.compareAndSet(false, true)) {
|
||||
val address = serviceInfo.host
|
||||
if (address != null && matches(address) && finished.compareAndSet(false, true)) {
|
||||
resolvedPort.set(serviceInfo.port)
|
||||
latch.countDown()
|
||||
} else {
|
||||
@@ -148,14 +169,16 @@ object WirelessAdbFix {
|
||||
fun setChildProcessLimit(context: Context, host: String, port: Int, enabled: Boolean) {
|
||||
loadOrCreateIdentity(context.applicationContext)
|
||||
Kadb.create(host, port).use { adb ->
|
||||
val value = if (enabled) "true" else "false"
|
||||
val change = adb.shell("settings put global settings_enable_monitor_phantom_procs $value")
|
||||
check(change.exitCode == 0) { change.allOutput.ifBlank { "ADB command failed (${change.exitCode})" } }
|
||||
val result = adb.shell("settings get global settings_enable_monitor_phantom_procs")
|
||||
check(result.exitCode == 0 && result.output.trim() == value) {
|
||||
PhantomProcessLimit.shellCommands(enabled).forEach { command ->
|
||||
val change = adb.shell(command)
|
||||
check(change.exitCode == 0) { change.allOutput.ifBlank { "ADB command failed (${change.exitCode})" } }
|
||||
}
|
||||
val result = adb.shell(PhantomProcessLimit.verifyCommand())
|
||||
check(result.exitCode == 0 && PhantomProcessLimit.verified(result.output, enabled)) {
|
||||
"Android did not confirm the child-process limit was changed: ${result.allOutput.trim()}"
|
||||
}
|
||||
}
|
||||
if (PhantomProcessLimit.usesDeviceConfig()) PhantomProcessLimit.rememberAndroid12(context, !enabled)
|
||||
saveConnection(context, host, port)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.RemoteInput
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import com.droiddeck.launcher.MainActivity
|
||||
import com.droiddeck.launcher.R
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class WirelessAdbPairingService : Service() {
|
||||
sealed interface Stage {
|
||||
data object Idle : Stage
|
||||
data object Waiting : Stage
|
||||
data class CodeNeeded(val error: String? = null) : Stage
|
||||
data class Working(val step: String) : Stage
|
||||
data object Done : Stage
|
||||
data class Failed(val error: String) : Stage
|
||||
}
|
||||
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
private var nsd: NsdManager? = null
|
||||
private var discovery: NsdManager.DiscoveryListener? = null
|
||||
private var pairingHost: String? = null
|
||||
private var pairingPort: Int? = null
|
||||
@Volatile private var working = false
|
||||
private val timeout = Runnable { finish(Stage.Failed("Timed out waiting for the pairing pop-up. Try again from DroidDeck.")) }
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_CODE -> {
|
||||
val code = RemoteInput.getResultsFromIntent(intent)?.getCharSequence(KEY_CODE)
|
||||
?.filter(Char::isDigit)?.toString().orEmpty()
|
||||
onCode(code)
|
||||
}
|
||||
ACTION_CANCEL -> finish(Stage.Idle)
|
||||
else -> begin()
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun begin() {
|
||||
pairingHost = null
|
||||
pairingPort = null
|
||||
working = false
|
||||
publish(Stage.Waiting)
|
||||
startForeground(NOTIFICATION_ID, notification(Stage.Waiting))
|
||||
startDiscovery()
|
||||
main.removeCallbacks(timeout)
|
||||
main.postDelayed(timeout, TIMEOUT_MS)
|
||||
}
|
||||
|
||||
private fun startDiscovery() {
|
||||
stopDiscovery()
|
||||
val manager = getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||
val listener = object : NsdManager.DiscoveryListener {
|
||||
override fun onDiscoveryStarted(serviceType: String) = Unit
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
|
||||
if (serviceInfo.serviceType?.contains(PAIRING_TYPE) != true) return
|
||||
runCatching { manager.resolveService(serviceInfo, resolveListener()) }
|
||||
}
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo) = Unit
|
||||
override fun onDiscoveryStopped(serviceType: String) = Unit
|
||||
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||
main.post { finish(Stage.Failed("Could not look for the pairing pop-up ($errorCode). Check that Wi-Fi is on.")) }
|
||||
}
|
||||
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) = Unit
|
||||
}
|
||||
runCatching { manager.discoverServices("$PAIRING_TYPE.", NsdManager.PROTOCOL_DNS_SD, listener) }
|
||||
.onSuccess { nsd = manager; discovery = listener }
|
||||
.onFailure { finish(Stage.Failed("Could not look for the pairing pop-up: ${it.localizedMessage}")) }
|
||||
}
|
||||
|
||||
private fun resolveListener() = object : NsdManager.ResolveListener {
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) = Unit
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||
val address = serviceInfo.host ?: return
|
||||
if (!WirelessAdbFix.isLocalAddress(address)) return
|
||||
main.post {
|
||||
if (working || current.value == Stage.Done) return@post
|
||||
pairingHost = address.hostAddress
|
||||
pairingPort = serviceInfo.port
|
||||
if (current.value !is Stage.CodeNeeded) show(Stage.CodeNeeded())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCode(code: String) {
|
||||
val host = pairingHost
|
||||
val port = pairingPort
|
||||
if (working) return
|
||||
if (host == null || port == null) {
|
||||
show(Stage.Waiting)
|
||||
return
|
||||
}
|
||||
if (code.length != 6) {
|
||||
show(Stage.CodeNeeded("Enter all 6 digits of the code"))
|
||||
return
|
||||
}
|
||||
working = true
|
||||
show(Stage.Working("Pairing…"))
|
||||
Thread({
|
||||
val paired = runCatching { kotlinx.coroutines.runBlocking { WirelessAdbFix.pair(this@WirelessAdbPairingService, WirelessAdbFix.LOOPBACK, port, code) } }
|
||||
if (paired.isFailure) {
|
||||
main.post {
|
||||
working = false
|
||||
pairingPort = null
|
||||
show(Stage.CodeNeeded("That code didn't work. Open the pairing pop-up again and enter its new code."))
|
||||
}
|
||||
return@Thread
|
||||
}
|
||||
main.post {
|
||||
stopDiscovery()
|
||||
show(Stage.Working("Paired. Connecting…"))
|
||||
}
|
||||
val result = runCatching {
|
||||
val connectPort = WirelessAdbFix.localConnectPort(this)
|
||||
?: error("Paired, but the Wireless debugging port was not found. Keep Wireless debugging on and try again.")
|
||||
main.post { show(Stage.Working("Applying the setting…")) }
|
||||
WirelessAdbFix.setChildProcessLimit(this, WirelessAdbFix.LOOPBACK, connectPort, false)
|
||||
}
|
||||
main.post {
|
||||
working = false
|
||||
finish(result.exceptionOrNull()?.let { Stage.Failed(it.localizedMessage ?: "Wireless debugging command failed") } ?: Stage.Done)
|
||||
}
|
||||
}, "wireless-adb-pairing").start()
|
||||
}
|
||||
|
||||
private fun show(stage: Stage) {
|
||||
publish(stage)
|
||||
getSystemService(NotificationManager::class.java)?.notify(NOTIFICATION_ID, notification(stage))
|
||||
}
|
||||
|
||||
private fun finish(stage: Stage) {
|
||||
main.removeCallbacks(timeout)
|
||||
stopDiscovery()
|
||||
publish(stage)
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
if (stage is Stage.Done || stage is Stage.Failed) {
|
||||
manager?.notify(NOTIFICATION_ID, notification(stage))
|
||||
stopForeground(STOP_FOREGROUND_DETACH)
|
||||
} else {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
manager?.cancel(NOTIFICATION_ID)
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun stopDiscovery() {
|
||||
val listener = discovery ?: return
|
||||
runCatching { nsd?.stopServiceDiscovery(listener) }
|
||||
discovery = null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
main.removeCallbacks(timeout)
|
||||
stopDiscovery()
|
||||
if (current.value.let { it is Stage.Waiting || it is Stage.CodeNeeded || it is Stage.Working }) publish(Stage.Idle)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun notification(stage: Stage): Notification {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager?.createNotificationChannel(NotificationChannel(CHANNEL_ID, "Wireless debugging setup",
|
||||
NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
description = "Asks for the Wireless debugging pairing code while Settings is open"
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
enableVibration(false)
|
||||
})
|
||||
val open = PendingIntent.getActivity(this, 0,
|
||||
Intent(this, MainActivity::class.java).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
|
||||
PendingIntent.FLAG_IMMUTABLE)
|
||||
val cancel = PendingIntent.getService(this, 1,
|
||||
Intent(this, WirelessAdbPairingService::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_IMMUTABLE)
|
||||
val builder = Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_session)
|
||||
.setContentIntent(open)
|
||||
.setShowWhen(false)
|
||||
.setCategory(Notification.CATEGORY_STATUS)
|
||||
when (stage) {
|
||||
Stage.Waiting, Stage.Idle -> builder
|
||||
.setContentTitle("Pair Wireless debugging")
|
||||
.setContentText("Turn on Wireless debugging, then tap “Pair device with pairing code”.")
|
||||
.setStyle(Notification.BigTextStyle().bigText(
|
||||
"Turn on Wireless debugging, then tap “Pair device with pairing code”. Stay in Settings: the code goes in this notification."))
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.addAction(Notification.Action.Builder(null, "Cancel", cancel).build())
|
||||
is Stage.CodeNeeded -> {
|
||||
val reply = PendingIntent.getService(this, 2,
|
||||
Intent(this, WirelessAdbPairingService::class.java).setAction(ACTION_CODE),
|
||||
if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
else PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val input = RemoteInput.Builder(KEY_CODE).setLabel("6-digit pairing code").build()
|
||||
val text = stage.error ?: "Tap Enter code and type the 6 digits shown in Settings."
|
||||
builder
|
||||
.setContentTitle(if (stage.error == null) "Enter the Wi-Fi pairing code" else "Try the code again")
|
||||
.setContentText(text)
|
||||
.setStyle(Notification.BigTextStyle().bigText(text))
|
||||
.setOngoing(true)
|
||||
.addAction(Notification.Action.Builder(null, "Enter code", reply).addRemoteInput(input).build())
|
||||
.addAction(Notification.Action.Builder(null, "Cancel", cancel).build())
|
||||
}
|
||||
is Stage.Working -> builder
|
||||
.setContentTitle("Setting up Steam")
|
||||
.setContentText(stage.step)
|
||||
.setProgress(0, 0, true)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
Stage.Done -> builder
|
||||
.setContentTitle("Steam is ready")
|
||||
.setContentText("Tap to go back to DroidDeck. You can turn Wireless debugging off now.")
|
||||
.setStyle(Notification.BigTextStyle().bigText("Tap to go back to DroidDeck. You can turn Wireless debugging off now."))
|
||||
.setAutoCancel(true)
|
||||
is Stage.Failed -> builder
|
||||
.setContentTitle("Wireless debugging setup stopped")
|
||||
.setContentText(stage.error)
|
||||
.setStyle(Notification.BigTextStyle().bigText(stage.error))
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "wireless-adb-pairing"
|
||||
private const val NOTIFICATION_ID = 3
|
||||
private const val PAIRING_TYPE = "_adb-tls-pairing._tcp"
|
||||
private const val ACTION_CODE = "com.droiddeck.launcher.action.WIRELESS_ADB_CODE"
|
||||
private const val ACTION_CANCEL = "com.droiddeck.launcher.action.WIRELESS_ADB_CANCEL"
|
||||
private const val KEY_CODE = "code"
|
||||
private const val TIMEOUT_MS = 5 * 60_000L
|
||||
|
||||
private val current = MutableStateFlow<Stage>(Stage.Idle)
|
||||
val stage: StateFlow<Stage> = current
|
||||
|
||||
private fun publish(stage: Stage) { current.value = stage }
|
||||
|
||||
fun start(context: Context) {
|
||||
context.getSystemService(NotificationManager::class.java)?.cancel(NOTIFICATION_ID)
|
||||
publish(Stage.Waiting)
|
||||
val intent = Intent(context, WirelessAdbPairingService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent)
|
||||
}
|
||||
|
||||
fun cancel(context: Context) {
|
||||
if (current.value.let { it is Stage.Done || it is Stage.Failed || it is Stage.Idle }) {
|
||||
publish(Stage.Idle)
|
||||
return
|
||||
}
|
||||
context.startService(Intent(context, WirelessAdbPairingService::class.java).setAction(ACTION_CANCEL))
|
||||
}
|
||||
|
||||
fun notificationsEnabled(context: Context): Boolean =
|
||||
context.getSystemService(NotificationManager::class.java)?.areNotificationsEnabled() != false
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class FileManagerActivity : ComponentActivity() {
|
||||
}
|
||||
val initialDir = intent.getStringExtra(EXTRA_START_DIR)?.let(::File)
|
||||
setContent {
|
||||
DroidDeckTheme {
|
||||
DroidDeckTheme(appScale = com.droiddeck.launcher.core.AppUiPrefs.scale(this)) {
|
||||
Surface(modifier = Modifier.fillMaxSize().systemBarsPadding(), color = MaterialTheme.colorScheme.background) {
|
||||
FileManagerScreen(initialDir = initialDir)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class FilePickerActivity : ComponentActivity() {
|
||||
?.let { File(it) }?.takeIf { it.isDirectory }
|
||||
|
||||
setContent {
|
||||
DroidDeckTheme {
|
||||
DroidDeckTheme(appScale = com.droiddeck.launcher.core.AppUiPrefs.scale(this)) {
|
||||
Surface(modifier = Modifier.fillMaxSize().systemBarsPadding(), color = MaterialTheme.colorScheme.background) {
|
||||
FileManagerScreen(
|
||||
pickMode = true,
|
||||
|
||||
Loaded 100 of 393 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user