mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
208
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c197a017e6 | ||
|
|
15e8a57fc0 | ||
|
|
cd4f02d0b0 | ||
|
|
bf14487872 | ||
|
|
8035963559 | ||
|
|
cf556596f0 | ||
|
|
7f1a743733 | ||
|
|
6a9c5da0d0 | ||
|
|
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 | ||
|
|
051b9a320e | ||
|
|
71057fefe1 | ||
|
|
49934ce933 | ||
|
|
5179b54745 |
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
|
||||
+116
-9
@@ -28,7 +28,7 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
PRELOAD_CACHE_VERSION: v2
|
||||
PRELOAD_CACHE_VERSION: v3
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -148,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
|
||||
@@ -155,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'
|
||||
@@ -174,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
|
||||
@@ -196,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
|
||||
@@ -206,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"
|
||||
@@ -221,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
|
||||
@@ -285,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
|
||||
@@ -308,6 +366,7 @@ 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'
|
||||
@@ -372,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 -->"
|
||||
@@ -385,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"
|
||||
@@ -454,6 +522,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
env:
|
||||
CI_REPO: Droid-Deck/DroidDeck-CI
|
||||
KEEP: 10
|
||||
@@ -472,6 +541,34 @@ jobs:
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
|
||||
# What this build is, for the top of the release notes: the merged PR's title and the first
|
||||
# paragraph of its description, or the commit's subject for a push made without a PR.
|
||||
- name: Describe the change
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
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:
|
||||
@@ -481,7 +578,11 @@ jobs:
|
||||
tag=main-${GITHUB_SHA::7}
|
||||
cat signed/SHA256SUMS-*.txt > signed/SHA256SUMS.txt
|
||||
rm signed/SHA256SUMS-*.txt
|
||||
notes="Signed build of $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA - unreleased, for testing.
|
||||
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:
|
||||
@@ -543,3 +644,9 @@ jobs:
|
||||
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,100 +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" =~ ^com\.droiddeck\.launcher\ [0-9]+$ &&
|
||||
${candidate##* } -ge ${stable##* } ]] || {
|
||||
echo "::error::PR package/versionCode '$candidate' cannot install over 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"
|
||||
|
||||
@@ -103,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]
|
||||
@@ -115,11 +175,34 @@ 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
|
||||
# What this build is, for the top of the release notes: the PR's title and the first
|
||||
# paragraph of its description.
|
||||
- name: Describe the change
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
|
||||
title=$(jq -r .title <<<"$pr")
|
||||
summary=$(jq -r '.body // ""' <<<"$pr" | tr -d '\r' | { grep -v '^🤖' || true; } |
|
||||
awk '!p && /^#/ { next } NF { p = 1; print; next } p { exit }' | head -n 6 | cut -c1-400)
|
||||
{
|
||||
echo "**Not merged** - a test build of open [PR #$PR_NUMBER]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$PR_NUMBER): $title"
|
||||
if [ -n "$summary" ]; then
|
||||
echo
|
||||
echo "$summary"
|
||||
fi
|
||||
} > notes-head.md
|
||||
cat notes-head.md
|
||||
# A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses.
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
@@ -129,27 +212,137 @@ jobs:
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
- name: Publish to DroidDeck-CI
|
||||
id: publish
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
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
|
||||
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
|
||||
@@ -62,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
|
||||
@@ -14,6 +14,7 @@ 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/
|
||||
|
||||
@@ -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.
|
||||
+35
-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 9
|
||||
versionName "0.2.0"
|
||||
versionCode 10
|
||||
versionName "0.3.0"
|
||||
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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* a game become more of these nodes instead of vanishing.
|
||||
*/
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
@@ -38,6 +39,9 @@
|
||||
#include <sys/inotify.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -50,7 +54,7 @@
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define EXPORT __attribute__((visibility("default"))) extern "C"
|
||||
#define EXPORT extern "C" __attribute__((visibility("default")))
|
||||
|
||||
// glibc declares the request as unsigned long; bionic as int. The same source also serves the
|
||||
// Linux runtime's preload (tools/linuxfs), where Steam and SDL run against glibc.
|
||||
@@ -295,21 +299,52 @@ static void controller_fork_child() {
|
||||
|
||||
namespace Logger {
|
||||
int log_enabled;
|
||||
// FAKE_EVDEV_LOG_FILE: the session's pad.log, shared by every process the library is loaded in
|
||||
// (the client, each game), so a pad report has the guest side in one file of the session folder.
|
||||
// Opened with the raw syscall: open() is this library's own hook.
|
||||
static int log_fd = -1;
|
||||
static char log_name[17];
|
||||
|
||||
void init() {
|
||||
log_enabled = getenv("FAKE_EVDEV_LOG") && atoi(getenv("FAKE_EVDEV_LOG"));
|
||||
const char *path = getenv("FAKE_EVDEV_LOG_FILE");
|
||||
if (log_enabled && path && *path)
|
||||
log_fd = static_cast<int>(syscall(SYS_openat, AT_FDCWD, path, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0644));
|
||||
if (prctl(PR_GET_NAME, log_name) != 0) strcpy(log_name, "?");
|
||||
}
|
||||
|
||||
void log(const char *message, ...) {
|
||||
if (!log_enabled)
|
||||
return;
|
||||
|
||||
if (log_fd < 0) {
|
||||
va_list args;
|
||||
va_start(args, message);
|
||||
vfprintf(stderr, message, args);
|
||||
va_end(args);
|
||||
std::cerr.flush();
|
||||
return;
|
||||
}
|
||||
// One write per line: O_APPEND keeps lines from different processes whole.
|
||||
char line[512];
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_REALTIME, &now);
|
||||
struct tm local;
|
||||
localtime_r(&now.tv_sec, &local);
|
||||
int n = snprintf(line, sizeof(line), "%02d:%02d:%02d.%03ld %d %s: ", local.tm_hour, local.tm_min,
|
||||
local.tm_sec, now.tv_nsec / 1000000, getpid(), log_name);
|
||||
va_list args;
|
||||
va_start(args, message);
|
||||
vfprintf(stderr, message, args);
|
||||
if (n > 0 && n < static_cast<int>(sizeof(line)))
|
||||
n += vsnprintf(line + n, sizeof(line) - n, message, args);
|
||||
va_end(args);
|
||||
|
||||
std::cerr.flush();
|
||||
if (n <= 0) return;
|
||||
if (n >= static_cast<int>(sizeof(line))) {
|
||||
n = sizeof(line) - 1;
|
||||
line[n - 1] = '\n';
|
||||
}
|
||||
ssize_t ignored = write(log_fd, line, n);
|
||||
(void)ignored;
|
||||
}
|
||||
} // namespace Logger
|
||||
|
||||
@@ -649,31 +684,43 @@ open_fake_input_ring(const char *event, int flags) {
|
||||
// does not exist here, so the game was left with no controller at all. With
|
||||
// FAKE_EVDEV_STEAM_VIRTUAL set the pad carries the virtual gamepad's identity for every process
|
||||
// but the client's own, which must keep seeing the pad as the physical one it reads.
|
||||
__attribute__((visibility("hidden"))) static bool presents_steam_virtual() {
|
||||
// Whether this process is the Steam client itself. Asked once: a process never becomes another.
|
||||
__attribute__((visibility("hidden"))) static bool is_steam_client() {
|
||||
static int known;
|
||||
if (known == 0) {
|
||||
const char *enabled = getenv("FAKE_EVDEV_STEAM_VIRTUAL");
|
||||
char exe[PATH_MAX];
|
||||
ssize_t length = enabled && atoi(enabled) ? readlink("/proc/self/exe", exe, sizeof(exe) - 1) : -1;
|
||||
ssize_t length = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
|
||||
bool client = false;
|
||||
if (length > 0) {
|
||||
exe[length] = '\0';
|
||||
const char *name = strrchr(exe, '/');
|
||||
client = !strncmp(name ? name + 1 : exe, "steam", 5);
|
||||
}
|
||||
known = length > 0 && !client ? 1 : -1;
|
||||
known = client ? 1 : -1;
|
||||
}
|
||||
return known == 1;
|
||||
}
|
||||
|
||||
// The virtual gamepad is an X-Box 360 pad, whose triggers are ABS_Z and ABS_RZ. Wine places a
|
||||
// gamepad's axes by their position among the ones it advertises, so the triggers have to sit
|
||||
__attribute__((visibility("hidden"))) static bool presents_steam_virtual() {
|
||||
static int known;
|
||||
if (known == 0) {
|
||||
const char *enabled = getenv("FAKE_EVDEV_STEAM_VIRTUAL");
|
||||
known = enabled && atoi(enabled) && !is_steam_client() ? 1 : -1;
|
||||
}
|
||||
return known == 1;
|
||||
}
|
||||
|
||||
// The virtual gamepad is an X-Box 360 pad, whose triggers are ABS_Z and ABS_RZ. Wine and SDL place
|
||||
// a gamepad's axes by their position among the ones it advertises, so the triggers have to sit
|
||||
// where that pad has them: as ABS_GAS and ABS_BRAKE they follow the sticks, the right stick
|
||||
// lands on the left trigger, and a released right trigger reads as the right stick held up.
|
||||
// A pad made through /dev/uinput is that pad for real, whoever reads it.
|
||||
// lands on the left trigger, and a released right trigger reads as the right stick held up. That
|
||||
// is every reader but the Steam client, which maps the pad from what it finds (controller.txt)
|
||||
// and has always been shown ABS_GAS/ABS_BRAKE: a game Steam Input is off for reads this pad
|
||||
// itself (the Xbox 360 setting), and so do the desktop's programs. A pad made through
|
||||
// /dev/uinput is that pad for real, whoever reads it.
|
||||
__attribute__((visibility("hidden"))) static bool
|
||||
presents_xbox_triggers(const FakeController &fake) {
|
||||
return fake.uinput || presents_steam_virtual();
|
||||
return fake.uinput || !is_steam_client();
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static uint16_t
|
||||
@@ -1256,6 +1303,14 @@ __attribute__((visibility("hidden"))) static bool process_is_steam_client() {
|
||||
return known == 1;
|
||||
}
|
||||
|
||||
// What the client starts with, so a pad log says whether it was offered a Deck at all.
|
||||
__attribute__((constructor)) static void log_client_start() {
|
||||
config();
|
||||
if (process_is_steam_client())
|
||||
Logger::log("loaded in the Steam client: deck %s, ring slot 0 %s\n", fake_deck_enabled() ? "on" : "off",
|
||||
get_ring_path_for_slot(0).empty() ? "(none configured)" : get_ring_path_for_slot(0).c_str());
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool is_deck_hidraw_path(const char *pathname) {
|
||||
return pathname && fake_deck_enabled() && !strcmp(pathname, DECK_HIDRAW_PATH);
|
||||
}
|
||||
@@ -1364,12 +1419,16 @@ __attribute__((visibility("hidden"))) static void *deck_report_thread(void *arg)
|
||||
__attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags) {
|
||||
static auto my_open = reinterpret_cast<int (*)(const char *, int, ...)>(dlsym(RTLD_NEXT, "open"));
|
||||
if (!process_is_steam_client()) {
|
||||
// Only the client reads the Deck; a game sees Steam Input's virtual pad instead.
|
||||
Logger::log("deck: %s refused: not the Steam client\n", DECK_HIDRAW_PATH);
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
std::string ring_path = get_ring_path_for_slot(0);
|
||||
int ring_fd = ring_path.empty() ? -1 : my_open(ring_path.c_str(), O_RDONLY | O_CLOEXEC);
|
||||
if (ring_fd < 0) {
|
||||
Logger::log("deck: %s refused: ring %s not openable (%s)\n", DECK_HIDRAW_PATH,
|
||||
ring_path.empty() ? "(none configured)" : ring_path.c_str(), strerror(errno));
|
||||
errno = ENODEV;
|
||||
return -1;
|
||||
}
|
||||
@@ -1382,12 +1441,15 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags) {
|
||||
void *mapping = mmap(nullptr, mapping_size, PROT_READ, MAP_SHARED, ring_fd, 0);
|
||||
syscall(SYS_close, ring_fd);
|
||||
if (mapping == MAP_FAILED || !ring_header_is_valid(static_cast<FakeInputRingHeader *>(mapping))) {
|
||||
Logger::log("deck: %s refused: ring %s %s\n", DECK_HIDRAW_PATH, ring_path.c_str(),
|
||||
mapping == MAP_FAILED ? "could not be mapped" : "has no valid header");
|
||||
if (mapping != MAP_FAILED) munmap(mapping, mapping_size);
|
||||
errno = ENODEV;
|
||||
return -1;
|
||||
}
|
||||
int pair[2];
|
||||
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, pair) < 0) {
|
||||
Logger::log("deck: %s refused: socketpair failed (%s)\n", DECK_HIDRAW_PATH, strerror(errno));
|
||||
munmap(mapping, mapping_size);
|
||||
return -1;
|
||||
}
|
||||
@@ -1426,8 +1488,18 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags) {
|
||||
// into /sys, so those paths are said to be sysfs; nothing else is touched.
|
||||
static constexpr long SYSFS_FS_MAGIC = 0x62656572;
|
||||
|
||||
// FAKE_DECK_SYSFS_LISTING: stand-ins for the listings of /sys, /sys/class and /sys/bus
|
||||
// (SteamDeckPad.listingDir), for a client that is not allowed to list the real ones.
|
||||
__attribute__((visibility("hidden"))) static const char *deck_listing_dir() {
|
||||
static const char *dir = getenv("FAKE_DECK_SYSFS_LISTING");
|
||||
return dir && *dir ? dir : nullptr;
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool is_deck_sysfs_path(const char *path) {
|
||||
return path && (!strncmp(path, "/sys/devices/droiddeck", 22) || !strncmp(path, "/sys/class/hidraw", 17));
|
||||
if (!path) return false;
|
||||
if (!strncmp(path, "/sys/devices/droiddeck", 22) || !strncmp(path, "/sys/class/hidraw", 17)) return true;
|
||||
const char *listing = deck_listing_dir();
|
||||
return listing && !strncmp(path, listing, strlen(listing));
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool is_deck_sysfs_fd(int fd) {
|
||||
@@ -1439,17 +1511,32 @@ __attribute__((visibility("hidden"))) static bool is_deck_sysfs_fd(int fd) {
|
||||
return is_deck_sysfs_path(path);
|
||||
}
|
||||
|
||||
// Once per process: proof the client's enumeration reached the Deck's sysfs at all. No line here
|
||||
// and no open of /dev/hidraw16 means the client never looked; a line here and no open means it
|
||||
// looked and turned the device down.
|
||||
__attribute__((visibility("hidden"))) static void note_deck_sysfs(const char *how, int result) {
|
||||
static std::atomic<bool> noted{false};
|
||||
if (!noted.exchange(true))
|
||||
Logger::log("deck: sysfs checked (%s%s)\n", how, result == 0 ? "" : ", failed");
|
||||
}
|
||||
|
||||
EXPORT int fstatfs(int fd, struct statfs *buf) {
|
||||
static auto real = reinterpret_cast<int (*)(int, struct statfs *)>(dlsym(RTLD_NEXT, "fstatfs"));
|
||||
int result = real(fd, buf);
|
||||
if (result == 0 && fake_deck_enabled() && is_deck_sysfs_fd(fd)) buf->f_type = SYSFS_FS_MAGIC;
|
||||
if (fake_deck_enabled() && is_deck_sysfs_fd(fd)) {
|
||||
note_deck_sysfs("fstatfs", result);
|
||||
if (result == 0) buf->f_type = SYSFS_FS_MAGIC;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
EXPORT int statfs(const char *path, struct statfs *buf) {
|
||||
static auto real = reinterpret_cast<int (*)(const char *, struct statfs *)>(dlsym(RTLD_NEXT, "statfs"));
|
||||
int result = real(path, buf);
|
||||
if (result == 0 && fake_deck_enabled() && is_deck_sysfs_path(path)) buf->f_type = SYSFS_FS_MAGIC;
|
||||
if (fake_deck_enabled() && is_deck_sysfs_path(path)) {
|
||||
note_deck_sysfs(path, result);
|
||||
if (result == 0) buf->f_type = SYSFS_FS_MAGIC;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1471,6 +1558,15 @@ __attribute__((visibility("hidden"))) static int copy_ioctl_string(ioctl_request
|
||||
}
|
||||
|
||||
// Feature reports carry the report id (0) first, then the Valve message: type, length, payload.
|
||||
// The client repeats some feature reports (settings, rumble) for as long as it has the pad; each
|
||||
// is logged the first time and then at 2, 4, 8... so the log shows what it asked and how often
|
||||
// without a line per report.
|
||||
__attribute__((visibility("hidden"))) static void log_deck_feature(const char *what, uint8_t feature) {
|
||||
static std::atomic<unsigned> counts[2][256];
|
||||
unsigned n = ++counts[what[0] == 's'][feature];
|
||||
if ((n & (n - 1)) == 0) Logger::log("deck: feature 0x%02x %s (x%u)\n", feature, what, n);
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static int
|
||||
deck_feature(DeckHidraw &deck, ioctl_request_t op, uint8_t *buf, bool set) {
|
||||
size_t size = _IOC_SIZE(op);
|
||||
@@ -1484,7 +1580,7 @@ deck_feature(DeckHidraw &deck, ioctl_request_t op, uint8_t *buf, bool set) {
|
||||
int left = buf[6] | buf[7] << 8, right = buf[8] | buf[9] << 8;
|
||||
send_vibration(left, right, left || right ? 1000 : 0, 0);
|
||||
}
|
||||
Logger::log("deck: feature 0x%02x set\n", buf[1]);
|
||||
log_deck_feature("set", buf[1]);
|
||||
return static_cast<int>(size);
|
||||
}
|
||||
uint8_t reply[DECK_REPORT_BYTES + 1] = {};
|
||||
@@ -1514,7 +1610,7 @@ deck_feature(DeckHidraw &deck, ioctl_request_t op, uint8_t *buf, bool set) {
|
||||
reply[1] = deck.pending_feature;
|
||||
break;
|
||||
}
|
||||
Logger::log("deck: feature 0x%02x read\n", deck.pending_feature);
|
||||
log_deck_feature("read", deck.pending_feature);
|
||||
size_t length = std::min(size, sizeof(reply));
|
||||
memcpy(buf, reply, length);
|
||||
return static_cast<int>(length);
|
||||
@@ -1558,6 +1654,155 @@ ioctl_deck(DeckHidraw &deck, ioctl_request_t op, void *argp) {
|
||||
}
|
||||
}
|
||||
|
||||
// A device scan that fails on a phone and not on the Thor: libudev gives up its whole scan on any
|
||||
// sysfs directory it cannot read for a reason other than its absence, and a phone's SELinux policy
|
||||
// denies directories a permissive device lets through. The client's /sys failures that are not
|
||||
// ENOENT are logged, each path once, so pad.log names the directory that stopped the scan.
|
||||
__attribute__((visibility("hidden"))) static std::string note_sys_failure(int dirfd, const char *pathname) {
|
||||
int saved_errno = errno;
|
||||
std::string path;
|
||||
if (saved_errno == ENOENT || !pathname || !process_is_steam_client()) {
|
||||
errno = saved_errno;
|
||||
return path;
|
||||
}
|
||||
if (pathname[0] == '/' || dirfd == AT_FDCWD) {
|
||||
path = pathname;
|
||||
} else {
|
||||
char link[64], dir[PATH_MAX];
|
||||
snprintf(link, sizeof(link), "/proc/self/fd/%d", dirfd);
|
||||
ssize_t length = readlink(link, dir, sizeof(dir) - 1);
|
||||
if (length <= 0) {
|
||||
errno = saved_errno;
|
||||
return path;
|
||||
}
|
||||
path.assign(dir, length);
|
||||
path += '/';
|
||||
path += pathname;
|
||||
}
|
||||
if (path.compare(0, 5, "/sys/") != 0 && path != "/sys") {
|
||||
errno = saved_errno;
|
||||
return {};
|
||||
}
|
||||
static std::mutex mutex;
|
||||
static std::vector<std::string> *seen = new std::vector<std::string>();
|
||||
bool first = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
if (seen->size() < 64 && std::find(seen->begin(), seen->end(), path) == seen->end()) {
|
||||
seen->push_back(path);
|
||||
first = true;
|
||||
}
|
||||
}
|
||||
if (first) Logger::log("sysfs: %s not readable (%s)\n", path.c_str(), strerror(saved_errno));
|
||||
errno = saved_errno;
|
||||
return path;
|
||||
}
|
||||
|
||||
// An enforcing SELinux policy (every retail phone; the Thor only with it forced on) refuses an app
|
||||
// a listing of /sys, /sys/class and /sys/bus. libudev lists /sys/class to find the subsystems to
|
||||
// descend into and gives up its whole scan when it cannot, so the client never reached hidraw and
|
||||
// found no Deck. The client is listed the stand-ins instead - hidraw under class - and opens
|
||||
// /sys/class/hidraw itself, which is ours and readable. Only on that refusal, only for the client.
|
||||
__attribute__((visibility("hidden"))) static std::string deck_listing_for(const char *path) {
|
||||
const char *listing = deck_listing_dir();
|
||||
if (!path || !listing || errno != EACCES || !fake_deck_enabled() || !process_is_steam_client()) return {};
|
||||
std::string p(path);
|
||||
while (p.size() > 1 && p.back() == '/') p.pop_back();
|
||||
int which = p == "/sys" ? 0 : p == "/sys/class" ? 1 : p == "/sys/bus" ? 2 : -1;
|
||||
if (which < 0) return {};
|
||||
static const char *const names[] = {"sys", "class", "bus"};
|
||||
static std::atomic<int> logged{0};
|
||||
if (!(logged.fetch_or(1 << which) & (1 << which)))
|
||||
Logger::log("sysfs: %s listed from the Deck's stand-in\n", p.c_str());
|
||||
return std::string(listing) + "/" + names[which];
|
||||
}
|
||||
|
||||
#ifdef __GLIBC__
|
||||
// sd-device's path chase now requires a mount ID, which statx only supplies from Linux 5.8.
|
||||
// Older handheld kernels still publish the real ID in fdinfo. Scope this compatibility path to
|
||||
// libudev/libsystemd in the Deck-reading Steam client, and never invent a unique mount ID.
|
||||
__attribute__((visibility("hidden"))) static bool deck_device_library(void *caller) {
|
||||
Dl_info info;
|
||||
if (!dladdr(caller, &info) || !info.dli_fname) return false;
|
||||
const char *name = strrchr(info.dli_fname, '/');
|
||||
name = name ? name + 1 : info.dli_fname;
|
||||
return !strncmp(name, "libudev.so", 10) || !strncmp(name, "libsystemd.so", 13);
|
||||
}
|
||||
|
||||
static int real_fstat(int fd, struct stat *buf);
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool deck_mount_id_from_fdinfo(
|
||||
int dirfd, const char *path, int flags, struct statx *buf) {
|
||||
static auto real_openat = reinterpret_cast<decltype(&::openat)>(dlsym(RTLD_NEXT, "openat"));
|
||||
int target;
|
||||
if (!path || !*path) {
|
||||
if (!(flags & AT_EMPTY_PATH)) return false;
|
||||
target = dirfd == AT_FDCWD ? real_openat(AT_FDCWD, ".", O_PATH | O_CLOEXEC) :
|
||||
syscall(SYS_fcntl, dirfd, F_DUPFD_CLOEXEC, 0);
|
||||
} else {
|
||||
target = real_openat(dirfd, path, O_PATH | O_CLOEXEC |
|
||||
((flags & AT_SYMLINK_NOFOLLOW) ? O_NOFOLLOW : 0));
|
||||
}
|
||||
if (target < 0) return false;
|
||||
struct stat st;
|
||||
// Pin the inode and check it still matches the successful statx before adding information.
|
||||
bool same = real_fstat(target, &st) == 0 && (buf->stx_mask & STATX_INO) &&
|
||||
st.st_ino == buf->stx_ino && major(st.st_dev) == buf->stx_dev_major &&
|
||||
minor(st.st_dev) == buf->stx_dev_minor;
|
||||
char name[64];
|
||||
snprintf(name, sizeof(name), "/proc/self/fdinfo/%d", target);
|
||||
int info = same ? real_openat(AT_FDCWD, name, O_RDONLY | O_CLOEXEC) : -1;
|
||||
char value[512];
|
||||
ssize_t length = info >= 0 ? syscall(SYS_read, info, value, sizeof(value) - 1) : -1;
|
||||
if (info >= 0) syscall(SYS_close, info);
|
||||
syscall(SYS_close, target);
|
||||
if (length < 0) return false;
|
||||
value[length] = '\0';
|
||||
for (const char *line = value; line && *line;) {
|
||||
unsigned long long id;
|
||||
if (sscanf(line, "mnt_id: %llu", &id) == 1 && id > 0) {
|
||||
buf->stx_mnt_id = id;
|
||||
buf->stx_mask |= STATX_MNT_ID;
|
||||
return true;
|
||||
}
|
||||
const char *next = strchr(line, '\n');
|
||||
line = next ? next + 1 : nullptr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
EXPORT int statx(int dirfd, const char *path, int flags, unsigned mask, struct statx *buf) {
|
||||
static auto real = reinterpret_cast<decltype(&::statx)>(dlsym(RTLD_NEXT, "statx"));
|
||||
int result = real(dirfd, path, flags, mask, buf);
|
||||
int saved_errno = errno;
|
||||
constexpr unsigned unique_mount_id = 0x4000; // STATX_MNT_ID_UNIQUE, newer than build headers.
|
||||
if (result == 0 && (mask & STATX_MNT_ID) &&
|
||||
!(buf->stx_mask & (STATX_MNT_ID | unique_mount_id)) && fake_deck_enabled() &&
|
||||
process_is_steam_client() && deck_device_library(__builtin_return_address(0))) {
|
||||
bool recovered = deck_mount_id_from_fdinfo(dirfd, path, flags, buf);
|
||||
static std::atomic<bool> noted_success{false}, noted_failure{false};
|
||||
if (!(recovered ? noted_success : noted_failure).exchange(true))
|
||||
Logger::log("deck discovery: statx mount id %s for %s (returned mask=0x%x id=%llu)\n",
|
||||
recovered ? "recovered from fdinfo" : "unavailable in fdinfo",
|
||||
path && *path ? path : "(fd)", buf->stx_mask,
|
||||
static_cast<unsigned long long>(buf->stx_mnt_id));
|
||||
}
|
||||
errno = saved_errno;
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
EXPORT DIR *opendir(const char *name) {
|
||||
static auto real = reinterpret_cast<DIR *(*)(const char *)>(dlsym(RTLD_NEXT, "opendir"));
|
||||
DIR *dir = real(name);
|
||||
if (!dir) {
|
||||
note_sys_failure(AT_FDCWD, name);
|
||||
std::string listing = deck_listing_for(name);
|
||||
if (!listing.empty()) dir = real(listing.c_str());
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
EXPORT int open(const char *pathname, int flags, ...) {
|
||||
va_list va;
|
||||
mode_t mode;
|
||||
@@ -1621,6 +1866,10 @@ EXPORT int open(const char *pathname, int flags, ...) {
|
||||
fd = my_open(pathname, flags, mode);
|
||||
else
|
||||
fd = my_open(pathname, flags);
|
||||
if (fd < 0) {
|
||||
std::string listing = deck_listing_for(note_sys_failure(AT_FDCWD, pathname).c_str());
|
||||
if (!listing.empty()) fd = my_open(listing.c_str(), flags);
|
||||
}
|
||||
|
||||
if (fake_path)
|
||||
free(fake_path);
|
||||
@@ -1691,6 +1940,10 @@ EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
|
||||
fd = my_openat(dirfd, pathname, flags, mode);
|
||||
else
|
||||
fd = my_openat(dirfd, pathname, flags);
|
||||
if (fd < 0) {
|
||||
std::string listing = deck_listing_for(note_sys_failure(dirfd, pathname).c_str());
|
||||
if (!listing.empty()) fd = my_openat(AT_FDCWD, listing.c_str(), flags);
|
||||
}
|
||||
|
||||
if (fake_path)
|
||||
free(fake_path);
|
||||
@@ -1698,9 +1951,8 @@ EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
|
||||
return fd;
|
||||
}
|
||||
|
||||
EXPORT int stat(const char *pathname, struct stat *statbuf) {
|
||||
static auto my_stat = reinterpret_cast<decltype(&::stat)>(dlsym(RTLD_NEXT, "stat"));
|
||||
|
||||
template <typename S, typename Real>
|
||||
static int fake_stat_path(const char *pathname, S *statbuf, Real real) {
|
||||
if (is_withdrawn_pad_path(pathname) || (is_deck_hidraw_path(pathname) && !process_is_steam_client())) {
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
@@ -1736,7 +1988,7 @@ EXPORT int stat(const char *pathname, struct stat *statbuf) {
|
||||
}
|
||||
}
|
||||
|
||||
int ret = my_stat(pathname, statbuf);
|
||||
int ret = real(pathname, statbuf);
|
||||
|
||||
if (ret == 0 && event && get_event_number(event) >= 0) {
|
||||
statbuf->st_mode = (statbuf->st_mode & ~S_IFMT) | S_IFCHR;
|
||||
@@ -1749,10 +2001,9 @@ EXPORT int stat(const char *pathname, struct stat *statbuf) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
EXPORT int fstat(int fd, struct stat *buf) {
|
||||
static auto my_fstat = reinterpret_cast<decltype(&::fstat)>(dlsym(RTLD_NEXT, "fstat"));
|
||||
|
||||
int ret = my_fstat(fd, buf);
|
||||
template <typename S, typename Real>
|
||||
static int fake_stat_fd(int fd, S *buf, Real real) {
|
||||
int ret = real(fd, buf);
|
||||
|
||||
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
|
||||
if (ret == 0 && deck_map().count(fd)) {
|
||||
@@ -1769,6 +2020,36 @@ EXPORT int fstat(int fd, struct stat *buf) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int real_stat(const char *pathname, struct stat *statbuf) {
|
||||
static auto fn = reinterpret_cast<int (*)(const char *, struct stat *)>(dlsym(RTLD_NEXT, "stat"));
|
||||
if (fn) return fn(pathname, statbuf);
|
||||
#ifdef _STAT_VER
|
||||
static auto xfn = reinterpret_cast<int (*)(int, const char *, struct stat *)>(dlsym(RTLD_NEXT, "__xstat"));
|
||||
if (xfn) return xfn(_STAT_VER, pathname, statbuf);
|
||||
#endif
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int real_fstat(int fd, struct stat *buf) {
|
||||
static auto fn = reinterpret_cast<int (*)(int, struct stat *)>(dlsym(RTLD_NEXT, "fstat"));
|
||||
if (fn) return fn(fd, buf);
|
||||
#ifdef _STAT_VER
|
||||
static auto xfn = reinterpret_cast<int (*)(int, int, struct stat *)>(dlsym(RTLD_NEXT, "__fxstat"));
|
||||
if (xfn) return xfn(_STAT_VER, fd, buf);
|
||||
#endif
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
EXPORT int stat(const char *pathname, struct stat *statbuf) {
|
||||
return fake_stat_path(pathname, statbuf, real_stat);
|
||||
}
|
||||
|
||||
EXPORT int fstat(int fd, struct stat *buf) {
|
||||
return fake_stat_fd(fd, buf, real_fstat);
|
||||
}
|
||||
|
||||
EXPORT int access(const char *pathname, int mode) {
|
||||
static auto my_access = reinterpret_cast<decltype(&::access)>(dlsym(RTLD_NEXT, "access"));
|
||||
if (fake_uinput_enabled() && is_uinput_path(pathname)) return 0;
|
||||
@@ -2614,31 +2895,57 @@ EXPORT int openat64(int dirfd, const char *pathname, int flags, ...) {
|
||||
return openat(dirfd, pathname, flags, mode);
|
||||
}
|
||||
|
||||
static int real_stat64(const char *pathname, struct stat64 *statbuf) {
|
||||
static auto fn = reinterpret_cast<int (*)(const char *, struct stat64 *)>(dlsym(RTLD_NEXT, "stat64"));
|
||||
if (fn) return fn(pathname, statbuf);
|
||||
#ifdef _STAT_VER
|
||||
static auto xfn = reinterpret_cast<int (*)(int, const char *, struct stat64 *)>(dlsym(RTLD_NEXT, "__xstat64"));
|
||||
if (xfn) return xfn(_STAT_VER, pathname, statbuf);
|
||||
#endif
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int real_fstat64(int fd, struct stat64 *buf) {
|
||||
static auto fn = reinterpret_cast<int (*)(int, struct stat64 *)>(dlsym(RTLD_NEXT, "fstat64"));
|
||||
if (fn) return fn(fd, buf);
|
||||
#ifdef _STAT_VER
|
||||
static auto xfn = reinterpret_cast<int (*)(int, int, struct stat64 *)>(dlsym(RTLD_NEXT, "__fxstat64"));
|
||||
if (xfn) return xfn(_STAT_VER, fd, buf);
|
||||
#endif
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
EXPORT int stat64(const char *pathname, struct stat64 *statbuf) {
|
||||
return stat(pathname, reinterpret_cast<struct stat *>(statbuf));
|
||||
return fake_stat_path(pathname, statbuf, real_stat64);
|
||||
}
|
||||
|
||||
EXPORT int fstat64(int fd, struct stat64 *buf) {
|
||||
return fstat(fd, reinterpret_cast<struct stat *>(buf));
|
||||
return fake_stat_fd(fd, buf, real_fstat64);
|
||||
}
|
||||
|
||||
EXPORT int __xstat(int version, const char *pathname, struct stat *statbuf) {
|
||||
(void)version;
|
||||
return stat(pathname, statbuf);
|
||||
static auto real = reinterpret_cast<int (*)(int, const char *, struct stat *)>(dlsym(RTLD_NEXT, "__xstat"));
|
||||
if (!real) return stat(pathname, statbuf);
|
||||
return fake_stat_path(pathname, statbuf, [version](const char *path, struct stat *buf) { return real(version, path, buf); });
|
||||
}
|
||||
|
||||
EXPORT int __xstat64(int version, const char *pathname, struct stat64 *statbuf) {
|
||||
(void)version;
|
||||
return stat(pathname, reinterpret_cast<struct stat *>(statbuf));
|
||||
static auto real = reinterpret_cast<int (*)(int, const char *, struct stat64 *)>(dlsym(RTLD_NEXT, "__xstat64"));
|
||||
if (!real) return stat64(pathname, statbuf);
|
||||
return fake_stat_path(pathname, statbuf, [version](const char *path, struct stat64 *buf) { return real(version, path, buf); });
|
||||
}
|
||||
|
||||
EXPORT int __fxstat(int version, int fd, struct stat *buf) {
|
||||
(void)version;
|
||||
return fstat(fd, buf);
|
||||
static auto real = reinterpret_cast<int (*)(int, int, struct stat *)>(dlsym(RTLD_NEXT, "__fxstat"));
|
||||
if (!real) return fstat(fd, buf);
|
||||
return fake_stat_fd(fd, buf, [version](int f, struct stat *b) { return real(version, f, b); });
|
||||
}
|
||||
|
||||
EXPORT int __fxstat64(int version, int fd, struct stat64 *buf) {
|
||||
(void)version;
|
||||
return fstat(fd, reinterpret_cast<struct stat *>(buf));
|
||||
static auto real = reinterpret_cast<int (*)(int, int, struct stat64 *)>(dlsym(RTLD_NEXT, "__fxstat64"));
|
||||
if (!real) return fstat64(fd, buf);
|
||||
return fake_stat_fd(fd, buf, [version](int f, struct stat64 *b) { return real(version, f, b); });
|
||||
}
|
||||
#endif
|
||||
@@ -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,7 +12,7 @@ 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
|
||||
@@ -36,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
|
||||
@@ -54,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);
|
||||
@@ -2,7 +2,7 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "blend_pass.h"
|
||||
#include "vk_loader.h"
|
||||
#include "vk_present.h" /* banner_log, vkp_gpu_name */
|
||||
#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)
|
||||
@@ -168,12 +168,12 @@ static int objects_ensure(void) {
|
||||
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;
|
||||
banner_log("gpu", "alpha composition ready on %s: translucent surfaces are blended over what is under them",
|
||||
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();
|
||||
banner_log("error", "gpu: the alpha composition pass could not be built on this driver; translucent surfaces "
|
||||
droiddeck_log("error", "gpu: the alpha composition pass could not be built on this driver; translucent surfaces "
|
||||
"are drawn opaque this session");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* bannerlator-wayland - the embedded compositor of the Wayland display path.
|
||||
* droiddeck-wayland - the embedded compositor of the Wayland display path.
|
||||
*
|
||||
* Clients are the Wine processes of a container (winewayland.drv) and, through them,
|
||||
* Mesa's Vulkan WSI. Each process is its own client, so a Windows virtual desktop is
|
||||
@@ -50,12 +50,12 @@
|
||||
#include "framegen_bridge.h"
|
||||
#include <pthread.h>
|
||||
#include "effects_chain.h"
|
||||
#include "banner_ext.h"
|
||||
#include "banner_color.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "droiddeck_color.h"
|
||||
#include "compositor_internal.h"
|
||||
|
||||
#define WLOGI(...) __android_log_print(ANDROID_LOG_INFO, "BannerWayland", __VA_ARGS__)
|
||||
#define WLOGE(...) __android_log_print(ANDROID_LOG_ERROR, "BannerWayland", __VA_ARGS__)
|
||||
#define WLOGI(...) __android_log_print(ANDROID_LOG_INFO, "DroidDeckWayland", __VA_ARGS__)
|
||||
#define WLOGE(...) __android_log_print(ANDROID_LOG_ERROR, "DroidDeckWayland", __VA_ARGS__)
|
||||
|
||||
/* ------------------------------------------------------------------ session log
|
||||
* One readable, time-stamped file per Wayland session in Download/Wayland-logs, also
|
||||
@@ -75,7 +75,7 @@ static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static char *g_prelog[PRELOG_MAX];
|
||||
static int g_prelog_n;
|
||||
|
||||
void banner_log(const char *tag, const char *fmt, ...) {
|
||||
void droiddeck_log(const char *tag, const char *fmt, ...) {
|
||||
char msg[512];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
@@ -121,11 +121,11 @@ static void open_session_log(void) {
|
||||
setvbuf(f, NULL, _IOLBF, 0);
|
||||
strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", &tm);
|
||||
fprintf(f,
|
||||
"Bannerlator Wayland session\n"
|
||||
"===========================\n"
|
||||
"DroidDeck Wayland session\n"
|
||||
"=========================\n"
|
||||
"Started %s\n"
|
||||
"Display Wayland: Windows programs draw through the Bannerlator compositor.\n"
|
||||
" No X server is used for this session.\n"
|
||||
"Display Wayland: programs draw through the DroidDeck compositor.\n"
|
||||
" gamescope and the desktop start their own Xwayland for X11 programs.\n"
|
||||
"Log %s\n\n"
|
||||
"Time Area Event\n"
|
||||
"------------ -------- -----------------------------------------------------\n",
|
||||
@@ -178,8 +178,8 @@ static const char *client_name(struct wl_client *client) {
|
||||
|
||||
static void on_client_destroyed(struct wl_listener *l, void *data) {
|
||||
struct client_info *ci = wl_container_of(l, ci, destroy), **pp;
|
||||
banner_log("program", "disconnected: %s (pid %d)", ci->name, (int)ci->pid);
|
||||
banner_color_client_gone(ci->client); /* HDR summary for a program that presented HDR (gate open only) */
|
||||
droiddeck_log("program", "disconnected: %s (pid %d)", ci->name, (int)ci->pid);
|
||||
droiddeck_color_client_gone(ci->client); /* HDR summary for a program that presented HDR (gate open only) */
|
||||
for (pp = &g_clients; *pp; pp = &(*pp)->next)
|
||||
if (*pp == ci) { *pp = ci->next; break; }
|
||||
free(ci);
|
||||
@@ -211,7 +211,7 @@ static void on_client_created(struct wl_listener *l, void *data) {
|
||||
wl_client_add_destroy_listener(client, &ci->destroy);
|
||||
ci->next = g_clients;
|
||||
g_clients = ci;
|
||||
banner_log("program", "connected over Wayland: %s (pid %d)", ci->name, (int)ci->pid);
|
||||
droiddeck_log("program", "connected over Wayland: %s (pid %d)", ci->name, (int)ci->pid);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ surfaces */
|
||||
@@ -304,7 +304,7 @@ volatile int g_fps_limit;
|
||||
* the X11 renderer's unviewable "explorer.exe". They still exist for input routing. */
|
||||
volatile int g_hide_shell;
|
||||
|
||||
/* Experimental layer mode (BANNER_WAYLAND_ZERO_COPY=1 in the container's environment): a single
|
||||
/* Experimental layer mode (DROIDDECK_WAYLAND_ZERO_COPY=1 in the container's environment): a single
|
||||
* fullscreen window is shown on its own Android layer (sc_layer.c) instead of being blitted into the
|
||||
* screen swapchain. Set from the app before the compositor starts; read per frame. */
|
||||
volatile int g_zero_copy;
|
||||
@@ -314,11 +314,11 @@ static int g_zero_copy_fx_skip_said;
|
||||
/* Compressed (UBWC) game buffers: zwp_linux_dmabuf_v1 advertises DRM_FORMAT_MOD_QCOM_COMPRESSED next
|
||||
* to LINEAR for every format the renderer's own driver can import that way, so Turnip's Wayland WSI
|
||||
* in the game allocates UBWC swapchain images instead of resolving each frame to a linear copy.
|
||||
* BANNER_WAYLAND_UBWC=0 in the container's environment turns the advertisement off (A/B switch). Set
|
||||
* DROIDDECK_WAYLAND_UBWC=0 in the container's environment turns the advertisement off (A/B switch). Set
|
||||
* from the app before the compositor starts; read at every zwp_linux_dmabuf_v1 bind. */
|
||||
volatile int g_ubwc = 1;
|
||||
|
||||
/* Debug (BANNER_WAYLAND_NO_RENDER_NODE=1): name no DRM device in the dma-buf feedback, as a phone
|
||||
/* Debug (DROIDDECK_WAYLAND_NO_RENDER_NODE=1): name no DRM device in the dma-buf feedback, as a phone
|
||||
* that exposes no /dev/dri node to apps does, to reproduce its OpenGL path here. Set from the app
|
||||
* before the compositor starts. */
|
||||
volatile int g_no_render_node;
|
||||
@@ -541,9 +541,16 @@ static void cursor_publish_shm(struct wl_shm_buffer *shm, int hx, int hy) {
|
||||
wl_shm_buffer_end_access(shm);
|
||||
}
|
||||
|
||||
/* The serial alone, without the lock: the app asks on every pointer move, and the snapshot below
|
||||
* copies up to 256 KB of pixels across JNI and holds the lock the compositor thread takes to
|
||||
* change the cursor. A torn read is only a missed or an extra snapshot. */
|
||||
int droiddeck_cursor_serial(void) {
|
||||
return __atomic_load_n(&g_cursor_serial, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
/* Read by the app (UI thread). out = [serial, hidden, w, h, hotspotX, hotspotY, pixels...].
|
||||
* Returns the number of ints written, or 0 if out is too small. */
|
||||
int banner_cursor_snapshot(int *out, int cap) {
|
||||
int droiddeck_cursor_snapshot(int *out, int cap) {
|
||||
int n = 0;
|
||||
pthread_mutex_lock(&g_cursor_lock);
|
||||
int need = 6 + (g_cursor_hidden ? 0 : g_cursor_w * g_cursor_h);
|
||||
@@ -643,7 +650,7 @@ static void map_toplevel(struct surface *s) {
|
||||
{
|
||||
char name[160];
|
||||
describe(s, name, sizeof(name));
|
||||
banner_log("window", "opened %s %dx%d at %d,%d%s", name, w, h, s->x, s->y,
|
||||
droiddeck_log("window", "opened %s %dx%d at %d,%d%s", name, w, h, s->x, s->y,
|
||||
s->placed ? "" : " (no desktop position yet)");
|
||||
}
|
||||
wl_list_insert(g_toplevels.prev, &s->toplevel_link); /* new windows start on top */
|
||||
@@ -660,12 +667,12 @@ static void unmap_toplevel(struct surface *s) {
|
||||
{
|
||||
char name[160];
|
||||
describe(s, name, sizeof(name));
|
||||
banner_log("window", "closed %s", name);
|
||||
droiddeck_log("window", "closed %s", name);
|
||||
}
|
||||
wl_list_remove(&s->toplevel_link);
|
||||
wl_list_init(&s->toplevel_link);
|
||||
if (g_ime_click == s) g_ime_click = NULL;
|
||||
banner_text_input_refocus();
|
||||
droiddeck_text_input_refocus();
|
||||
}
|
||||
|
||||
/* Let go of the surface's dmabuf content. paced = 1: the buffer was replaced, give it back on the
|
||||
@@ -743,11 +750,13 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res
|
||||
static struct surface *g_hud_surface;
|
||||
/* When the HUD's window last presented a frame (now_ns), to let a replacement take over. */
|
||||
static int64_t g_hud_last_ns;
|
||||
extern void banner_on_game_surface(const char *window, const char *gpu); /* window NULL = gone */
|
||||
extern void banner_on_game_frame(void);
|
||||
/* That window committed a frame the screen has not drawn yet. */
|
||||
static int g_hud_fresh;
|
||||
extern void droiddeck_on_game_surface(const char *window, const char *gpu); /* window NULL = gone */
|
||||
extern void droiddeck_on_game_frame(void);
|
||||
/* The program behind that window: its Linux pid (the Wayland client's credentials) and executable name
|
||||
* ("" when /proc gave none) - the app arms its CPU affinity on it (X11 does that from window events). */
|
||||
extern void banner_on_game_program(int pid, const char *program);
|
||||
extern void droiddeck_on_game_program(int pid, const char *program);
|
||||
|
||||
static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_resource *buffer) {
|
||||
if (s->dmabuf != buffer || s->dmabuf_buf != b) {
|
||||
@@ -778,7 +787,7 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
|
||||
(s->buf_w != b->width || s->buf_h != b->height)) {
|
||||
char name[160];
|
||||
describe(s, name, sizeof(name));
|
||||
banner_log("vulkan", "%s changed its frame size: %dx%d -> %dx%d", name,
|
||||
droiddeck_log("vulkan", "%s changed its frame size: %dx%d -> %dx%d", name,
|
||||
s->buf_w, s->buf_h, b->width, b->height);
|
||||
}
|
||||
s->buf_w = b->width;
|
||||
@@ -794,18 +803,18 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
|
||||
if (b->img)
|
||||
/* Imported for the copy path. Whether its frames ALSO go on the display layer without a copy
|
||||
* is decided per frame (fullscreen, zero-copy on) and counted in the 10 s lines. */
|
||||
banner_log("vulkan", "%s is presenting GPU frames through Wayland: %dx%d, format %c%c%c%c, %s (%s)",
|
||||
droiddeck_log("vulkan", "%s is presenting GPU frames through Wayland: %dx%d, format %c%c%c%c, %s (%s)",
|
||||
name, b->width, b->height, b->format & 0xff, (b->format >> 8) & 0xff,
|
||||
(b->format >> 16) & 0xff, (b->format >> 24) & 0xff,
|
||||
vkp_modifier_name(b->modifier),
|
||||
ahb_swapchain_has_ahb(b) ? "gralloc buffers: can go on the display layer without a copy"
|
||||
: "dma-buf: copied into the screen swapchain");
|
||||
else if (ahb_swapchain_has_ahb(b))
|
||||
banner_log("vulkan", "%s is presenting GPU frames through Wayland: %dx%d on its own display layer only "
|
||||
droiddeck_log("vulkan", "%s is presenting GPU frames through Wayland: %dx%d on its own display layer only "
|
||||
"(gralloc buffers the compositor cannot import for the copy path)",
|
||||
name, b->width, b->height);
|
||||
else
|
||||
banner_log("error", "could not import GPU frames from %s (%dx%d, modifier %#llx)",
|
||||
droiddeck_log("error", "could not import GPU frames from %s (%dx%d, modifier %#llx)",
|
||||
name, b->width, b->height, (unsigned long long)b->modifier);
|
||||
}
|
||||
/* The HUD follows the window whether its frames are copied or go straight to the layer: a
|
||||
@@ -821,21 +830,21 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
|
||||
char name[160];
|
||||
describe(s, name, sizeof(name));
|
||||
g_hud_surface = s;
|
||||
banner_on_game_surface(name, vkp_gpu_name());
|
||||
droiddeck_on_game_surface(name, vkp_gpu_name());
|
||||
struct client_info *ci = client_info_of(wl_resource_get_client(s->resource));
|
||||
if (ci) banner_on_game_program((int)ci->pid, strncmp(ci->name, "pid ", 4) ? ci->name : "");
|
||||
if (ci) droiddeck_on_game_program((int)ci->pid, strncmp(ci->name, "pid ", 4) ? ci->name : "");
|
||||
}
|
||||
/* HDR session (gate open): a DXVK game switches to HDR by REBUILDING its swapchain on the same
|
||||
* surface, so the one-shot line above never sees the 10-bit buffers - name every format change. */
|
||||
if (b->format != s->hdr_fmt_logged && banner_color_hdr_open()) {
|
||||
if (b->format != s->hdr_fmt_logged && droiddeck_color_hdr_open()) {
|
||||
char name[160];
|
||||
uint32_t af = ahb_swapchain_ahb_format(b);
|
||||
int ten = b->format == FOURCC('A', 'B', '3', '0') || b->format == FOURCC('X', 'B', '3', '0');
|
||||
describe(s, name, sizeof(name));
|
||||
banner_log("color", "%s presents %dx%d buffers in %c%c%c%c (%s), %s%s%s; %s", name, b->width, b->height,
|
||||
droiddeck_log("color", "%s presents %dx%d buffers in %c%c%c%c (%s), %s%s%s; %s", name, b->width, b->height,
|
||||
b->format & 0xff, (b->format >> 8) & 0xff, (b->format >> 16) & 0xff, (b->format >> 24) & 0xff,
|
||||
ten ? "10-bit A2B10G10R10" : "8-bit", vkp_modifier_name(b->modifier),
|
||||
af ? ", gralloc " : ", no gralloc buffer (copy path only)", af ? banner_ahb_format_name(af) : "",
|
||||
af ? ", gralloc " : ", no gralloc buffer (copy path only)", af ? droiddeck_ahb_format_name(af) : "",
|
||||
b->img ? "the compositor imported it"
|
||||
: af ? "the compositor's driver could NOT import it (display layer only, fullscreen)"
|
||||
: "the compositor's driver could NOT import it (nothing can show it)");
|
||||
@@ -845,23 +854,23 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
|
||||
* Asking whether the compositor could import it, as this used to, is a question about the
|
||||
* copy path and not about whether a frame happened: a game whose buffers go straight to
|
||||
* the display layer draws on screen while the counter sat at 0.0 fps and 1000.0 ms. */
|
||||
if (s == g_hud_surface) { g_hud_last_ns = now_ns(); banner_on_game_frame(); }
|
||||
if (s == g_hud_surface) { g_hud_last_ns = now_ns(); g_hud_fresh = 1; droiddeck_on_game_frame(); }
|
||||
}
|
||||
|
||||
/* ---- hooks for ahb_swapchain.c (zero-copy layers) */
|
||||
struct dmabuf_buffer *banner_dmabuf_from_resource(struct wl_resource *buffer) { return get_dmabuf(buffer); }
|
||||
int banner_dmabuf_fd(const struct dmabuf_buffer *b) { return b && b->n_planes > 0 ? b->fd[0] : -1; }
|
||||
void banner_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h) { *w = b->width; *h = b->height; }
|
||||
void **banner_dmabuf_ahb_slot(struct dmabuf_buffer *b) { return &b->ahb_state; }
|
||||
void banner_dmabuf_ref(struct dmabuf_buffer *b) { b->refs++; }
|
||||
void banner_dmabuf_unref(struct dmabuf_buffer *b) { dmabuf_buffer_unref(b); }
|
||||
void banner_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns) {
|
||||
struct dmabuf_buffer *droiddeck_dmabuf_from_resource(struct wl_resource *buffer) { return get_dmabuf(buffer); }
|
||||
int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b) { return b && b->n_planes > 0 ? b->fd[0] : -1; }
|
||||
void droiddeck_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h) { *w = b->width; *h = b->height; }
|
||||
void **droiddeck_dmabuf_ahb_slot(struct dmabuf_buffer *b) { return &b->ahb_state; }
|
||||
void droiddeck_dmabuf_ref(struct dmabuf_buffer *b) { b->refs++; }
|
||||
void droiddeck_dmabuf_unref(struct dmabuf_buffer *b) { dmabuf_buffer_unref(b); }
|
||||
void droiddeck_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns) {
|
||||
if (paced && s) release_buffer(s, buffer, since_ns);
|
||||
else { wl_buffer_send_release(buffer); perf_note_release(since_ns); }
|
||||
}
|
||||
void banner_surface_describe(const struct surface *s, char *out, size_t size) { describe(s, out, size); }
|
||||
const struct banner_color *banner_surface_color(const struct surface *s) {
|
||||
return s ? banner_color_of(s->resource) : NULL;
|
||||
void droiddeck_surface_describe(const struct surface *s, char *out, size_t size) { describe(s, out, size); }
|
||||
const struct droiddeck_color *droiddeck_surface_color(const struct surface *s) {
|
||||
return s ? droiddeck_color_of(s->resource) : NULL;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ wl_surface */
|
||||
@@ -1125,7 +1134,7 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
wl_list_insert_list(s->feedback.prev, &s->pending_feedback);
|
||||
wl_list_init(&s->pending_feedback);
|
||||
constraints_surface_commit(s);
|
||||
banner_color_commit(s->resource); /* wp_color_management_surface_v1 state (returns at once when HDR is off) */
|
||||
droiddeck_color_commit(s->resource); /* wp_color_management_surface_v1 state (returns at once when HDR is off) */
|
||||
|
||||
if (s->role == ROLE_TOPLEVEL && s->xdg_toplevel) {
|
||||
if (s->has_content) map_toplevel(s);
|
||||
@@ -1175,9 +1184,9 @@ static void surface_resource_destroy(struct wl_resource *r) {
|
||||
if (g_grab == s) g_grab = NULL;
|
||||
if (g_key_target == s) g_key_target = NULL;
|
||||
if (g_ime_click == s) g_ime_click = NULL;
|
||||
banner_text_input_surface_gone(r);
|
||||
if (g_desktop == s) { g_desktop = NULL; banner_log("desktop", "the desktop closed"); }
|
||||
if (g_hud_surface == s) { g_hud_surface = NULL; banner_on_game_surface(NULL, NULL); }
|
||||
droiddeck_text_input_surface_gone(r);
|
||||
if (g_desktop == s) { g_desktop = NULL; droiddeck_log("desktop", "the desktop closed"); }
|
||||
if (g_hud_surface == s) { g_hud_surface = NULL; droiddeck_on_game_surface(NULL, NULL); }
|
||||
constraints_surface_gone(s);
|
||||
|
||||
unmap_toplevel(s);
|
||||
@@ -1427,7 +1436,7 @@ static void xdg_toplevel_set_title(struct wl_client *c, struct wl_resource *r, c
|
||||
if (announce) {
|
||||
char after[160];
|
||||
describe(s, after, sizeof(after));
|
||||
banner_log("window", "renamed %s -> %s", before, after);
|
||||
droiddeck_log("window", "renamed %s -> %s", before, after);
|
||||
}
|
||||
}
|
||||
static void xdg_toplevel_set_app_id(struct wl_client *c, struct wl_resource *r, const char *id) {}
|
||||
@@ -1604,7 +1613,7 @@ static void desktop_set_desktop(struct wl_client *c, struct wl_resource *r, stru
|
||||
if (!s || (s->role != ROLE_NONE && s->role != ROLE_DESKTOP)) return;
|
||||
s->role = ROLE_DESKTOP;
|
||||
g_desktop = s;
|
||||
banner_log("desktop", "Windows virtual desktop created by %s", client_name(c));
|
||||
droiddeck_log("desktop", "Windows virtual desktop created by %s", client_name(c));
|
||||
schedule_render();
|
||||
}
|
||||
static void desktop_set_window(struct wl_client *c, struct wl_resource *r, struct wl_resource *surface,
|
||||
@@ -1614,7 +1623,7 @@ static void desktop_set_window(struct wl_client *c, struct wl_resource *r, struc
|
||||
if (s->placed && (s->x != x || s->y != y) && s->mapped && log_budget()) {
|
||||
char name[160];
|
||||
describe(s, name, sizeof(name));
|
||||
banner_log("window", "moved %s to %d,%d", name, x, y);
|
||||
droiddeck_log("window", "moved %s to %d,%d", name, x, y);
|
||||
}
|
||||
s->hwnd = hwnd;
|
||||
s->x = x;
|
||||
@@ -1657,13 +1666,13 @@ static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t
|
||||
if (!r) { wl_client_post_no_memory(c); return; }
|
||||
wl_resource_set_implementation(r, &output_impl, NULL, NULL);
|
||||
wl_output_send_geometry(r, 0, 0, 340, 190, WL_OUTPUT_SUBPIXEL_UNKNOWN,
|
||||
"Bannerlator", "Wayland", WL_OUTPUT_TRANSFORM_NORMAL);
|
||||
"DroidDeck", "Wayland", WL_OUTPUT_TRANSFORM_NORMAL);
|
||||
wl_output_send_mode(r, WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED,
|
||||
g_output_w > 0 ? g_output_w : 1920, g_output_h > 0 ? g_output_h : 1080,
|
||||
g_output_refresh_mhz > 0 ? g_output_refresh_mhz : 60000);
|
||||
if (ver >= WL_OUTPUT_NAME_SINCE_VERSION) {
|
||||
wl_output_send_name(r, "Bannerlator-1");
|
||||
wl_output_send_description(r, "Bannerlator display");
|
||||
wl_output_send_name(r, "DroidDeck-1");
|
||||
wl_output_send_description(r, "DroidDeck display");
|
||||
}
|
||||
if (ver >= 2) {
|
||||
wl_output_send_scale(r, 1);
|
||||
@@ -1692,7 +1701,7 @@ static int g_hdr_unimported_rect[4], g_hdr_unimported_below; /* scene x,y,w,h; d
|
||||
|
||||
static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, int ox, int oy) {
|
||||
if (!s->dmabuf_buf || s->dmabuf_buf->img || !ahb_swapchain_has_ahb(s->dmabuf_buf)) return;
|
||||
const struct banner_color *c = banner_color_of(s->resource);
|
||||
const struct droiddeck_color *c = droiddeck_color_of(s->resource);
|
||||
if (!c || !c->dataspace) return;
|
||||
if (s->src_set && (s->src[0] != 0 || s->src[1] != 0 || (int)(s->src[2] + 0.5f) != s->buf_w ||
|
||||
(int)(s->src[3] + 0.5f) != s->buf_h)) return; /* cropped: the layer shows whole buffers */
|
||||
@@ -1721,7 +1730,7 @@ static void add_surface(struct draw_list *dl, struct surface *s, int ox, int oy)
|
||||
int dw, dh;
|
||||
|
||||
if (!img) {
|
||||
if (g_zero_copy && banner_color_hdr_open()) note_hdr_unimported(dl, s, ox, oy);
|
||||
if (g_zero_copy && droiddeck_color_hdr_open()) note_hdr_unimported(dl, s, ox, oy);
|
||||
return;
|
||||
}
|
||||
if (s->src_set) { sx = s->src[0]; sy = s->src[1]; sw = s->src[2]; sh = s->src[3]; }
|
||||
@@ -1841,7 +1850,7 @@ static struct surface *ahb_layer_only_candidate(int scene_w, int scene_h) {
|
||||
return s;
|
||||
}
|
||||
|
||||
/* ---- HDR (banner_color.h) in the scene
|
||||
/* ---- HDR (droiddeck_color.h) in the scene
|
||||
* An HDR frame is only right on the game's own display layer: everything the compositor draws itself -
|
||||
* the scene blit, the effects chain, frame generation, its swapchain - is 8-bit sRGB, and a PQ-encoded
|
||||
* frame pushed through it as it stands comes out washed out. So a scene with HDR in it takes one of
|
||||
@@ -1857,7 +1866,7 @@ static struct surface *ahb_layer_only_candidate(int scene_w, int scene_h) {
|
||||
* tone-mapped to SDR (correct colours instead of washed out).
|
||||
* 3 a frame the compositor could not import: only its display layer can show it, so effects and
|
||||
* frame generation are skipped for it (said in the log).
|
||||
* The drawer's HDR output switch (banner_color_output) OFF: the same routes, tone-mapped - route 0
|
||||
* The drawer's HDR output switch (droiddeck_color_output) OFF: the same routes, tone-mapped - route 0
|
||||
* becomes route 1 (the whole scene composed, tone-mapped to sRGB, onto the game layer UNtagged), route 2
|
||||
* tone-maps into an SDR swapchain, route 3 cannot (nothing can read those frames) and stays HDR, said
|
||||
* in the log. The game is told nothing; it keeps rendering HDR.
|
||||
@@ -1867,7 +1876,7 @@ struct hdr_scene {
|
||||
struct vkp_hdr_frame hf;
|
||||
unsigned char *is_hdr; /* per draw (calloc'd; free after the frame) */
|
||||
int n_hdr;
|
||||
const struct banner_color *color; /* the topmost HDR draw's description: what the picture is tagged */
|
||||
const struct droiddeck_color *color; /* the topmost HDR draw's description: what the picture is tagged */
|
||||
struct surface *who; /* its surface, for the log */
|
||||
const char *why; /* route 1: why the game cannot be alone on its layer */
|
||||
};
|
||||
@@ -1882,29 +1891,29 @@ static struct surface *hdr_unimported_fullscreen(const struct draw_list *dl, int
|
||||
}
|
||||
|
||||
static int is_hdr_surface(struct surface *s) {
|
||||
const struct banner_color *c = s ? banner_color_of(s->resource) : NULL;
|
||||
const struct droiddeck_color *c = s ? droiddeck_color_of(s->resource) : NULL;
|
||||
return c && c->dataspace;
|
||||
}
|
||||
|
||||
/* Which route this scene takes (see above); fills hs. 0 whenever the gate is closed or no HDR is drawn. */
|
||||
static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int framegen, struct hdr_scene *hs) {
|
||||
memset(hs, 0, sizeof(*hs));
|
||||
if (!banner_color_hdr_open()) return 0;
|
||||
if (!droiddeck_color_hdr_open()) return 0;
|
||||
/* A fullscreen HDR frame this compositor could not import has no draw: layer only (route 3). */
|
||||
struct surface *un = hdr_unimported_fullscreen(dl, w, h);
|
||||
if (!un) {
|
||||
struct surface *lo = ahb_layer_only_candidate(w, h);
|
||||
if (lo && is_hdr_surface(lo)) un = lo;
|
||||
}
|
||||
if (un && g_zero_copy) { hs->color = banner_color_of(un->resource); hs->who = un; return 3; }
|
||||
hs->hf.tonemap = !banner_color_output();
|
||||
if (un && g_zero_copy) { hs->color = droiddeck_color_of(un->resource); hs->who = un; return 3; }
|
||||
hs->hf.tonemap = !droiddeck_color_output();
|
||||
if (dl->n <= 0 || !(hs->is_hdr = calloc((size_t)dl->n, 1))) return 0;
|
||||
for (int i = 0; i < dl->n; i++) {
|
||||
struct surface *s = surface_for_image(dl->d[i].img);
|
||||
if (!is_hdr_surface(s)) continue;
|
||||
hs->is_hdr[i] = 1;
|
||||
hs->n_hdr++;
|
||||
hs->color = banner_color_of(s->resource); /* later draws are above: the topmost wins */
|
||||
hs->color = droiddeck_color_of(s->resource); /* later draws are above: the topmost wins */
|
||||
hs->who = s;
|
||||
}
|
||||
if (!hs->n_hdr) { free(hs->is_hdr); hs->is_hdr = NULL; return 0; }
|
||||
@@ -1933,7 +1942,7 @@ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int fra
|
||||
static void hdr_note_route(int route, const struct hdr_scene *hs) {
|
||||
static int said = -1, said_tm = -1;
|
||||
static const char *said_why;
|
||||
const int tm = route != 0 && !banner_color_output(); /* route 0 never runs with the switch off */
|
||||
const int tm = route != 0 && !droiddeck_color_output(); /* route 0 never runs with the switch off */
|
||||
if (route == said && tm == said_tm && (route != 1 || hs->why == said_why)) return;
|
||||
int was = said;
|
||||
said = route; said_why = hs->why; said_tm = tm;
|
||||
@@ -1942,30 +1951,30 @@ static void hdr_note_route(int route, const struct hdr_scene *hs) {
|
||||
switch (route) {
|
||||
case 1:
|
||||
if (tm)
|
||||
banner_log("color", "tone-mapped picture for %s: %s - the whole scene is composed and tone-mapped to SDR "
|
||||
droiddeck_log("color", "tone-mapped picture for %s: %s - the whole scene is composed and tone-mapped to SDR "
|
||||
"(HDR peak %.0f nits rolled off to SDR white, screen effects applied after it) on the game's "
|
||||
"display layer, untagged", name, hs->why ? hs->why : "?", hs->hf.peak_nits);
|
||||
else
|
||||
banner_log("color", "HDR picture for %s: %s - the whole scene is composed into one 10-bit PQ BT.2020 picture "
|
||||
droiddeck_log("color", "HDR picture for %s: %s - the whole scene is composed into one 10-bit PQ BT.2020 picture "
|
||||
"(SDR content at %.0f nits, screen effects applied in 10-bit) on the game's display layer, tagged "
|
||||
"BT2020_PQ", name, hs->why ? hs->why : "?", banner_color_sdr_white());
|
||||
"BT2020_PQ", name, hs->why ? hs->why : "?", droiddeck_color_sdr_white());
|
||||
break;
|
||||
case 2:
|
||||
if (tm)
|
||||
banner_log("color", "HDR with frame generation for %s, HDR output switched off: the scene is tone-mapped to "
|
||||
droiddeck_log("color", "HDR with frame generation for %s, HDR output switched off: the scene is tone-mapped to "
|
||||
"SDR and presented through an SDR screen swapchain", name);
|
||||
else
|
||||
banner_log("color", "HDR with frame generation for %s: the scene is composed into PQ and presented through "
|
||||
droiddeck_log("color", "HDR with frame generation for %s: the scene is composed into PQ and presented through "
|
||||
"the screen swapchain (HDR10 where the surface offers it, else tone-mapped to SDR)", name);
|
||||
break;
|
||||
case 3:
|
||||
if (tm)
|
||||
banner_log("color", "HDR output is switched off, but %s's HDR frames cannot be imported by the compositor, "
|
||||
droiddeck_log("color", "HDR output is switched off, but %s's HDR frames cannot be imported by the compositor, "
|
||||
"so nothing can tone-map them: they stay HDR on its own display layer", name);
|
||||
break; /* otherwise hdr_note_precedence says it */
|
||||
default:
|
||||
if (was == 1 || was == 2)
|
||||
banner_log("color", "%s is back on its own display layer (no composition needed)", name);
|
||||
droiddeck_log("color", "%s is back on its own display layer (no composition needed)", name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1978,18 +1987,18 @@ static void hdr_note_precedence(struct surface *hs, int fx_on, int framegen) {
|
||||
if (hs) describe(hs, name, sizeof(name));
|
||||
if (fx_skip != fx_said) {
|
||||
if (fx_skip)
|
||||
banner_log("color", "screen effects are NOT applied to %s: its HDR frames cannot be imported by the "
|
||||
droiddeck_log("color", "screen effects are NOT applied to %s: its HDR frames cannot be imported by the "
|
||||
"compositor, so only its own display layer can show them (as they are)", name);
|
||||
else if (fx_said == 1)
|
||||
banner_log("color", "screen effects apply to the scene again (no HDR game on its layer, or effects off)");
|
||||
droiddeck_log("color", "screen effects apply to the scene again (no HDR game on its layer, or effects off)");
|
||||
fx_said = fx_skip;
|
||||
}
|
||||
if (fg_skip != fg_said) {
|
||||
if (fg_skip)
|
||||
banner_log("color", "frame generation is NOT applied to %s: its HDR frames cannot be imported by the "
|
||||
droiddeck_log("color", "frame generation is NOT applied to %s: its HDR frames cannot be imported by the "
|
||||
"compositor, so only its own display layer can show them (as they are)", name);
|
||||
else if (fg_said == 1)
|
||||
banner_log("color", "frame generation applies again (no HDR game on its layer, or frame generation off)");
|
||||
droiddeck_log("color", "frame generation applies again (no HDR game on its layer, or frame generation off)");
|
||||
fg_said = fg_skip;
|
||||
}
|
||||
}
|
||||
@@ -1998,11 +2007,11 @@ static void hdr_note_precedence(struct surface *hs, int fx_on, int framegen) {
|
||||
static void hdr_count_copied(const struct draw_list *dl, const char *reason) {
|
||||
for (int i = 0; i < dl->n; i++) {
|
||||
struct surface *s = surface_for_image(dl->d[i].img);
|
||||
const struct banner_color *c = s ? banner_color_of(s->resource) : NULL;
|
||||
const struct droiddeck_color *c = s ? droiddeck_color_of(s->resource) : NULL;
|
||||
if (!c || !c->dataspace) continue;
|
||||
char name[160];
|
||||
describe(s, name, sizeof(name));
|
||||
banner_color_frame_copied(name, reason);
|
||||
droiddeck_color_frame_copied(name, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2014,12 +2023,13 @@ static void render_scene(void) {
|
||||
int copy = 0; /* this scene went through the screen swapchain (the perf line's "copy") */
|
||||
|
||||
g_dirty = 0;
|
||||
g_hud_fresh = 0;
|
||||
g_hdr_unimported = NULL; /* found again while the scene is built (HDR gate open only) */
|
||||
wl_list_for_each(s, &g_surfaces, link) s->drawn = 0;
|
||||
scene_size(&w, &h);
|
||||
if (w != g_scene_w || h != g_scene_h) {
|
||||
if (g_desktop) banner_log("desktop", "size %dx%d", w, h);
|
||||
else banner_log("desktop", "no desktop: showing %dx%d (largest window)", w, h);
|
||||
if (g_desktop) droiddeck_log("desktop", "size %dx%d", w, h);
|
||||
else droiddeck_log("desktop", "no desktop: showing %dx%d (largest window)", w, h);
|
||||
g_scene_w = w;
|
||||
g_scene_h = h;
|
||||
}
|
||||
@@ -2038,7 +2048,7 @@ static void render_scene(void) {
|
||||
struct hdr_scene hs;
|
||||
int hdr_route = hdr_plan(&dl, w, h, fx_on, framegen, &hs);
|
||||
const char *hdr_copy = NULL; /* why this scene's HDR frames (if any) took the copy path */
|
||||
if (banner_color_hdr_open()) {
|
||||
if (droiddeck_color_hdr_open()) {
|
||||
hdr_note_route(hdr_route, &hs);
|
||||
hdr_note_precedence(hdr_route == 3 ? hs.who : NULL, fx_on, framegen);
|
||||
}
|
||||
@@ -2049,12 +2059,12 @@ static void render_scene(void) {
|
||||
sc_layer_hide_overlay();
|
||||
if (sc_layer_present_hdr_scene(dl.d, dl.n, &hs.hf, w, h, hs.color) == 0) {
|
||||
rendered = vkp_base_black(w, h) == 0; /* black under the composed picture, presented only when it is not already */
|
||||
if (fell_back) { fell_back = 0; banner_log("color", "the composed picture is on the game's display layer again"); }
|
||||
if (fell_back) { fell_back = 0; droiddeck_log("color", "the composed picture is on the game's display layer again"); }
|
||||
} else {
|
||||
hdr_route = 2; /* no layer this frame: the picture goes through the swapchain instead */
|
||||
if (!fell_back) {
|
||||
fell_back = 1;
|
||||
banner_log("color", "the composed picture could not go on the game's display layer (no layer, or the "
|
||||
droiddeck_log("color", "the composed picture could not go on the game's display layer (no layer, or the "
|
||||
"10-bit pass failed): it goes through the screen swapchain instead until that changes");
|
||||
}
|
||||
}
|
||||
@@ -2064,8 +2074,8 @@ static void render_scene(void) {
|
||||
sc_layer_hide();
|
||||
rendered = vkp_render_hdr(w, h, dl.d, dl.n, &hs.hf, &how) == 0;
|
||||
copy = 1;
|
||||
if (rendered && how == 1) banner_color_frame_shown(hs.color, BANNER_HDR_SWAPCHAIN, 0);
|
||||
else if (rendered && how == 2) banner_color_frame_shown(hs.color, BANNER_HDR_TONEMAPPED, 0);
|
||||
if (rendered && how == 1) droiddeck_color_frame_shown(hs.color, DROIDDECK_HDR_SWAPCHAIN, 0);
|
||||
else if (rendered && how == 2) droiddeck_color_frame_shown(hs.color, DROIDDECK_HDR_TONEMAPPED, 0);
|
||||
else if (rendered) hdr_copy = "the HDR composition pass is unavailable on this driver";
|
||||
}
|
||||
if (hdr_route == 0 || hdr_route == 3) {
|
||||
@@ -2094,14 +2104,14 @@ static void render_scene(void) {
|
||||
if (g_zero_copy && blocked != g_zero_copy_paused) {
|
||||
g_zero_copy_paused = blocked;
|
||||
if (blocked == 1)
|
||||
banner_log("framegen", "zero-copy paused: frame generation needs the compositor pass");
|
||||
droiddeck_log("framegen", "zero-copy paused: frame generation needs the compositor pass");
|
||||
else if (blocked == 2)
|
||||
banner_log("effects", "zero-copy paused: a window above the game needs the compositor pass for the whole scene");
|
||||
droiddeck_log("effects", "zero-copy paused: a window above the game needs the compositor pass for the whole scene");
|
||||
else if (blocked == 3)
|
||||
banner_log("layer", "zero-copy paused: a window above the game would need a second display layer, "
|
||||
droiddeck_log("layer", "zero-copy paused: a window above the game would need a second display layer, "
|
||||
"which this display cannot compose in hardware - whole scene on the copy path");
|
||||
else
|
||||
banner_log("effects", "zero-copy resumed: the game is back on its own display layer");
|
||||
droiddeck_log("effects", "zero-copy resumed: the game is back on its own display layer");
|
||||
}
|
||||
struct surface *ls = li >= 0 ? surface_for_image(dl.d[li].img) : NULL;
|
||||
/* A frame this renderer could not import can only be shown on the layer, effects or not. */
|
||||
@@ -2111,7 +2121,7 @@ static void render_scene(void) {
|
||||
if (li < 0 && !ls) ls = hdr_unimported_fullscreen(&dl, w, h);
|
||||
if (ls && pass_on && li < 0 && !g_zero_copy_fx_skip_said) {
|
||||
g_zero_copy_fx_skip_said = 1;
|
||||
banner_log(framegen ? "framegen" : "effects",
|
||||
droiddeck_log(framegen ? "framegen" : "effects",
|
||||
"the game's frames cannot be imported by the compositor: shown zero-copy, %s skipped",
|
||||
framegen ? "frame generation" : "effects");
|
||||
}
|
||||
@@ -2138,7 +2148,7 @@ static void render_scene(void) {
|
||||
r = ahb_swapchain_present(ls->dmabuf_buf, ls, w, h);
|
||||
if (r != 0 && li >= 0)
|
||||
r = fx_on ? sc_layer_present_pass(&dl.d[li], 1, w, h)
|
||||
: sc_layer_present(dl.d[li].img, w, h, ls ? banner_surface_color(ls) : NULL);
|
||||
: sc_layer_present(dl.d[li].img, w, h, ls ? droiddeck_surface_color(ls) : NULL);
|
||||
if (r == 0) {
|
||||
if (ls) ls->drawn = 1;
|
||||
/* The one window above the game keeps the game off the copy path entirely: it goes on
|
||||
@@ -2178,7 +2188,7 @@ static void render_scene(void) {
|
||||
}
|
||||
} /* routes 0 and 3 */
|
||||
free(hs.is_hdr);
|
||||
if (hdr_copy && rendered && banner_color_hdr_open()) hdr_count_copied(&dl, hdr_copy);
|
||||
if (hdr_copy && rendered && droiddeck_color_hdr_open()) hdr_count_copied(&dl, hdr_copy);
|
||||
if (rendered) {
|
||||
int64_t t = now_ns();
|
||||
g_stat_frames++;
|
||||
@@ -2229,6 +2239,13 @@ static void schedule_render(void) {
|
||||
wl_event_source_timer_update(g_fallback_timer, 8);
|
||||
}
|
||||
|
||||
/* With frame generation on, every scene is the engine's next source frame, so one drawn for
|
||||
* another window (Steam's overlay) counted as a game frame and took its present slots. Such a
|
||||
* change waits for the game's next frame, unless the game has been still for 100 ms. */
|
||||
static int scene_waits_for_game(int64_t now) {
|
||||
return g_hud_surface && !g_hud_fresh && now - g_hud_last_ns < 100000000LL && vkp_framegen_active();
|
||||
}
|
||||
|
||||
/* A screen refresh (Choreographer tick): draw the newest state once. */
|
||||
static void on_vsync(int64_t frame_time_ns) {
|
||||
int64_t now = now_ns();
|
||||
@@ -2244,7 +2261,7 @@ static void on_vsync(int64_t frame_time_ns) {
|
||||
if (vkp_apply_window_request()) g_dirty = 1;
|
||||
/* A screen-effect setting changed (JNI, any thread): redraw so it shows on a static scene too. */
|
||||
if (vkp_effects_sync()) g_dirty = 1;
|
||||
if (g_dirty) render_scene();
|
||||
if (g_dirty && !scene_waits_for_game(now)) render_scene();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ wl_seat */
|
||||
@@ -2353,7 +2370,7 @@ static void bind_seat(struct wl_client *c, void *data, uint32_t ver, uint32_t id
|
||||
struct wl_resource *r = wl_resource_create(c, &wl_seat_interface, ver, id);
|
||||
wl_resource_set_implementation(r, &seat_impl, NULL, NULL);
|
||||
wl_seat_send_capabilities(r, WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD | WL_SEAT_CAPABILITY_TOUCH);
|
||||
if (ver >= 2) wl_seat_send_name(r, "bannerlator-seat");
|
||||
if (ver >= 2) wl_seat_send_name(r, "droiddeck-seat");
|
||||
}
|
||||
|
||||
static struct seat_pointer *pointer_for(struct wl_client *client) {
|
||||
@@ -2432,7 +2449,7 @@ static void keyboard_focus(struct wl_resource *target) {
|
||||
g_mods_depressed, 0, g_mods_locked, 0);
|
||||
}
|
||||
wl_array_release(&keys);
|
||||
banner_clipboard_keyboard_focus(client); /* wl_data_device selection follows focus */
|
||||
droiddeck_clipboard_keyboard_focus(client); /* wl_data_device selection follows focus */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ pointer constraints
|
||||
@@ -2485,14 +2502,14 @@ static int g_raw_valid;
|
||||
|
||||
/* JNI: the app switches its touch/mouse path to deltas while a lock holds and re-syncs its
|
||||
* pointer to x,y when it ends. */
|
||||
extern void banner_on_pointer_lock(int locked, int x, int y);
|
||||
extern void droiddeck_on_pointer_lock(int locked, int x, int y);
|
||||
|
||||
static void sync_lock_notify(void) {
|
||||
static int last;
|
||||
int locked = g_active_constraint && g_active_constraint->is_lock;
|
||||
if (locked == last) return;
|
||||
last = locked;
|
||||
banner_on_pointer_lock(locked, (int)g_ptr_x, (int)g_ptr_y);
|
||||
droiddeck_on_pointer_lock(locked, (int)g_ptr_x, (int)g_ptr_y);
|
||||
}
|
||||
|
||||
static void constraint_describe(const struct constraint *k, char *out, size_t size) {
|
||||
@@ -2553,7 +2570,7 @@ static void constraint_end(struct constraint *k, int tell_client, const char *wh
|
||||
g_raw_valid = 0;
|
||||
if (tell_client) constraint_send_state(k, 0);
|
||||
constraint_describe(k, name, sizeof(name));
|
||||
banner_log("pointer", "%s: %s (%s), pointer at %d,%d", k->is_lock ? "unlocked" : "unconfined",
|
||||
droiddeck_log("pointer", "%s: %s (%s), pointer at %d,%d", k->is_lock ? "unlocked" : "unconfined",
|
||||
name, why, (int)g_ptr_x, (int)g_ptr_y);
|
||||
sync_lock_notify();
|
||||
}
|
||||
@@ -2573,15 +2590,15 @@ static void constraint_activate(struct constraint *k) {
|
||||
constraint_send_state(k, 1);
|
||||
constraint_describe(k, name, sizeof(name));
|
||||
if (k->is_lock)
|
||||
banner_log("pointer", "locked: %s (%s), pointer frozen at %d,%d", name,
|
||||
droiddeck_log("pointer", "locked: %s (%s), pointer frozen at %d,%d", name,
|
||||
k->lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT ? "oneshot" : "persistent",
|
||||
(int)g_ptr_x, (int)g_ptr_y);
|
||||
else if (k->region.set)
|
||||
banner_log("pointer", "confined: %s (%s) to %dx%d at %d,%d of the window", name,
|
||||
droiddeck_log("pointer", "confined: %s (%s) to %dx%d at %d,%d of the window", name,
|
||||
k->lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT ? "oneshot" : "persistent",
|
||||
k->region.w, k->region.h, k->region.x, k->region.y);
|
||||
else
|
||||
banner_log("pointer", "confined: %s (%s) to the whole window", name,
|
||||
droiddeck_log("pointer", "confined: %s (%s) to the whole window", name,
|
||||
k->lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT ? "oneshot" : "persistent");
|
||||
sync_lock_notify();
|
||||
wl_display_flush_clients(g_display);
|
||||
@@ -2635,7 +2652,7 @@ static void constraints_surface_commit(struct surface *s) {
|
||||
g_ptr_x = ox + k->hint_x;
|
||||
g_ptr_y = oy + k->hint_y;
|
||||
if (log_budget())
|
||||
banner_log("pointer", "position hint: pointer moved to %d,%d", (int)g_ptr_x, (int)g_ptr_y);
|
||||
droiddeck_log("pointer", "position hint: pointer moved to %d,%d", (int)g_ptr_x, (int)g_ptr_y);
|
||||
}
|
||||
}
|
||||
if (k->pending_region_set) {
|
||||
@@ -2716,7 +2733,7 @@ static void constraint_create(struct wl_client *c, struct wl_resource *r, uint32
|
||||
wl_list_insert(g_constraints.prev, &k->link);
|
||||
if (!s) { k->defunct = 1; return; }
|
||||
constraint_describe(k, name, sizeof(name));
|
||||
banner_log("pointer", "%s requested by %s for %s", is_lock ? "lock" : "confine", client_name(c), name);
|
||||
droiddeck_log("pointer", "%s requested by %s for %s", is_lock ? "lock" : "confine", client_name(c), name);
|
||||
constraint_activate(k);
|
||||
}
|
||||
|
||||
@@ -2747,7 +2764,7 @@ static void bind_pointer_constraints(struct wl_client *c, void *data, uint32_t v
|
||||
static void relative_pointer_resource_destroy(struct wl_resource *r) {
|
||||
struct relative_pointer *rp = wl_resource_get_user_data(r);
|
||||
if (!rp) return;
|
||||
banner_log("pointer", "relative pointer released by %s", client_name(wl_resource_get_client(r)));
|
||||
droiddeck_log("pointer", "relative pointer released by %s", client_name(wl_resource_get_client(r)));
|
||||
wl_list_remove(&rp->link);
|
||||
free(rp);
|
||||
}
|
||||
@@ -2766,7 +2783,7 @@ static void relative_pointer_manager_get(struct wl_client *c, struct wl_resource
|
||||
rp->pointer = pointer;
|
||||
wl_resource_set_implementation(res, &relative_pointer_impl, rp, relative_pointer_resource_destroy);
|
||||
wl_list_insert(g_relative_pointers.prev, &rp->link);
|
||||
banner_log("pointer", "relative pointer created for %s (motion arrives as deltas)", client_name(c));
|
||||
droiddeck_log("pointer", "relative pointer created for %s (motion arrives as deltas)", client_name(c));
|
||||
}
|
||||
static const struct zwp_relative_pointer_manager_v1_interface relative_pointer_manager_impl = {
|
||||
.destroy = relative_pointer_manager_destroy,
|
||||
@@ -2873,7 +2890,7 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int
|
||||
if (button && pressed && !(k && k->is_lock)) {
|
||||
struct surface *clicked = toplevel_at(g_ptr_x, g_ptr_y);
|
||||
if (clicked) g_ime_click = clicked;
|
||||
banner_text_input_refocus();
|
||||
droiddeck_text_input_refocus();
|
||||
}
|
||||
wl_display_flush_clients(g_display);
|
||||
}
|
||||
@@ -3017,7 +3034,7 @@ static void touch_down(int id, double x, double y) {
|
||||
if (!any_active) {
|
||||
char name[160];
|
||||
describe(target, name, sizeof(name));
|
||||
banner_log("touch", "%s gets %s", name, has_touch ? "touch" : "the pointer (it takes no touch)");
|
||||
droiddeck_log("touch", "%s gets %s", name, has_touch ? "touch" : "the pointer (it takes no touch)");
|
||||
}
|
||||
if (!has_touch) {
|
||||
if (any_active) return; /* one finger drives the pointer; a second one is ignored */
|
||||
@@ -3096,7 +3113,7 @@ static void deliver_touch(const struct input_msg *m) {
|
||||
}
|
||||
|
||||
static void key_event(uint32_t evdev, int pressed);
|
||||
struct wl_resource *banner_ime_target(void);
|
||||
struct wl_resource *droiddeck_ime_target(void);
|
||||
static void deliver_key(const struct input_msg *m) {
|
||||
key_event((uint32_t)m->p1, m->p2);
|
||||
}
|
||||
@@ -3160,7 +3177,7 @@ static void key_event(uint32_t evdev, int pressed) {
|
||||
* thread, not to the foreground program (X11 delivers keys to the focused app window, so
|
||||
* this mirrors it). The desktop itself is the last resort. */
|
||||
struct surface *target = NULL;
|
||||
struct wl_resource *ime = banner_ime_target();
|
||||
struct wl_resource *ime = droiddeck_ime_target();
|
||||
if (ime) target = wl_resource_get_user_data(ime);
|
||||
if (!target && g_key_target && g_key_target->mapped && g_key_target != g_desktop) target = g_key_target;
|
||||
if (!target) target = g_desktop;
|
||||
@@ -3203,14 +3220,14 @@ static int on_input_readable(int fd, uint32_t mask, void *data) {
|
||||
|
||||
/* Called from JNI (Android UI thread). Queues a pointer event; the compositor thread
|
||||
* dispatches it. x/y are in INPUT_SPACE (0..1919, 0..1079) over the whole output. */
|
||||
void banner_wayland_send_pointer(int action, int x, int y) {
|
||||
void droiddeck_wayland_send_pointer(int action, int x, int y) {
|
||||
if (g_input_pipe[1] < 0) return;
|
||||
struct input_msg m = { 0, action, x, y, 0 };
|
||||
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
|
||||
(void)n;
|
||||
}
|
||||
|
||||
void banner_wayland_send_touch(int action, int pointer_id, int x, int y) {
|
||||
void droiddeck_wayland_send_touch(int action, int pointer_id, int x, int y) {
|
||||
if (g_input_pipe[1] < 0) return;
|
||||
struct input_msg m = { 7, action, pointer_id, x, y };
|
||||
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
|
||||
@@ -3218,7 +3235,7 @@ void banner_wayland_send_touch(int action, int pointer_id, int x, int y) {
|
||||
}
|
||||
|
||||
/* Called from JNI. Queues a key event. evdev = Linux input keycode (e.g. KEY_A=30); state 1=down 0=up. */
|
||||
void banner_wayland_send_key(int evdev, int state) {
|
||||
void droiddeck_wayland_send_key(int evdev, int state) {
|
||||
if (g_input_pipe[1] < 0) return;
|
||||
struct input_msg m = { 1, evdev, state, 0, 0 };
|
||||
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
|
||||
@@ -3226,7 +3243,7 @@ void banner_wayland_send_key(int evdev, int state) {
|
||||
}
|
||||
|
||||
/* Called from JNI on every screen refresh (Choreographer). */
|
||||
void banner_wayland_vsync(int64_t frame_time_ns) {
|
||||
void droiddeck_wayland_vsync(int64_t frame_time_ns) {
|
||||
if (g_input_pipe[1] < 0) return;
|
||||
struct input_msg m = { 5, (int)(frame_time_ns >> 32), (int)(uint32_t)frame_time_ns, 0, 0 };
|
||||
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
|
||||
@@ -3236,7 +3253,7 @@ void banner_wayland_vsync(int64_t frame_time_ns) {
|
||||
/* Called from JNI with the app's X-server input (on-screen controls, mouse): type 2 = motion
|
||||
* to scene x,y; 3 = evdev button a pressed/released (b); 4 = a wheel steps (negative = up);
|
||||
* 6 = relative motion by a,b in 1/256 pixel (the app's Relative Mouse / captured-mouse path). */
|
||||
void banner_wayland_send_scene_input(int type, int a, int b) {
|
||||
void droiddeck_wayland_send_scene_input(int type, int a, int b) {
|
||||
if (g_input_pipe[1] < 0) return;
|
||||
struct input_msg m = { type, a, b, 0, 0 };
|
||||
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
|
||||
@@ -3244,15 +3261,15 @@ void banner_wayland_send_scene_input(int type, int a, int b) {
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ extension module hooks
|
||||
* What wl_clipboard.c / wl_text_input.c need from the scene (see banner_ext.h). */
|
||||
* What wl_clipboard.c / wl_text_input.c need from the scene (see droiddeck_ext.h). */
|
||||
|
||||
const char *banner_client_name(struct wl_client *client) { return client_name(client); }
|
||||
struct wl_display *banner_get_display(void) { return g_display; }
|
||||
void banner_request_redraw(void) { schedule_render(); }
|
||||
const char *droiddeck_client_name(struct wl_client *client) { return client_name(client); }
|
||||
struct wl_display *droiddeck_get_display(void) { return g_display; }
|
||||
void droiddeck_request_redraw(void) { schedule_render(); }
|
||||
|
||||
/* The surface text input follows: the last clicked mapped program window, else the topmost
|
||||
* mapped window not owned by the desktop's process (explorer). NULL when there is none. */
|
||||
struct wl_resource *banner_ime_target(void) {
|
||||
struct wl_resource *droiddeck_ime_target(void) {
|
||||
struct surface *s;
|
||||
if (g_ime_click && g_ime_click->mapped) return g_ime_click->resource;
|
||||
struct wl_client *shell = g_desktop ? wl_resource_get_client(g_desktop->resource) : NULL;
|
||||
@@ -3261,13 +3278,13 @@ struct wl_resource *banner_ime_target(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void banner_surface_scene_origin(struct wl_resource *surface, int *x, int *y) {
|
||||
void droiddeck_surface_scene_origin(struct wl_resource *surface, int *x, int *y) {
|
||||
struct surface *s = surface ? wl_resource_get_user_data(surface) : NULL;
|
||||
*x = 0; *y = 0;
|
||||
if (s && s->placed) { *x = s->x; *y = s->y; }
|
||||
}
|
||||
|
||||
void banner_inject_key(uint32_t evdev, int pressed) { key_event(evdev, pressed); }
|
||||
void droiddeck_inject_key(uint32_t evdev, int pressed) { key_event(evdev, pressed); }
|
||||
|
||||
/* ------------------------------------------------------------------ 10 s summary */
|
||||
|
||||
@@ -3294,7 +3311,7 @@ static int on_stats_timer(void *data) {
|
||||
* zero-copy, so they are counted apart from the line above. */
|
||||
if (layer_frames) off += snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u layer frames", layer_frames);
|
||||
if (generated) snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u generated frames", generated);
|
||||
banner_log("stats", "last 10 s: %u frames on screen (%.1f fps) | %u GPU frames from games | %u window redraws | %d windows open%s",
|
||||
droiddeck_log("stats", "last 10 s: %u frames on screen (%.1f fps) | %u GPU frames from games | %u window redraws | %d windows open%s",
|
||||
g_stat_frames + generated, (g_stat_frames + generated) / 10.0, g_stat_dmabuf, g_stat_shm, windows, extra);
|
||||
}
|
||||
/* The perf line, next to the stats line and only when something happened: the compositor thread's
|
||||
@@ -3306,7 +3323,7 @@ static int on_stats_timer(void *data) {
|
||||
const unsigned drops = sc_layer_drops_take();
|
||||
#define PERF_MS(sum, n) ((n) ? (double)(sum) / 1e6 / (double)(n) : 0.0)
|
||||
if (g_perf.scenes || vp.presents || g_perf.releases || drops)
|
||||
banner_log("perf", "last 10 s: %u ticks, %u scenes, %u on screen (copy %u, zero-copy %u, layer copy %u) | "
|
||||
droiddeck_log("perf", "last 10 s: %u ticks, %u scenes, %u on screen (copy %u, zero-copy %u, layer copy %u) | "
|
||||
"render_scene %.2f/%.2f ms | base %u black kept, %u presented | acquire %.2f/%.2f ms | "
|
||||
"present %.2f/%.2f ms (%u) | fence wait %.2f/%.2f ms (%u, %u GPU release waits) | "
|
||||
"release %.2f/%.2f ms (%u, %u held) | %u pool drops",
|
||||
@@ -3322,7 +3339,7 @@ static int on_stats_timer(void *data) {
|
||||
memset(&g_perf, 0, sizeof(g_perf));
|
||||
}
|
||||
g_stat_frames = g_stat_dmabuf = g_stat_shm = 0;
|
||||
banner_color_stats_tick(); /* HDR evidence for the same window (nothing when the HDR gate is closed) */
|
||||
droiddeck_color_stats_tick(); /* HDR evidence for the same window (nothing when the HDR gate is closed) */
|
||||
/* A program that asked for dma-buf feedback (EGL's GPU path does, first thing) but has drawn
|
||||
* only wl_shm frames since is an OpenGL program whose EGL gave up on the GPU. The software
|
||||
* path it fell to has no rasteriser in the Wayland layers, so what it commits is black. */
|
||||
@@ -3330,7 +3347,7 @@ static int on_stats_timer(void *data) {
|
||||
if (!ci->asked_feedback || ci->dmabuf_buffers || ci->shm_gl_said || ci->shm_frames < 150)
|
||||
continue;
|
||||
ci->shm_gl_said = 1;
|
||||
banner_log("opengl", "%s asked for GPU buffers but has drawn only software (shared-memory) "
|
||||
droiddeck_log("opengl", "%s asked for GPU buffers but has drawn only software (shared-memory) "
|
||||
"frames: its OpenGL fell back to software rendering, which the Wayland layer cannot "
|
||||
"draw - expect a black picture (main device %u:%u)", ci->name,
|
||||
(unsigned)major(g_main_device), (unsigned)minor(g_main_device));
|
||||
@@ -3356,7 +3373,7 @@ static void test_input_line(const char *line) {
|
||||
int n = sscanf(line, "%15s %lf %lf", cmd, &x, &y);
|
||||
|
||||
if (n < 2) return;
|
||||
banner_log("test", "input: %s", line);
|
||||
droiddeck_log("test", "input: %s", line);
|
||||
if (!strcmp(cmd, "key") || !strcmp(cmd, "keydown") || !strcmp(cmd, "keyup")) {
|
||||
code = (unsigned)x;
|
||||
if (strcmp(cmd, "keyup")) key_event(code, 1);
|
||||
@@ -3419,7 +3436,7 @@ static void start_test_input(struct wl_event_loop *loop) {
|
||||
|
||||
static struct wl_listener g_client_created = { .notify = on_client_created };
|
||||
|
||||
int banner_wayland_run(void) {
|
||||
int droiddeck_wayland_run(void) {
|
||||
wl_list_init(&g_surfaces);
|
||||
wl_list_init(&g_toplevels);
|
||||
wl_list_init(&g_constraints);
|
||||
@@ -3451,7 +3468,7 @@ int banner_wayland_run(void) {
|
||||
rt ? rt : "(null)");
|
||||
return 1;
|
||||
}
|
||||
banner_log("display", "Wayland compositor listening on %s/wayland-0", rt ? rt : "?");
|
||||
droiddeck_log("display", "Wayland compositor listening on %s/wayland-0", rt ? rt : "?");
|
||||
wl_display_add_client_created_listener(display, &g_client_created);
|
||||
|
||||
wl_global_create(display, &wl_compositor_interface, 6, NULL, bind_compositor);
|
||||
@@ -3468,13 +3485,13 @@ int banner_wayland_run(void) {
|
||||
wl_global_create(display, &wp_presentation_interface, 2, NULL, bind_presentation);
|
||||
wl_global_create(display, &zwp_pointer_constraints_v1_interface, 1, NULL, bind_pointer_constraints);
|
||||
wl_global_create(display, &zwp_relative_pointer_manager_v1_interface, 1, NULL, bind_relative_pointer_manager);
|
||||
banner_ext_init(display); /* clipboard, text input, toplevel icons (own files, see banner_ext.h) */
|
||||
droiddeck_ext_init(display); /* clipboard, text input, toplevel icons (own files, see droiddeck_ext.h) */
|
||||
ahb_swapchain_init(display); /* zero-copy layers: banner_ahb_v1, advertised whenever a display
|
||||
* layer is possible; the mode event carries the live switch
|
||||
* (ahb_swapchain.h) */
|
||||
banner_color_init(display); /* HDR10 (opt-in): decides the gate from the app's request + the
|
||||
droiddeck_color_init(display); /* HDR10 (opt-in): decides the gate from the app's request + the
|
||||
* display + the layer path above; creates wp_color_manager_v1
|
||||
* only when it is open (banner_color.h). Before any client can
|
||||
* only when it is open (droiddeck_color.h). Before any client can
|
||||
* bind zwp_linux_dmabuf_v1, whose table it widens. */
|
||||
wl_list_init(&g_pending_releases);
|
||||
g_release_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
|
||||
|
||||
@@ -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 */
|
||||
@@ -71,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 *);
|
||||
@@ -95,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");
|
||||
@@ -125,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;
|
||||
@@ -214,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++)
|
||||
@@ -330,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 */
|
||||
@@ -368,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;
|
||||
@@ -387,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)";
|
||||
@@ -404,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
|
||||
@@ -427,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;
|
||||
}
|
||||
@@ -439,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; }
|
||||
@@ -451,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);
|
||||
@@ -532,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));
|
||||
}
|
||||
}
|
||||
@@ -549,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();
|
||||
@@ -559,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;
|
||||
}
|
||||
@@ -585,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] ? ")" : "");
|
||||
}
|
||||
}
|
||||
@@ -606,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
|
||||
@@ -630,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;
|
||||
@@ -711,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,
|
||||
@@ -748,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;
|
||||
@@ -760,14 +760,14 @@ 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();
|
||||
@@ -792,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;
|
||||
}
|
||||
@@ -805,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;
|
||||
@@ -820,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;
|
||||
@@ -836,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();
|
||||
@@ -862,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);
|
||||
@@ -885,14 +889,14 @@ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const stru
|
||||
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) != 0) return -1;
|
||||
if (color && color->dataspace) banner_color_frame_shown(color, BANNER_HDR_LAYER_COPY, AHB_RGBA8);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -912,11 +916,11 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -931,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;
|
||||
@@ -945,7 +949,7 @@ 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);
|
||||
}
|
||||
@@ -953,25 +957,26 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct
|
||||
if (vkp_pass_copy_to(l->slots[idx].img, wait_fd) != 0) return -1;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1020,13 +1025,13 @@ 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;
|
||||
@@ -1077,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) {
|
||||
@@ -1093,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "framegen_bridge.h"
|
||||
#include "hdr_compose.h"
|
||||
#include "blend_pass.h"
|
||||
#include "banner_color.h"
|
||||
#include "droiddeck_color.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -19,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 {
|
||||
@@ -73,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
|
||||
@@ -179,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; }
|
||||
@@ -276,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;
|
||||
}
|
||||
|
||||
@@ -353,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);
|
||||
@@ -391,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;
|
||||
@@ -407,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,
|
||||
@@ -444,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);
|
||||
|
||||
@@ -464,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;
|
||||
}
|
||||
@@ -487,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;
|
||||
@@ -506,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");
|
||||
|
||||
@@ -641,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)" : "");
|
||||
}
|
||||
|
||||
@@ -678,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);
|
||||
}
|
||||
@@ -702,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);
|
||||
}
|
||||
@@ -748,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;
|
||||
@@ -767,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;
|
||||
}
|
||||
@@ -833,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
|
||||
@@ -971,7 +971,7 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1182,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;
|
||||
}
|
||||
|
||||
@@ -1218,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;
|
||||
}
|
||||
|
||||
@@ -1249,7 +1249,7 @@ 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++) {
|
||||
@@ -1272,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);
|
||||
}
|
||||
}
|
||||
@@ -1287,7 +1287,7 @@ 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);
|
||||
@@ -1385,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;
|
||||
@@ -1422,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();
|
||||
@@ -1464,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};
|
||||
@@ -1476,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;
|
||||
@@ -1506,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);
|
||||
}
|
||||
|
||||
@@ -1522,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();
|
||||
}
|
||||
@@ -1531,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();
|
||||
@@ -1570,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);
|
||||
}
|
||||
}
|
||||
@@ -1746,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;
|
||||
}
|
||||
@@ -1791,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) {
|
||||
@@ -1799,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;
|
||||
}
|
||||
@@ -1815,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;
|
||||
@@ -1856,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) {
|
||||
|
||||
@@ -122,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
|
||||
@@ -184,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
|
||||
@@ -13,10 +14,11 @@ import com.droiddeck.launcher.session.ComponentsManager
|
||||
*/
|
||||
internal class ComponentsMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) {
|
||||
var compSnapshot by mutableStateOf<ComponentsManager.Snapshot?>(null)
|
||||
var defaultProtonId by mutableStateOf<String?>(null)
|
||||
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())
|
||||
@@ -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())
|
||||
defaultProtonId = chosen
|
||||
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 (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
|
||||
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()
|
||||
refreshComponents()
|
||||
}
|
||||
}, "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()
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,8 @@
|
||||
package com.droiddeck.launcher
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import android.hardware.input.InputManager
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.content.Intent
|
||||
@@ -23,6 +26,7 @@ import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -32,12 +36,18 @@ import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.core.TextureFiltering
|
||||
import com.droiddeck.launcher.files.InAppFilePicker
|
||||
import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.gpu.FrameGen
|
||||
import com.droiddeck.launcher.gpu.LsfgNative
|
||||
import com.droiddeck.launcher.gpu.ScreenEffectLooks
|
||||
import com.droiddeck.launcher.gpu.ScreenEffects
|
||||
import com.droiddeck.launcher.gpu.Lossless
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import com.droiddeck.launcher.input.EvdevKeys
|
||||
import com.droiddeck.launcher.input.KeyboardHost
|
||||
import com.droiddeck.launcher.input.ControllerPrefs
|
||||
import com.droiddeck.launcher.input.SessionClipboard
|
||||
import com.droiddeck.launcher.input.OnScreenControls
|
||||
import com.droiddeck.launcher.input.PadBridge
|
||||
import com.droiddeck.launcher.input.PointerGestures
|
||||
@@ -49,6 +59,7 @@ import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.LoadingState
|
||||
import com.droiddeck.launcher.session.PerfHud
|
||||
import com.droiddeck.launcher.session.PerfMode
|
||||
import com.droiddeck.launcher.session.GameEnvironmentStore
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.SessionEvents
|
||||
import com.droiddeck.launcher.session.SessionArtifacts
|
||||
@@ -86,6 +97,10 @@ import kotlin.math.abs
|
||||
* the HUD line, the loading overlay, the drawer and its dialogs.
|
||||
*/
|
||||
class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private lateinit var pip: com.droiddeck.launcher.session.SessionPipController
|
||||
private var pipUi by mutableStateOf(false)
|
||||
private var pipAutoEnter by mutableStateOf(false)
|
||||
private val guestKeysDown = mutableSetOf<Int>()
|
||||
private lateinit var surfaceView: SurfaceView
|
||||
private lateinit var sessionOverlay: ComposeView
|
||||
private lateinit var loading: LoadingState
|
||||
@@ -95,15 +110,39 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var padMotion: com.droiddeck.launcher.input.PadMotion? = null
|
||||
/** Between onResume and onPause. */
|
||||
private var resumed = false
|
||||
private val deckPadListener: () -> Unit = { uiHandler.post { updatePadMotion() } }
|
||||
private val deckPadListener: () -> Unit = { uiHandler.post { updatePadMotion(); openDeckRing("deck pad changed") } }
|
||||
|
||||
/**
|
||||
* A Deck's controller is there from the moment it boots, and the client looks for it once, as
|
||||
* it starts. The ring behind the Deck (libfakeinput's /dev/hidraw16) opens on the pad's first
|
||||
* input otherwise - on a phone with nothing attached, the first touch of the on-screen
|
||||
* controls, seconds after the client had looked, found no controller and never set the touch
|
||||
* mode, so its interface took a finger as a mouse. Opened as soon as the session settles on a
|
||||
* Deck instead.
|
||||
*/
|
||||
private fun openDeckRing(why: String) {
|
||||
val deck = SessionState.deckPad
|
||||
val opened = if (deck) padBridge?.start() else null
|
||||
Log.i(TAG, "deck ring ($why): deck pad $deck" + when (opened) {
|
||||
null -> ", left for first input"
|
||||
true -> ", ring open"
|
||||
false -> ", ring NOT open"
|
||||
})
|
||||
}
|
||||
|
||||
/** Motion is read while the session shows and only if the pad is a Deck controller - which
|
||||
* the service can decide after this activity has resumed (SessionState.deckPadListener). */
|
||||
private fun updatePadMotion() {
|
||||
if (resumed && SessionState.deckPad) padMotion?.start() else padMotion?.stop()
|
||||
if (resumed && !pipUi && SessionState.deckPad) padMotion?.start() else padMotion?.stop()
|
||||
}
|
||||
private var onScreenControls: OnScreenControls? = null
|
||||
private var keyboard: KeyboardHost? = null
|
||||
private var controllerSettings by mutableStateOf<ControllerPrefs.Settings?>(null)
|
||||
private val sessionClipboard by lazy {
|
||||
SessionClipboard(this) {
|
||||
window.decorView.hasWindowFocus() || secondScreenPresentation?.window?.decorView?.hasWindowFocus() == true
|
||||
}
|
||||
}
|
||||
private var watching = true
|
||||
private lateinit var touchpad: TouchpadGestures
|
||||
private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO)
|
||||
@@ -160,6 +199,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var quitFrom by mutableStateOf<androidx.compose.ui.geometry.Rect?>(null)
|
||||
/** Left behind that flood: the front end opens on its blue, so no sink of our own. */
|
||||
private var quitFlooded = false
|
||||
/** Leaving behind a flood spreading out of the throbber (FloodSpread); see finish(). */
|
||||
private var leaving by mutableStateOf(false)
|
||||
/** Close without the leaving flood: a retry opens a fresh session at once. */
|
||||
private var closeAtOnce = false
|
||||
/** The drawer's Components tab: the Protons as last read (ComponentsManager). */
|
||||
private var drawerComponents by mutableStateOf<ComponentsManager.Snapshot?>(null)
|
||||
private var drawerPage by mutableIntStateOf(0)
|
||||
@@ -169,9 +212,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var pcKeyboardOpen by mutableStateOf(false)
|
||||
private var hudOn by mutableStateOf(true)
|
||||
private var fillScreen by mutableStateOf(true)
|
||||
private var frameGenLabel by mutableStateOf("Off")
|
||||
private var frameGenEngine by mutableStateOf(FrameGen.ENGINE_OFF)
|
||||
private var frameGenMultiplier by mutableStateOf(2)
|
||||
private var upscaler by mutableStateOf(0)
|
||||
private var upscaleSharpness by mutableStateOf(75)
|
||||
private var effects by mutableStateOf(ScreenEffects.OFF)
|
||||
private var textureAnisotropy by mutableStateOf(0)
|
||||
private var textureLodBias by mutableStateOf(TextureFiltering.LOD_BIAS_OFF)
|
||||
private var lossless by mutableStateOf(Lossless.State.NONE)
|
||||
private val pickLossless = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedFile(r.data)?.let { importLossless(it) }
|
||||
}
|
||||
private var frameGen by mutableStateOf(FrameGen.Mode.OFF)
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var suspendPolicy by mutableStateOf(SessionPrefs.SUSPEND_MANUAL)
|
||||
private var oscMode by mutableStateOf(SessionPrefs.OSC_AUTO)
|
||||
@@ -186,7 +236,24 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private val displayListener = object : DisplayManager.DisplayListener {
|
||||
override fun onDisplayAdded(displayId: Int) = refreshSecondScreenDisplays()
|
||||
override fun onDisplayRemoved(displayId: Int) = refreshSecondScreenDisplays()
|
||||
override fun onDisplayChanged(displayId: Int) = refreshSecondScreenDisplays()
|
||||
override fun onDisplayChanged(displayId: Int) {
|
||||
followPanelRefresh(displayId)
|
||||
refreshSecondScreenDisplays()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The panel's rate, followed for the rest of the session. It is read once at start, and a
|
||||
* panel that drops from 120 to 60 Hz later (battery saver, heat, a vendor overriding the mode
|
||||
* asked for) left the compositor telling gamescope every frame took 8.3 ms, so the client and
|
||||
* its games paced against a vblank that no longer came. (WinNative 53836ca9.)
|
||||
*/
|
||||
private fun followPanelRefresh(displayId: Int) {
|
||||
if (!CompositorHost.isStarted) return
|
||||
val display = (if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay) ?: return
|
||||
if (display.displayId != displayId) return
|
||||
val hz = display.refreshRate
|
||||
if (hz > 1f) WaylandCompositor.nativeSetOutputRefreshRate(hz)
|
||||
}
|
||||
private var isHomeApp by mutableStateOf(false)
|
||||
private var androidApps by mutableStateOf<List<HomeApp.LaunchableApp>>(emptyList())
|
||||
@@ -197,27 +264,52 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
* press, and a user without one must not be left with no way to answer Big Picture.
|
||||
*/
|
||||
private val deviceListener = object : InputManager.InputDeviceListener {
|
||||
override fun onInputDeviceAdded(deviceId: Int) = updateOnScreenControls()
|
||||
override fun onInputDeviceRemoved(deviceId: Int) = updateOnScreenControls()
|
||||
override fun onInputDeviceAdded(deviceId: Int) {
|
||||
logInputDevice("connected", deviceId)
|
||||
updateOnScreenControls()
|
||||
}
|
||||
override fun onInputDeviceRemoved(deviceId: Int) {
|
||||
Log.i(TAG, "input device $deviceId disconnected")
|
||||
updateOnScreenControls()
|
||||
}
|
||||
override fun onInputDeviceChanged(deviceId: Int) = updateOnScreenControls()
|
||||
}
|
||||
|
||||
private var runtimeRemovalBlocked = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isRemoving()) {
|
||||
runtimeRemovalBlocked = true
|
||||
android.widget.Toast.makeText(this, "Wait for runtime removal to finish", android.widget.Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (!SessionState.running && SessionState.phase in setOf(SessionPhase.IDLE, SessionPhase.FAILED)) {
|
||||
SessionEvents.begin(this, intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM)
|
||||
val mode = intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM
|
||||
// A Flatpak app or AppImage is named by its arguments, which the name lookup reads here.
|
||||
SessionState.programArgs = intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS)?.toList().orEmpty()
|
||||
SessionEvents.begin(this, mode, SessionPaths.label(mode, intent.getStringExtra(SessionService.EXTRA_PROGRAM)))
|
||||
}
|
||||
if (!SessionState.running && SessionState.phase in setOf(SessionPhase.IDLE, SessionPhase.FAILED)) {
|
||||
SessionState.program = intent.getStringExtra(SessionService.EXTRA_PROGRAM)
|
||||
SessionState.programArgs = intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS)?.toList().orEmpty()
|
||||
SessionState.steamUi = intent.getStringExtra(SessionService.EXTRA_STEAM_UI)
|
||||
SessionState.steamUrl = intent.getStringExtra(SessionService.EXTRA_STEAM_URL)
|
||||
}
|
||||
SessionState.program = intent.getStringExtra(SessionService.EXTRA_PROGRAM)
|
||||
SessionState.programArgs = intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS)?.toList().orEmpty()
|
||||
SessionState.steamUi = intent.getStringExtra(SessionService.EXTRA_STEAM_UI)
|
||||
SessionState.steamUrl = intent.getStringExtra(SessionService.EXTRA_STEAM_URL)
|
||||
if (intent.action == SessionService.ACTION_AGENT_START) {
|
||||
SessionEvents.record("agent.start_requested", mapOf("mode" to SessionState.mode))
|
||||
}
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
// Game-tier power policy for the whole session: the panel's fastest mode, and the OS told
|
||||
// it is in gameplay. Logged so a slow device says why.
|
||||
Log.i(TAG, "perf: " + PerfMode.apply(this))
|
||||
// Game-tier power policy for the whole session: the panel's fastest mode (one the frame cap
|
||||
// divides, when there is a cap), and the OS told it is in gameplay. Logged so a slow device
|
||||
// says why. The cap is the session's: an activity re-created mid-session keeps it.
|
||||
if (!SessionState.running) {
|
||||
SessionState.fpsLimit = SessionPrefs.fpsLimit(
|
||||
this, SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM),
|
||||
)
|
||||
}
|
||||
Log.i(TAG, "perf: " + PerfMode.apply(this, SessionState.fpsLimit))
|
||||
goFullscreen()
|
||||
// The device's volume keys change the stream the session plays on (the relay and
|
||||
// PulseAudio are media playback); they are never forwarded to the guest.
|
||||
@@ -241,6 +333,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
(if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay)?.rotation ?: android.view.Surface.ROTATION_0
|
||||
}
|
||||
SessionState.deckPadListener = deckPadListener
|
||||
openDeckRing("activity created")
|
||||
// A player on the pad or the on-screen controls has no use for the mouse arrow; the next
|
||||
// touchpad or mouse move brings it back (showCursor), once the pad has been quiet a moment.
|
||||
bridge.setOnPlayerInput {
|
||||
@@ -248,7 +341,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
uiHandler.removeCallbacks(cursorHide)
|
||||
cursorVisible = false
|
||||
}
|
||||
onScreenControls = OnScreenControls(this, bridge).also { root.addView(it) }
|
||||
onScreenControls = OnScreenControls(this, bridge, onKeyboard = ::togglePcKeyboard).also { root.addView(it) }
|
||||
keyboard = KeyboardHost(this).also { root.addView(it) }
|
||||
touchpad = TouchpadGestures(PointerGestures.slop(this), pointerListener)
|
||||
// One arrow, ours: Android draws a system pointer for a mouse over any window, and the
|
||||
@@ -284,6 +377,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
sessionOverlay = createSessionOverlay()
|
||||
root.addView(sessionOverlay)
|
||||
setContentView(root)
|
||||
pip = com.droiddeck.launcher.session.SessionPipController(this, ::pipBounds, ::showPipUi)
|
||||
surfaceView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> pip.refresh() }
|
||||
pip.refresh()
|
||||
handleHomeGuideIntent(intent)
|
||||
|
||||
updateOnScreenControls()
|
||||
@@ -294,6 +390,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// The single action waits out the double-press window so the two actions stay distinct.
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (pipUi) return
|
||||
// The PC keyboard closes first (B on a controller), then Back is the drawer again.
|
||||
when {
|
||||
::loading.isInitialized && loading.ended -> finish()
|
||||
@@ -313,8 +410,21 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private fun createSessionOverlay(): ComposeView = ComposeView(this).apply {
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
// Compose draws each control's focus ring. Android's default highlight would cover
|
||||
// this full-screen host and remain as a white veil after the drawer closes.
|
||||
defaultFocusHighlightEnabled = false
|
||||
setContent {
|
||||
DroidDeckTheme {
|
||||
if (pipUi) {
|
||||
if (SessionState.suspended) androidx.compose.foundation.layout.Box(
|
||||
Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center,
|
||||
) {
|
||||
androidx.compose.material3.Text(getString(R.string.pip_suspended),
|
||||
color = androidx.compose.ui.graphics.Color.White,
|
||||
modifier = Modifier.background(androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.7f)).padding(6.dp))
|
||||
}
|
||||
} else {
|
||||
com.droiddeck.launcher.ui.FocusGlideHost(androidx.compose.ui.Modifier.fillMaxSize()) {
|
||||
CursorOverlay(cursorPos, cursorVisible, resources.displayMetrics.density,
|
||||
cursorImage, cursorHotX, cursorHotY, cursorImageScale)
|
||||
if (hud.text.isNotEmpty()) HudText(hud.text)
|
||||
@@ -336,6 +446,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// Opening the drawer takes the controller away from the game: release its pad.
|
||||
androidx.compose.runtime.LaunchedEffect(drawerOpen) {
|
||||
if (drawerOpen) {
|
||||
syncLossless()
|
||||
padBridge?.releaseAll()
|
||||
androidx.compose.runtime.withFrameNanos { }
|
||||
val requested = sessionOverlay.requestFocus()
|
||||
@@ -347,7 +458,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// The PC keyboard takes it too, for moving over the keys; the drawer opens over it.
|
||||
androidx.compose.runtime.LaunchedEffect(pcKeyboardOpen) { if (pcKeyboardOpen) padBridge?.releaseAll() }
|
||||
if (pcKeyboardOpen) com.droiddeck.launcher.ui.PcKeyboard(
|
||||
sendKey = { code, down -> if (CompositorHost.isStarted) WaylandCompositor.nativeSendKey(code, if (down) 1 else 0) },
|
||||
sendKey = { code, down -> if (CompositorHost.isStarted) sendGuestKey(code, down) },
|
||||
onAndroidKeyboard = { pcKeyboardOpen = false; keyboard?.toggle() },
|
||||
onClose = { pcKeyboardOpen = false },
|
||||
)
|
||||
@@ -359,8 +470,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
androidApps = androidApps,
|
||||
hudOn = hudOn,
|
||||
fillScreen = if (SessionState.mode == SessionService.MODE_STEAM) fillScreen else null,
|
||||
frameGenEngine = frameGenEngine, frameGenMultiplier = frameGenMultiplier,
|
||||
lsfgReady = LsfgNative.isInstalled(this@SessionActivity),
|
||||
upscaler = upscaler, upscaleSharpness = upscaleSharpness,
|
||||
effects = effects, textureAnisotropy = textureAnisotropy, textureLodBias = textureLodBias,
|
||||
frameGen = frameGen,
|
||||
lossless = lossless,
|
||||
oscMode = oscMode, suspendPolicy = suspendPolicy, touchMode = touchMode,
|
||||
touchAuto = if (usingTouchpad()) "touchpad" else "direct",
|
||||
shapeMode = shapeMode, fexPreset = fexPreset,
|
||||
@@ -369,25 +482,40 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
selectedSecondScreenDisplay = selectedSecondScreenDisplay,
|
||||
onHud = { on -> SessionPrefs.setHudEnabled(this@SessionActivity, on); hudOn = on; hud.refresh() },
|
||||
onFillScreen = { on -> SessionPrefs.setForceFullscreen(this@SessionActivity, on); fillScreen = on },
|
||||
onFrameGenPick = { engine, multiplier ->
|
||||
FrameGen.set(this@SessionActivity, engine, multiplier)
|
||||
onUpscaler = { m ->
|
||||
SessionPrefs.setUpscaler(this@SessionActivity, m); upscaler = m
|
||||
WaylandCompositor.nativeSetUpscaler(m)
|
||||
// Two Looks carry a scaling mode of their own, so the mode decides which one the log names.
|
||||
WaylandCompositor.nativeSetLookName(ScreenEffectLooks.match(effects, m)?.name)
|
||||
},
|
||||
onUpscaleSharpness = { pct ->
|
||||
SessionPrefs.setUpscaleSharpness(this@SessionActivity, pct); upscaleSharpness = pct
|
||||
WaylandCompositor.nativeSetUpscaleSharpness(pct)
|
||||
},
|
||||
onEffects = { e ->
|
||||
SessionPrefs.setScreenEffects(this@SessionActivity, e); effects = e
|
||||
e.push(upscaler)
|
||||
},
|
||||
onTextureAnisotropy = { v -> SessionPrefs.setTextureAnisotropy(this@SessionActivity, v); textureAnisotropy = v },
|
||||
onTextureLodBias = { v -> SessionPrefs.setTextureLodBias(this@SessionActivity, v); textureLodBias = v },
|
||||
onFrameGenPick = { mode ->
|
||||
FrameGen.set(this@SessionActivity, mode)
|
||||
readPrefs()
|
||||
applyFrameGen()
|
||||
},
|
||||
onImportLossless = {
|
||||
pickLossless.launch(InAppFilePicker.buildIntent(
|
||||
this@SessionActivity, listOf("dll"), getString(R.string.lsfg_pick_title)))
|
||||
},
|
||||
onKeyboard = { drawerOpen = false; pcKeyboardOpen = false; keyboard?.toggle() },
|
||||
onHardwareKeyboard = { drawerOpen = false; pcKeyboardOpen = !pcKeyboardOpen },
|
||||
onHardwareKeyboard = ::togglePcKeyboard,
|
||||
controller = controllerSettings,
|
||||
onRumble = { on -> updateControllerPrefs { ControllerPrefs.setRumble(this@SessionActivity, on) } },
|
||||
onSteamButton = { on -> updateControllerPrefs { ControllerPrefs.setSteamButton(this@SessionActivity, on) } },
|
||||
onQamButton = { on -> updateControllerPrefs { ControllerPrefs.setQamButton(this@SessionActivity, on) } },
|
||||
onKeyboardButton = { on -> updateControllerPrefs { ControllerPrefs.setKeyboardButton(this@SessionActivity, on) } },
|
||||
onSteamMenu = if (SessionState.mode == SessionService.MODE_STEAM) ({ sendSteamGuide() }) else null,
|
||||
onQam = if (SessionState.mode == SessionService.MODE_STEAM) ({ triggerSteamQam() }) else null,
|
||||
// The desktop with Steam's desktop client in it, in this session's place: the
|
||||
// session ends with status 0 and onSessionEnded starts the relaunch (Stop's
|
||||
// finish() would skip it).
|
||||
onSwitchToDesktop = if (SessionState.mode == SessionService.MODE_STEAM) ({
|
||||
drawerOpen = false
|
||||
SessionState.relaunch = Intent(this@SessionActivity, SessionActivity::class.java)
|
||||
.putExtra(SessionService.EXTRA_MODE, SessionService.MODE_DESKTOP)
|
||||
.putExtra(SessionService.EXTRA_STEAM_UI, "desktop")
|
||||
SessionService.stop(this@SessionActivity)
|
||||
}) else null,
|
||||
backActionsInverted = backActionsInverted,
|
||||
onBackActionsInverted = { inverted ->
|
||||
SessionPrefs.setBackActionsInverted(this@SessionActivity, inverted)
|
||||
@@ -414,7 +542,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
Toast.makeText(this@SessionActivity, "Could not open ${app.label} on $target", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
onBackground = { drawerOpen = false; moveTaskToBack(true) },
|
||||
pipSupported = com.droiddeck.launcher.session.SessionPipController.supported(this@SessionActivity),
|
||||
pipAutoEnter = pipAutoEnter,
|
||||
onPip = { pip.enter() },
|
||||
onPipAutoEnter = { on ->
|
||||
SessionPrefs.setPipAutoEnter(this@SessionActivity, on)
|
||||
pipAutoEnter = on
|
||||
pip.refresh(true)
|
||||
},
|
||||
onBackground = { drawerOpen = false; pip.background(); moveTaskToBack(true) },
|
||||
onShareLogs = { drawerOpen = false; shareCurrentSessionLogs() },
|
||||
onStop = { drawerOpen = false; SessionService.stop(this@SessionActivity); finish() },
|
||||
onStopFrom = { onScreen ->
|
||||
@@ -435,6 +571,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
title = pausedTitle(),
|
||||
onResume = { SessionService.resume(this@SessionActivity) },
|
||||
)
|
||||
if (leaving) com.droiddeck.launcher.ui.FloodSpread(
|
||||
com.droiddeck.launcher.ui.LocalPalette.current.signal,
|
||||
ball = if (loading.visible) com.droiddeck.launcher.ui.ThrobberSpot.ball else null,
|
||||
) { completeLeave() }
|
||||
quitFrom?.let { from ->
|
||||
val error = androidx.compose.material3.MaterialTheme.colorScheme.error
|
||||
com.droiddeck.launcher.ui.LaunchFlood(
|
||||
@@ -445,6 +585,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshHomeApp() {
|
||||
@@ -465,6 +607,19 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
padBridge?.triggerQam()
|
||||
}
|
||||
|
||||
private fun togglePcKeyboard() {
|
||||
drawerOpen = false
|
||||
keyboard?.hide()
|
||||
onScreenControls?.releaseAll()
|
||||
pcKeyboardOpen = !pcKeyboardOpen
|
||||
}
|
||||
|
||||
private fun updateControllerPrefs(change: () -> Unit) {
|
||||
change()
|
||||
controllerSettings = ControllerPrefs.read(this)
|
||||
onScreenControls?.reload()
|
||||
}
|
||||
|
||||
private fun routeBackAction() {
|
||||
pendingBackAction?.let { pending ->
|
||||
uiHandler.removeCallbacks(pending)
|
||||
@@ -477,8 +632,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
performBackAction(double = false)
|
||||
}
|
||||
pendingBackAction = pending
|
||||
// Android's own double-tap window (300 ms): the single action waits it out, so it is felt.
|
||||
uiHandler.postDelayed(pending, android.view.ViewConfiguration.getDoubleTapTimeout().toLong())
|
||||
// Wait 500 ms for a second Back press before performing the single-press action.
|
||||
uiHandler.postDelayed(pending, 500L)
|
||||
}
|
||||
|
||||
private fun performBackAction(double: Boolean) {
|
||||
@@ -551,7 +706,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun shareCurrentSessionLogs() {
|
||||
val folder = SessionPaths.current()
|
||||
// Once a session has ended its folder has been handed on (SessionPaths.take), and the
|
||||
// ended screen's Share logs is for that one.
|
||||
val folder = SessionPaths.current() ?: SessionPaths.lastEnded()
|
||||
if (folder == null || !folder.isDirectory) {
|
||||
Toast.makeText(this, "No logs for this session.", Toast.LENGTH_LONG).show()
|
||||
return
|
||||
@@ -575,12 +732,27 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun readPrefs() {
|
||||
pipAutoEnter = SessionPrefs.pipAutoEnter(this)
|
||||
hudOn = SessionPrefs.hudEnabled(this)
|
||||
fillScreen = SessionPrefs.forceFullscreen(this)
|
||||
touchMode = SessionPrefs.touchMode(this)
|
||||
frameGenLabel = FrameGen.label(this)
|
||||
frameGenEngine = FrameGen.engine(this)
|
||||
frameGenMultiplier = FrameGen.multiplier(this)
|
||||
upscaler = SessionPrefs.upscaler(this)
|
||||
upscaleSharpness = SessionPrefs.upscaleSharpness(this)
|
||||
effects = SessionPrefs.screenEffects(this)
|
||||
textureAnisotropy = SessionPrefs.textureAnisotropy(this)
|
||||
textureLodBias = SessionPrefs.textureLodBias(this)
|
||||
val nextTouchMode = SessionPrefs.touchMode(this)
|
||||
if (touchMode != nextTouchMode) {
|
||||
if (::touchpad.isInitialized) touchpad.cancel()
|
||||
if (CompositorHost.isStarted) {
|
||||
WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
|
||||
listOf(PointerGestures.BTN_LEFT, PointerGestures.BTN_RIGHT, PointerGestures.BTN_MIDDLE).forEach {
|
||||
WaylandCompositor.nativeSendSceneInput(3, it, 0)
|
||||
}
|
||||
}
|
||||
if (nextTouchMode == SessionPrefs.TOUCH_OFF) cursorVisible = false
|
||||
}
|
||||
touchMode = nextTouchMode
|
||||
frameGen = FrameGen.mode(this)
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
suspendPolicy = SessionPrefs.suspendPolicy(this, SessionState.mode)
|
||||
oscMode = SessionPrefs.oscMode(this)
|
||||
@@ -688,7 +860,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
?: return "Could not reach the runtime catalog. Check the connection and press Play again."
|
||||
val ok = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.install(this, release,
|
||||
progressFor("the Linux runtime", release.size / 1_000_000))
|
||||
return if (ok) null else "The Linux runtime did not install. Check the connection and press Play again."
|
||||
return if (ok) null else "The Linux runtime did not install. Check the runtime in Setup and press Play again."
|
||||
}
|
||||
|
||||
/** Null when the desktop package is in, else the loading screen's closing line. */
|
||||
@@ -732,9 +904,20 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val size = if (SessionState.running) SessionState.outputSize else outputSize()
|
||||
SessionState.outputSize = size
|
||||
if (!SessionState.running) SessionState.refreshHz = refreshHz()
|
||||
// How far the panel enlarges the session, for texture sharpness "Auto" (TextureFiltering):
|
||||
// the game environment is published again so the next launch derives it from this session.
|
||||
val panel = panelBounds()
|
||||
SessionState.upscaleRatio = maxOf(panel.width(), panel.height()).toFloat() / maxOf(size.first, size.second).coerceAtLeast(1)
|
||||
Thread({
|
||||
runCatching { GameEnvironmentStore.publish(this) }
|
||||
.onFailure { Log.e(TAG, "Could not update game environment", it) }
|
||||
}, "game-env-publish").start()
|
||||
// Letterbox, never stretch or crop: the output can be a different shape from the panel,
|
||||
// and a game's picture must keep its proportions with bars, not lose its edges.
|
||||
WaylandCompositor.nativeSetScaleMode(SCALE_FIT, ALIGN_CENTER)
|
||||
WaylandCompositor.nativeSetUpscaler(SessionPrefs.upscaler(this))
|
||||
WaylandCompositor.nativeSetUpscaleSharpness(SessionPrefs.upscaleSharpness(this))
|
||||
SessionPrefs.screenEffects(this).push(SessionPrefs.upscaler(this))
|
||||
// The session's folder, claimed here because the compositor starts before the service and
|
||||
// opens its log once. The compositor reads the path from its environment; setting it after
|
||||
// it has started changes nothing, which is why the service copies the file in at teardown.
|
||||
@@ -762,7 +945,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val on = wanted && probe.reason == null
|
||||
SessionState.hdr = on
|
||||
if (on) {
|
||||
try { android.system.Os.setenv("BANNER_WAYLAND_HDR", "1", true) } catch (e: Exception) { Log.w(TAG, "BANNER_WAYLAND_HDR", e) }
|
||||
try { android.system.Os.setenv("DROIDDECK_WAYLAND_HDR", "1", true) } catch (e: Exception) { Log.w(TAG, "DROIDDECK_WAYLAND_HDR", e) }
|
||||
WaylandCompositor.nativeSetZeroCopy(true)
|
||||
}
|
||||
WaylandCompositor.nativeSetHdrRequest(
|
||||
@@ -775,7 +958,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
CompositorHost.startOrAttach(
|
||||
holder.surface, runtimeDir.path,
|
||||
driverId?.let { turnip.driverPath(it) }, driverId?.let { turnip.libraryName(it) },
|
||||
applicationInfo.nativeLibraryDir, size.first, size.second, refreshHz(),
|
||||
applicationInfo.nativeLibraryDir, size.first, size.second, refreshHz(), SessionState.fpsLimit,
|
||||
)
|
||||
if (!SessionState.running) SessionEvents.record("compositor.started")
|
||||
// The service owns everything below the compositor. It is started whenever no session is
|
||||
@@ -783,7 +966,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// whole process, so the second Play after a session ended used to re-attach the Surface,
|
||||
// start nothing, and leave the loading panel counting up over a dead session.
|
||||
if (!SessionState.running) {
|
||||
CompositorHost.newSession()
|
||||
SessionEvents.transition(SessionPhase.STARTING_GUEST, "guest.starting")
|
||||
SessionService.start(
|
||||
this, intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM,
|
||||
@@ -819,17 +1001,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
* The size the session renders at. gamescope is told this and scales its output onto whatever
|
||||
* the panel is, so a 1440p phone can run the client at 1080p without the client knowing.
|
||||
*/
|
||||
/** The panel, not the window: resources.displayMetrics is what is left after the system bars and the cutout are taken out. */
|
||||
private fun panelBounds(): android.graphics.Rect = if (Build.VERSION.SDK_INT >= 30) {
|
||||
windowManager.maximumWindowMetrics.bounds
|
||||
} else {
|
||||
val metrics = android.util.DisplayMetrics()
|
||||
@Suppress("DEPRECATION")
|
||||
windowManager.defaultDisplay.getRealMetrics(metrics)
|
||||
android.graphics.Rect(0, 0, metrics.widthPixels, metrics.heightPixels)
|
||||
}
|
||||
|
||||
private fun outputSize(): Pair<Int, Int> {
|
||||
// The panel, not the window: resources.displayMetrics is what is left after the system
|
||||
// bars and the cutout are taken out.
|
||||
val bounds = if (Build.VERSION.SDK_INT >= 30) {
|
||||
windowManager.maximumWindowMetrics.bounds
|
||||
} else {
|
||||
val metrics = android.util.DisplayMetrics()
|
||||
@Suppress("DEPRECATION")
|
||||
windowManager.defaultDisplay.getRealMetrics(metrics)
|
||||
android.graphics.Rect(0, 0, metrics.widthPixels, metrics.heightPixels)
|
||||
}
|
||||
val bounds = panelBounds()
|
||||
val panelW = maxOf(bounds.width(), bounds.height()).toFloat()
|
||||
val panelH = minOf(bounds.width(), bounds.height()).toFloat()
|
||||
// Never narrower than 16:9. A foldable's inner panel is nearly square, and a game handed a
|
||||
@@ -884,10 +1067,35 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val hz = refreshHz()
|
||||
Thread({
|
||||
val problem = FrameGen.apply(this, hz)
|
||||
if (problem != null) runOnUiThread { Toast.makeText(this, problem, Toast.LENGTH_LONG).show() }
|
||||
val state = Lossless.state(this)
|
||||
runOnUiThread {
|
||||
lossless = state
|
||||
if (problem != null) Toast.makeText(this, problem, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "frame-gen").start()
|
||||
}
|
||||
|
||||
/** A Lossless Scaling installed or updated in Steam during the session is taken when the drawer opens. */
|
||||
private fun syncLossless() {
|
||||
Thread({
|
||||
Lossless.sync(this)
|
||||
val state = Lossless.state(this)
|
||||
runOnUiThread { lossless = state }
|
||||
}, "lossless-sync").start()
|
||||
}
|
||||
|
||||
private fun importLossless(dll: File) {
|
||||
Thread({
|
||||
val message = Lossless.message(this, Lossless.import(this, dll))
|
||||
val state = Lossless.state(this)
|
||||
runOnUiThread {
|
||||
lossless = state
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
if (FrameGen.engine(this) == FrameGen.ENGINE_LSFG) applyFrameGen()
|
||||
}
|
||||
}, "lossless-import").start()
|
||||
}
|
||||
|
||||
// ── Session state ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Keeps the loading overlay current from the session log, and its clock moving. */
|
||||
@@ -897,6 +1105,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val poll = object : Runnable {
|
||||
override fun run() {
|
||||
if (!watching) return
|
||||
if (::pip.isInitialized) pip.refresh()
|
||||
if (SessionState.stopRequested && !SessionState.running && !installingRuntime) {
|
||||
SessionState.stopRequested = false
|
||||
SessionEvents.transition(SessionPhase.IDLE, "session.cancelled")
|
||||
@@ -915,12 +1124,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun onSessionEnded(status: Int) {
|
||||
if (isInPictureInPictureMode) {
|
||||
runOnUiThread { finish() }
|
||||
return
|
||||
}
|
||||
closeSecondScreen(reset = true)
|
||||
if (status == 0) {
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed) return@runOnUiThread
|
||||
// A session asked for in this one's place (the desktop's Steam launchers, the
|
||||
// drawer's DESKTOP) starts here: this activity restarts on its intent, as an agent
|
||||
// A session asked for in this one's place (the desktop's Steam launchers or
|
||||
// Steam's Switch to Desktop) starts here: this activity restarts on its intent, as an agent
|
||||
// start does. startActivity(next) could not do it - the activity is singleTop, so
|
||||
// the request landed in this instance's onNewIntent and went with its finish().
|
||||
SessionState.relaunch?.let { next ->
|
||||
@@ -943,14 +1156,19 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}, "session-end-hint").start()
|
||||
}
|
||||
|
||||
private fun showEnded(status: Int, hint: String?) {
|
||||
if (status == 0) {
|
||||
private fun showEnded(status: Int, hint: EndHint?) {
|
||||
if (status == 0 && hint?.evenOnSuccess != true) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
// It stays until the user leaves: a failure read against a timer is a failure not read.
|
||||
val what = when (SessionState.mode) {
|
||||
SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: "The program"
|
||||
SessionService.MODE_DESKTOP -> "The desktop"
|
||||
else -> "Steam"
|
||||
}
|
||||
loading.showEnded(
|
||||
hint ?: "Steam stopped unexpectedly. Share the logs with a bug report, or try again.",
|
||||
hint?.text ?: "$what stopped unexpectedly. Share the logs with a bug report, or try again.",
|
||||
"Exit status $status · ${SessionState.logFile?.path ?: "no log"}",
|
||||
)
|
||||
focusEndedScreen()
|
||||
@@ -966,6 +1184,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
/** The same start again, in a fresh activity: the ended one keeps no session to reuse. */
|
||||
private fun retrySession() {
|
||||
val again = Intent(intent)
|
||||
closeAtOnce = true
|
||||
finish()
|
||||
startActivity(again)
|
||||
}
|
||||
@@ -977,10 +1196,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
* FEX) on some devices (a Fold 5, twice). The switch that answers it is in Performance, and a
|
||||
* user who never opens the log would not know.
|
||||
*/
|
||||
private fun sessionEndHint(log: File?): String? {
|
||||
private class EndHint(val text: String, val evenOnSuccess: Boolean = false)
|
||||
|
||||
private fun sessionEndHint(log: File?): EndHint? {
|
||||
if (log == null || !log.isFile) return null
|
||||
return try {
|
||||
var enosys = 0
|
||||
var fexMissing = false
|
||||
// The tail is where a dying session says why; 512 KB covers the storm without reading a 1 GB log.
|
||||
val size = log.length()
|
||||
java.io.RandomAccessFile(log, "r").use { f ->
|
||||
@@ -990,11 +1212,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
f.readFully(bytes)
|
||||
String(bytes, Charsets.ISO_8859_1).lineSequence().forEach { line ->
|
||||
if (line.contains("Function not implemented")) enosys++
|
||||
if (line.contains(FEX_MISSING)) fexMissing = true
|
||||
}
|
||||
}
|
||||
if (enosys >= 8) {
|
||||
"The log shows $enosys \"Function not implemented\" errors: proot's seccomp acceleration is failing a helper on this device. " +
|
||||
"Try Performance \u2192 \"Run proot without seccomp\" (or \"Skip Steam's xalia helper\") and start again."
|
||||
if (fexMissing) {
|
||||
val what = com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: getString(R.string.session_end_program)
|
||||
EndHint(getString(R.string.session_end_fex_missing, what), evenOnSuccess = true)
|
||||
} else if (enosys >= 8) {
|
||||
EndHint("The log shows $enosys \"Function not implemented\" errors: proot's seccomp acceleration is failing a helper on this device. " +
|
||||
"Try Performance \u2192 \"Run proot without seccomp\" (or \"Skip Steam's xalia helper\") and start again.")
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -1010,6 +1236,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE ->
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
if (pipUi) return true
|
||||
val fromController = event.device != null && PadBridge.isFromController(event.device)
|
||||
if (fromController && event.action == KeyEvent.ACTION_DOWN) {
|
||||
if (drawerOpen && !drawerControllerActive) sessionOverlay.requestFocus()
|
||||
@@ -1049,7 +1276,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
return true
|
||||
}
|
||||
val resumeKey = event.keyCode == KeyEvent.KEYCODE_BUTTON_A || event.keyCode == KeyEvent.KEYCODE_BUTTON_START
|
||||
val resumeKey = event.keyCode == KeyEvent.KEYCODE_BUTTON_A || event.keyCode == KeyEvent.KEYCODE_BUTTON_START ||
|
||||
event.keyCode == KeyEvent.KEYCODE_BUTTON_MODE || event.keyCode == KeyEvent.KEYCODE_HOME
|
||||
val resumeKeyId = event.deviceId to event.keyCode
|
||||
if (fromController && resumeKey && (SessionState.suspended || resumeKeyId in resumeKeysDown)) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
@@ -1088,11 +1316,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (evdev <= 0 && event.scanCode > 0) evdev = event.scanCode
|
||||
if (evdev > 0) {
|
||||
if (down) {
|
||||
if (shiftEvdev != 0) WaylandCompositor.nativeSendKey(shiftEvdev, 1)
|
||||
WaylandCompositor.nativeSendKey(evdev, 1)
|
||||
if (shiftEvdev != 0) sendGuestKey(shiftEvdev, true)
|
||||
sendGuestKey(evdev, true)
|
||||
} else {
|
||||
WaylandCompositor.nativeSendKey(evdev, 0)
|
||||
if (shiftEvdev != 0) WaylandCompositor.nativeSendKey(shiftEvdev, 0)
|
||||
sendGuestKey(evdev, false)
|
||||
if (shiftEvdev != 0) sendGuestKey(shiftEvdev, false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1102,6 +1330,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
|
||||
if (pipUi) return true
|
||||
if (drawerOpen && event.device != null && PadBridge.isFromController(event.device)) {
|
||||
if (!drawerControllerActive) sessionOverlay.requestFocus()
|
||||
drawerControllerActive = true
|
||||
@@ -1120,6 +1349,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
||||
if (pipUi) return true
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN && event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
|
||||
drawerControllerActive = false
|
||||
}
|
||||
@@ -1251,6 +1481,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
*/
|
||||
private fun syncClientCursor(): Boolean {
|
||||
if (!CompositorHost.isStarted) return false
|
||||
// Asked on every pointer move; the image is copied only when the client has changed it.
|
||||
if (WaylandCompositor.nativeCursorSerial() == cursorSerial) return false
|
||||
val n = WaylandCompositor.nativeCursorSnapshot(cursorBuf)
|
||||
if (n < 6) return false
|
||||
val serial = cursorBuf[0]
|
||||
@@ -1319,6 +1551,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun showSecondScreen(mode: SecondScreenMode) {
|
||||
if (pipUi || !started) return
|
||||
val target = displayManager.getDisplay(selectedSecondScreenDisplay)
|
||||
if (target == null || !target.isValid || (target.flags and Display.FLAG_PRESENTATION) == 0) {
|
||||
// Not there right now - a panel still waking, say. The choice is kept; the display
|
||||
@@ -1404,6 +1637,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE)) return onMouse(event)
|
||||
// Child views get the event first, so menus, keyboards and the on-screen pad still work.
|
||||
if (touchMode == SessionPrefs.TOUCH_OFF) return true
|
||||
if (usingTouchpad()) {
|
||||
val rect = drawnRect() ?: return false
|
||||
if (touchpad.bounds != rect) {
|
||||
@@ -1424,6 +1659,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// the compositor still holds (an up lost to the drawer opening mid-touch) goes first.
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
|
||||
listOf(PointerGestures.BTN_LEFT, PointerGestures.BTN_RIGHT, PointerGestures.BTN_MIDDLE).forEach {
|
||||
WaylandCompositor.nativeSendSceneInput(3, it, 0)
|
||||
}
|
||||
sendTouch(0, event.actionIndex)
|
||||
}
|
||||
MotionEvent.ACTION_POINTER_DOWN -> sendTouch(0, event.actionIndex)
|
||||
@@ -1461,12 +1699,25 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
/** The mode decides which controls appear; `droiddeck-osc` in Downloads still overrides. */
|
||||
/** Which controller the session has, so a pad report says what was plugged in and when. */
|
||||
private fun logInputDevice(what: String, deviceId: Int) {
|
||||
val d = InputDevice.getDevice(deviceId) ?: return
|
||||
if (!PadBridge.isFromController(d)) return
|
||||
Log.i(TAG, String.format(java.util.Locale.ROOT, "controller %s: \"%s\" (%04x:%04x, id %d, sources 0x%x)",
|
||||
what, d.name, d.vendorId, d.productId, d.id, d.sources))
|
||||
}
|
||||
|
||||
private fun updateOnScreenControls() {
|
||||
val forced = File(Environment.getExternalStorageDirectory(), "Download/droiddeck-osc")
|
||||
.takeIf { it.isFile }
|
||||
?.let { FileUtils.readString(it)?.trim()?.lowercase() }
|
||||
?: SessionPrefs.oscMode(this)
|
||||
val controls = onScreenControls ?: return
|
||||
if (pipUi) {
|
||||
controls.releaseAll()
|
||||
controls.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
val buttonsOnly = forced == SessionPrefs.OSC_STEAM_QAM
|
||||
controls.setButtonsOnly(buttonsOnly)
|
||||
val show = when (forced) {
|
||||
@@ -1488,10 +1739,81 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
})
|
||||
}
|
||||
|
||||
private fun sendGuestKey(code: Int, down: Boolean) {
|
||||
if (down && pipUi) return
|
||||
if (down) guestKeysDown.add(code) else guestKeysDown.remove(code)
|
||||
WaylandCompositor.nativeSendKey(code, if (down) 1 else 0)
|
||||
}
|
||||
|
||||
private fun pipBounds(): android.graphics.Rect {
|
||||
val rect = android.graphics.Rect()
|
||||
if (!::surfaceView.isInitialized || !surfaceView.getGlobalVisibleRect(rect)) return rect
|
||||
val (w, h) = SessionState.outputSize
|
||||
if (w <= 0 || h <= 0) return rect
|
||||
val scale = minOf(rect.width().toFloat() / w, rect.height().toFloat() / h)
|
||||
val fittedW = (w * scale).toInt()
|
||||
val fittedH = (h * scale).toInt()
|
||||
rect.inset((rect.width() - fittedW) / 2, (rect.height() - fittedH) / 2)
|
||||
return rect
|
||||
}
|
||||
|
||||
private fun showPipUi(show: Boolean) {
|
||||
pipUi = show
|
||||
if (show) {
|
||||
drawerOpen = false
|
||||
pcKeyboardOpen = false
|
||||
pendingBackAction?.let(uiHandler::removeCallbacks)
|
||||
pendingBackAction = null
|
||||
releaseDrawerDirection()
|
||||
onScreenControls?.releaseAll()
|
||||
padBridge?.releaseAll()
|
||||
touchpad.cancel()
|
||||
guestKeysDown.toList().forEach { sendGuestKey(it, false) }
|
||||
keyboard?.hide()
|
||||
sessionClipboard.stop()
|
||||
padMotion?.stop()
|
||||
cursorVisible = false
|
||||
closeSecondScreen(reset = false)
|
||||
onScreenControls?.visibility = View.GONE
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
if (CompositorHost.isStarted) {
|
||||
WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
|
||||
listOf(PointerGestures.BTN_LEFT, PointerGestures.BTN_RIGHT, PointerGestures.BTN_MIDDLE).forEach {
|
||||
WaylandCompositor.nativeSendSceneInput(3, it, 0)
|
||||
}
|
||||
WaylandCompositor.nativeSetScaleMode(SCALE_FIT, ALIGN_CENTER)
|
||||
}
|
||||
} else if (!isFinishing && !isDestroyed) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
updateOnScreenControls()
|
||||
if (resumed) { sessionClipboard.start(); updatePadMotion() }
|
||||
if (started) refreshSecondScreenDisplays()
|
||||
if (CompositorHost.isStarted) applyFrameGen()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
if (::pip.isInitialized) pip.leaving()
|
||||
super.onUserLeaveHint()
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(inPip: Boolean, newConfig: android.content.res.Configuration) {
|
||||
super.onPictureInPictureModeChanged(inPip, newConfig)
|
||||
if (::pip.isInitialized) pip.changed(inPip)
|
||||
}
|
||||
|
||||
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
if (::pip.isInitialized && (requestCode >= 0 || intent.component?.packageName == packageName ||
|
||||
intent.action in setOf(Intent.ACTION_CHOOSER, Intent.ACTION_SEND, Intent.ACTION_OPEN_DOCUMENT,
|
||||
Intent.ACTION_GET_CONTENT, "android.settings.PICTURE_IN_PICTURE_SETTINGS"))) pip.background()
|
||||
super.startActivityForResult(intent, requestCode, options)
|
||||
}
|
||||
|
||||
// ── Lifecycle ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (runtimeRemovalBlocked) return
|
||||
started = true
|
||||
displayManager.registerDisplayListener(displayListener, Handler(Looper.getMainLooper()))
|
||||
refreshSecondScreenDisplays()
|
||||
@@ -1500,6 +1822,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
if (runtimeRemovalBlocked) { super.onStop(); return }
|
||||
started = false
|
||||
displayManager.unregisterDisplayListener(displayListener)
|
||||
// Closed while the session is out of sight (sleep, a closed lid, another app), and kept:
|
||||
@@ -1531,6 +1854,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (runtimeRemovalBlocked) return
|
||||
com.droiddeck.launcher.ui.Motion.refresh(this)
|
||||
refreshHomeApp()
|
||||
if (intent?.action == SessionService.ACTION_RESUME) {
|
||||
@@ -1540,14 +1864,19 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
(getSystemService(INPUT_SERVICE) as? InputManager)
|
||||
?.registerInputDeviceListener(deviceListener, Handler(Looper.getMainLooper()))
|
||||
onScreenControls?.reload()
|
||||
controllerSettings = ControllerPrefs.read(this)
|
||||
updateOnScreenControls()
|
||||
resumed = true
|
||||
if (!pipUi) sessionClipboard.start()
|
||||
updatePadMotion()
|
||||
readPrefs()
|
||||
if (::pip.isInitialized) pip.resumed()
|
||||
if (CompositorHost.isStarted) applyFrameGen()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (runtimeRemovalBlocked) { super.onPause(); return }
|
||||
sessionClipboard.stop()
|
||||
// Do not carry transient session UI across an app/display transition. In particular, the
|
||||
// drawer's dim layer can otherwise remain over Steam when this activity returns.
|
||||
drawerOpen = false
|
||||
@@ -1572,7 +1901,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) goFullscreen()
|
||||
if (runtimeRemovalBlocked) return
|
||||
if (hasFocus && !pipUi) {
|
||||
goFullscreen()
|
||||
refreshClipboard()
|
||||
}
|
||||
}
|
||||
|
||||
/** The keyboard/trackpad presentation can own focus instead of the main display. */
|
||||
fun refreshClipboard() {
|
||||
if (resumed) sessionClipboard.refresh()
|
||||
}
|
||||
|
||||
private fun goFullscreen() {
|
||||
@@ -1602,13 +1940,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (runtimeRemovalBlocked) { super.onDestroy(); return }
|
||||
// Deliberately does NOT end the session: this activity can be destroyed while the user is
|
||||
// in another app, and the whole point of the service is that Steam survives that.
|
||||
watching = false
|
||||
releaseDrawerDirection()
|
||||
pendingBackAction?.let(uiHandler::removeCallbacks)
|
||||
pendingBackAction = null
|
||||
closeSecondScreen(reset = true)
|
||||
if (::pip.isInitialized) pip.destroyed()
|
||||
closeSecondScreen(reset = false)
|
||||
if (::hud.isInitialized) hud.stop()
|
||||
padBridge?.stop()
|
||||
padMotion?.stop()
|
||||
@@ -1629,15 +1969,43 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
finish()
|
||||
}
|
||||
|
||||
/** Leaving the session sinks its surface back down onto the front end. */
|
||||
/**
|
||||
* Every way out on screen (backing out of the loading screen, closing the ended screen, the
|
||||
* session ending) leaves as a session arrives, in reverse: the blue spreads out of the
|
||||
* throbber's ball, or the middle of the screen once a game is up, and the front end opens on it
|
||||
* and draws it back into the button the session came from. Off screen, or with animations
|
||||
* off, it just closes.
|
||||
*/
|
||||
override fun finish() {
|
||||
if (!quitFlooded && !closeAtOnce && com.droiddeck.launcher.ui.Motion.scale != 0f &&
|
||||
lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) {
|
||||
if (!leaving) {
|
||||
leaving = true
|
||||
// Should the flood never get its frames, the session still closes.
|
||||
uiHandler.postDelayed({ completeLeave() }, LEAVE_TIMEOUT_MS)
|
||||
}
|
||||
return
|
||||
}
|
||||
super.finish()
|
||||
if (quitFlooded) overridePendingTransition(0, 0)
|
||||
else overridePendingTransition(R.anim.session_hold, R.anim.session_sink)
|
||||
}
|
||||
|
||||
/** The screen is all blue: close onto the front end, which opens on the same blue. */
|
||||
private fun completeLeave() {
|
||||
if (isFinishing) return
|
||||
val signal = com.droiddeck.launcher.ui.Themes.byId(SessionPrefs.theme(this)).signal
|
||||
com.droiddeck.launcher.ui.QuitFlood.mark(signal.toArgb())
|
||||
window.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(signal.toArgb()))
|
||||
quitFlooded = true
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** How long the leaving flood may take before the session closes without it. */
|
||||
private const val LEAVE_TIMEOUT_MS = 1_500L
|
||||
private const val TAG = "SessionActivity"
|
||||
private const val FEX_MISSING = "droiddeck-fex: x86 Linux programs need"
|
||||
private const val UNBUFFERED_SOURCES = InputDevice.SOURCE_CLASS_JOYSTICK or InputDevice.SOURCE_CLASS_TRACKBALL or InputDevice.SOURCE_CLASS_POSITION
|
||||
private const val CURSOR_PAD_HOLD_MS = 1200L
|
||||
private const val DRAWER_HAT_THRESHOLD = 0.5f
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -113,19 +114,24 @@ object DeviceReport {
|
||||
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)
|
||||
|
||||
@@ -145,13 +151,20 @@ object DeviceReport {
|
||||
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))
|
||||
@@ -168,7 +181,7 @@ object DeviceReport {
|
||||
|
||||
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)")
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ object GameEnvironment {
|
||||
|
||||
private val namePattern = Regex("[A-Za-z_][A-Za-z0-9_]*")
|
||||
private val unused = setOf(
|
||||
"WINEESYNC", "WINENTSYNC", "WINE_FAST_YIELD", "WINE_DO_NOT_CREATE_DXGI_DEVICE_MANAGER",
|
||||
"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",
|
||||
@@ -29,6 +29,12 @@ object GameEnvironment {
|
||||
"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('=')) }
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ object GameEnvironmentOptions {
|
||||
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),
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import java.util.zip.CRC32
|
||||
/** Windows game folders shared with the Steam session. */
|
||||
object AddedGames {
|
||||
private const val TAG = "AddedGames"
|
||||
private const val LIBRARY = "/mnt/droiddeck-sd"
|
||||
private const val LEGACY_LIBRARY = "/mnt/bannerlator-sd"
|
||||
|
||||
class Game(
|
||||
val folder: File, val name: String, val exe: File, val guestExe: String, val guestDir: String,
|
||||
@@ -65,7 +67,7 @@ object AddedGames {
|
||||
if (path.startsWith("$roms/")) return "/root/ROMs/" + path.removePrefix("$roms/")
|
||||
}
|
||||
GameStorage.effective(context)?.let { lib ->
|
||||
if (path.startsWith("${lib.path}/")) return "/mnt/bannerlator-sd/" + path.removePrefix("${lib.path}/")
|
||||
if (path.startsWith("${lib.path}/")) return "$LIBRARY/" + path.removePrefix("${lib.path}/")
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -94,11 +96,33 @@ object AddedGames {
|
||||
)
|
||||
for (dir in folders.distinctBy { it.absolutePath }) {
|
||||
if (!dir.isDirectory) { Log.w(TAG, "$dir is not a folder; skipped"); continue }
|
||||
for (folder in dir.listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() } ?: emptyList()) scanGame(context, folder, out)
|
||||
val steamInstalls = steamInstallDirs(dir)
|
||||
for (folder in dir.listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() } ?: emptyList()) {
|
||||
if (folder.name.lowercase() in steamInstalls) continue
|
||||
scanGame(context, folder, out)
|
||||
}
|
||||
}
|
||||
return out.distinctBy { it.folder.canonicalPath }
|
||||
}
|
||||
|
||||
/**
|
||||
* The folders under a library's steamapps/common that a manifest beside it already claims.
|
||||
* Steam lists those itself, so a shortcut would only add a non-Steam copy of the game. Steam
|
||||
* may lowercase installdir on Android's case-insensitive storage, so names compare lowercased.
|
||||
*/
|
||||
internal fun steamInstallDirs(dir: File): Set<String> {
|
||||
val steamapps = dir.parentFile?.takeIf { dir.name == "common" && it.name == "steamapps" } ?: return emptySet()
|
||||
return steamapps.listFiles { f -> f.isFile && f.name.startsWith("appmanifest_") && f.name.endsWith(".acf") }
|
||||
.orEmpty()
|
||||
.mapNotNull { manifest ->
|
||||
runCatching { INSTALL_DIR.find(manifest.readText())?.groupValues?.get(1)?.trim()?.lowercase() }.getOrNull()
|
||||
}
|
||||
.filter { it.isNotEmpty() }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
private val INSTALL_DIR = Regex("\"installdir\"\\s*\"([^\"]*)\"", RegexOption.IGNORE_CASE)
|
||||
|
||||
private fun scanGame(context: Context, folder: File, out: MutableList<Game>) {
|
||||
run {
|
||||
val candidates = candidates(folder)
|
||||
@@ -108,7 +132,8 @@ object AddedGames {
|
||||
if (guestExe == null) { Log.w(TAG, "${folder.name}: the session cannot see ${exe.path}"); return }
|
||||
val guestDir = guestPath(context, exe.parentFile ?: folder) ?: return
|
||||
val name = folder.name
|
||||
val crc = CRC32().apply { update(("\"$guestExe\"" + name).toByteArray()) }.value
|
||||
// Keyed by the pre-rename path so shortcut ids, and the prefixes and saves under them, stay put.
|
||||
val crc = CRC32().apply { update(("\"${guestExe.replaceFirst(Regex("^$LIBRARY/"), "$LEGACY_LIBRARY/")}\"" + name).toByteArray()) }.value
|
||||
val appId = crc or 0x80000000L
|
||||
val steamRoot = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam")
|
||||
val steamId = steamRoute(steamRoot, appId)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.GameStorage
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/** Runs while DroidDeck's process is alive, including downloads in a background Steam session. */
|
||||
object GameFileSync {
|
||||
private val worker = Executors.newSingleThreadScheduledExecutor { task -> Thread(task, "game-file-sync").apply { isDaemon = true } }
|
||||
private fun prefs(context: Context) = context.getSharedPreferences("game_files", Context.MODE_PRIVATE)
|
||||
fun folder(context: Context): String? = prefs(context).getString("folder", null)
|
||||
|
||||
fun start(context: Context) {
|
||||
val app = context.applicationContext
|
||||
worker.scheduleWithFixedDelay({
|
||||
runCatching { syncConfigured(app) }.onFailure { Log.w("GameFileSync", "Sync failed: ${it.message}") }
|
||||
}, 0, 30, TimeUnit.SECONDS)
|
||||
}
|
||||
|
||||
@Synchronized internal fun syncConfigured(context: Context) {
|
||||
val path = folder(context) ?: return
|
||||
sync(context, File(path))
|
||||
}
|
||||
|
||||
@Synchronized fun enable(context: Context, folder: File) {
|
||||
sync(context, folder)
|
||||
prefs(context).edit().putString("folder", folder.absolutePath).apply()
|
||||
}
|
||||
|
||||
@Synchronized fun disable(context: Context) {
|
||||
prefs(context).edit().remove("folder").apply()
|
||||
}
|
||||
|
||||
@Synchronized private fun sync(context: Context, folder: File) {
|
||||
check(LinuxRuntime.isInstalled(context)) { "Install the runtime first" }
|
||||
// A disconnected library must not be interpreted as a mass uninstall.
|
||||
check(File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps").isDirectory) { "Steam library is unavailable" }
|
||||
val storage = GameStorage.effective(context)
|
||||
val storageSetting = SessionPrefs.gameStorage(context)
|
||||
val previousStorage = prefs(context).getString("storagePath", null)
|
||||
if (storageSetting.isEmpty() && prefs(context).getString("storageSetting", null) == storageSetting && previousStorage != null) {
|
||||
check(File(previousStorage).isDirectory) { "Previous SD library is unavailable" }
|
||||
}
|
||||
storage?.let { check(File(it.path).listFiles() != null) { "Game storage is unavailable" } }
|
||||
SessionPrefs.addedGamesDirs(context).forEach { check(File(it).listFiles() != null) { "Added games folder is unavailable" } }
|
||||
GameFiles.sync(folder, Library.launchableGames(context, strictRead = true))
|
||||
prefs(context).edit().putString("storageSetting", storageSetting).putString("storagePath", storage?.path).apply()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AtomicFile
|
||||
import androidx.core.util.readText
|
||||
import androidx.core.util.writeBytes
|
||||
import androidx.core.util.writeText
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/** Small, portable launch files for frontends that scan ROM folders. No guest paths or commands. */
|
||||
object GameFiles {
|
||||
const val EXTENSION = "droiddeck"
|
||||
private const val MAX_BYTES = 128
|
||||
private const val INVENTORY = ".droiddeck-sync.json"
|
||||
private const val FORMAT = "droiddeck-game-files-v1"
|
||||
|
||||
/** Called on a worker: content providers and shared storage may block. */
|
||||
fun readIntent(context: Context, request: Intent): String? = runCatching {
|
||||
if (request.action != Intent.ACTION_VIEW) return null
|
||||
val uri = request.data ?: return null
|
||||
GameLaunchLink.parse(uri.toString())?.let { return it }
|
||||
val stream = when (uri.scheme) {
|
||||
"content" -> context.contentResolver.openInputStream(uri)
|
||||
"file" -> {
|
||||
if (!uri.authority.isNullOrEmpty() || uri.query != null || uri.fragment != null) return null
|
||||
File(uri.path ?: return null).inputStream()
|
||||
}
|
||||
null -> File(uri.toString()).takeIf { it.isAbsolute }?.inputStream()
|
||||
else -> null
|
||||
} ?: return null
|
||||
stream.use(::read)
|
||||
}.getOrNull()
|
||||
|
||||
fun read(stream: InputStream): String? {
|
||||
// An untrusted URI must not make us buffer an arbitrary ROM or parse a command.
|
||||
val bytes = ByteArray(MAX_BYTES + 1)
|
||||
var count = 0
|
||||
while (count < bytes.size) {
|
||||
val n = stream.read(bytes, count, bytes.size - count)
|
||||
if (n < 0) break
|
||||
if (n == 0) return null
|
||||
count += n
|
||||
}
|
||||
if (count > MAX_BYTES) return null
|
||||
val text = String(bytes, 0, count, Charsets.UTF_8).trim()
|
||||
return GameLaunchLink.parse(text) ?: text.takeIf(GameLaunchLink::validId)
|
||||
}
|
||||
|
||||
fun filename(game: Library.SteamGame): String {
|
||||
val sanitized = game.name.replace(Regex("[\\p{Cntrl}\\\\/:*?\"<>|]"), "_")
|
||||
.trim().trim('.').ifBlank { "Game" }
|
||||
val points = sanitized.codePoints().limit(40).toArray()
|
||||
val title = String(points, 0, points.size)
|
||||
return "$title (${game.gameIdString}).$EXTENSION"
|
||||
}
|
||||
|
||||
private fun contents(id: String) = GameLaunchLink.uri(id) + "\n"
|
||||
|
||||
@Synchronized fun export(folder: File, game: Library.SteamGame): File {
|
||||
require(folder.isDirectory) { "Folder is unavailable" }
|
||||
val target = child(folder, filename(game))
|
||||
val text = contents(game.gameIdString)
|
||||
require(!target.exists() || matches(target, text)) { "A different file already uses ${target.name}" }
|
||||
if (!target.exists()) write(target, text)
|
||||
exportArt(folder, game)
|
||||
return target
|
||||
}
|
||||
|
||||
/** Cocoon matches icons in its data folder's images directory. Never replace custom art. */
|
||||
private fun exportArt(folder: File, game: Library.SteamGame) {
|
||||
val art = listOfNotNull(game.icon, game.art).firstOrNull {
|
||||
it.isFile && it.extension.lowercase() in listOf("jpg", "jpeg", "png", "webp")
|
||||
} ?: return
|
||||
val title = filename(game).substringBeforeLast(" (${game.gameIdString})")
|
||||
.replace(Regex("\\[[^]]*]|\\([^)]*\\)"), "")
|
||||
.replace(Regex("[^a-zA-Z0-9\\s]"), "").trim()
|
||||
.replace(Regex("\\s+"), "-").lowercase(java.util.Locale.ROOT).take(50).trimEnd('-')
|
||||
if (title.isEmpty()) return
|
||||
val images = child(folder, "images")
|
||||
check(images.isDirectory || images.mkdir()) { "Artwork folder is unavailable" }
|
||||
val target = child(images, "$title-icon.${art.extension.lowercase()}")
|
||||
if (!target.exists()) AtomicFile(target).writeBytes(art.readBytes())
|
||||
}
|
||||
|
||||
/** Only remove files in our inventory whose content is still exactly what we wrote. */
|
||||
@Synchronized fun sync(folder: File, games: List<Library.SteamGame>) {
|
||||
require(folder.isDirectory) { "Folder is unavailable" }
|
||||
val inventory = child(folder, INVENTORY)
|
||||
val previous = if (inventory.exists()) {
|
||||
val record = JSONObject(AtomicFile(inventory).readText())
|
||||
require(record.optString("format") == FORMAT) { "Folder has an unrelated sync inventory" }
|
||||
record.getJSONObject("games")
|
||||
} else JSONObject()
|
||||
val next = JSONObject()
|
||||
val exports = games.distinctBy { it.gameId }.map { game ->
|
||||
val target = child(folder, filename(game))
|
||||
// Keep an edited export without letting it block the rest of the library sync.
|
||||
val file = if (previous.optString(target.name) == game.gameIdString && target.exists() &&
|
||||
!matches(target, contents(game.gameIdString))) target else export(folder, game)
|
||||
next.put(file.name, game.gameIdString)
|
||||
file
|
||||
}
|
||||
// Write new exports before pruning; a failed write must not remove old launch files.
|
||||
for (name in previous.keys()) {
|
||||
val id = previous.getString(name)
|
||||
if (!next.has(name) && name.endsWith(".$EXTENSION") && GameLaunchLink.validId(id)) {
|
||||
val file = child(folder, name)
|
||||
if (matches(file, contents(id))) {
|
||||
// Android shared storage is often case insensitive. A case-only rename may
|
||||
// still resolve to the live export, which must not be pruned as an old file.
|
||||
val retained = exports.any { java.nio.file.Files.isSameFile(it.toPath(), file.toPath()) }
|
||||
if (!retained) check(file.delete()) { "Could not remove $name" }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (previous.toString() != next.toString()) write(inventory, JSONObject().put("format", FORMAT).put("games", next).toString())
|
||||
}
|
||||
|
||||
private fun child(folder: File, name: String): File {
|
||||
require(name.isNotEmpty() && '/' !in name && '\\' !in name && name != "." && name != "..")
|
||||
val file = File(folder, name)
|
||||
require(file.canonicalFile.parentFile == folder.canonicalFile) { "File leaves the export folder" }
|
||||
return file
|
||||
}
|
||||
|
||||
private fun matches(file: File, text: String) = file.isFile && file.length() == text.toByteArray().size.toLong() &&
|
||||
file.readText() == text
|
||||
|
||||
private fun write(file: File, text: String) = AtomicFile(file).writeText(text)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
/** External callers can select a game, never a guest path or arbitrary Steam command. */
|
||||
object GameLaunchLink {
|
||||
private val idPattern = Regex("[1-9][0-9]{0,19}")
|
||||
private val linkPattern = Regex("droiddeck://game/([1-9][0-9]{0,19})")
|
||||
|
||||
fun validId(id: String): Boolean = idPattern.matches(id) && id.toULongOrNull() != null
|
||||
fun parse(link: String?): String? = link?.let { linkPattern.matchEntire(it)?.groupValues?.get(1) }
|
||||
?.takeIf(::validId)
|
||||
fun uri(id: String): String { require(validId(id)); return "droiddeck://game/$id" }
|
||||
fun steamUrl(id: String): String { require(validId(id)); return "steam://rungameid/$id" }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import com.droiddeck.launcher.MainActivity
|
||||
import com.droiddeck.launcher.R
|
||||
|
||||
object GameShortcuts {
|
||||
fun launchIntent(context: Context, gameId: String) = Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse(GameLaunchLink.uri(gameId)), context, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
|
||||
private fun info(context: Context, game: Library.SteamGame): ShortcutInfo {
|
||||
val name = game.name.ifBlank { context.getString(R.string.app_name) }
|
||||
val icon = listOfNotNull(game.icon, game.art).firstNotNullOfOrNull { file -> runCatching {
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(file.path, bounds)
|
||||
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return@runCatching null
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inSampleSize = 1
|
||||
while (maxOf(bounds.outWidth, bounds.outHeight) / inSampleSize > 256) inSampleSize *= 2
|
||||
}
|
||||
BitmapFactory.decodeFile(file.path, options)?.let(Icon::createWithBitmap)
|
||||
}.getOrNull() } ?: Icon.createWithResource(context, R.mipmap.ic_launcher)
|
||||
return ShortcutInfo.Builder(context, "game:${game.gameIdString}")
|
||||
.setActivity(android.content.ComponentName(context, MainActivity::class.java))
|
||||
.setShortLabel(name.take(40)).setLongLabel(name).setIcon(icon)
|
||||
.setIntent(launchIntent(context, game.gameIdString)).build()
|
||||
}
|
||||
|
||||
fun pin(activity: Activity, game: Library.SteamGame) {
|
||||
val manager = activity.getSystemService(ShortcutManager::class.java)
|
||||
val requested = runCatching { manager.isRequestPinShortcutSupported &&
|
||||
manager.requestPinShortcut(info(activity, game), null) }.getOrDefault(false)
|
||||
if (!requested) Toast.makeText(activity, R.string.game_shortcut_unsupported, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
/** Standard shortcut picker result, understood by launchers that import Android shortcuts. */
|
||||
fun result(activity: Activity, game: Library.SteamGame): Intent =
|
||||
activity.getSystemService(ShortcutManager::class.java).createShortcutResultIntent(info(activity, game))
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.GameStorage
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.SecondaryLibrary
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -22,7 +23,14 @@ object Library {
|
||||
val appId: Int, val name: String, val art: File?, val library: String, val gameId: Long = appId.toLong(),
|
||||
val hero: File? = null, val lastPlayed: Long = 0L,
|
||||
val gameFiles: File? = null, val protonPrefix: File? = null,
|
||||
)
|
||||
val icon: File? = null,
|
||||
) {
|
||||
/** Decimal form used by Steam links and Android shortcuts, including unsigned shortcut ids. */
|
||||
val gameIdString: String get() = java.lang.Long.toUnsignedString(gameId)
|
||||
}
|
||||
/** The [SteamGame.library] of a game added to the library rather than installed by Steam. */
|
||||
const val ADDED = "added"
|
||||
|
||||
class Rom(val name: String, val hostPath: File, val guestPath: String, val emulatorId: String, val art: File? = null)
|
||||
class Emulator(val id: String, val name: String, val system: String, val program: String, val installed: Boolean, val games: List<Rom>) {
|
||||
/** The emulator's own icon, bundled (the runtime keeps them as theme SVGs the app cannot draw). */
|
||||
@@ -64,6 +72,7 @@ object Library {
|
||||
)
|
||||
private val STEAM_CAPSULES = listOf("library_capsule.jpg", "library_600x900.jpg")
|
||||
private val STEAM_HEROES = listOf("library_hero.jpg")
|
||||
private val STEAM_ICON = Regex("[a-fA-F0-9]{40}\\.(jpg|png)")
|
||||
private val NAME = Regex("^\\s*\"name\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE)
|
||||
private val STATE = Regex("^\\s*\"StateFlags\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
private val LAST_PLAYED = Regex("^\\s*\"LastPlayed\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
@@ -81,25 +90,30 @@ object Library {
|
||||
/** Proton keeps each game's prefix below compatdata/<appid>/pfx in a Steam library. */
|
||||
fun protonPrefix(context: Context, appId: Long, preferredLibrary: File? = null): File? {
|
||||
val ids = listOf(appId.toString(), java.lang.Integer.toString(appId.toInt())).distinct()
|
||||
val roots = (listOfNotNull(preferredLibrary) + steamLibraries(context).map { it.first })
|
||||
val secondary = GameStorage.effective(context)?.let { File(it.path) }
|
||||
fun prefixRoot(root: File): File = if (secondary != null && root.canonicalPath == secondary.canonicalPath)
|
||||
SecondaryLibrary.privateRoot(context.filesDir, root).takeIf { File(it, "steamapps/compatdata").isDirectory } ?: root
|
||||
else root
|
||||
val roots = (listOfNotNull(preferredLibrary) + steamLibraries(context).map { it.first }).map(::prefixRoot)
|
||||
.distinctBy { runCatching { it.canonicalPath }.getOrDefault(it.absolutePath) }
|
||||
return roots.asSequence()
|
||||
.flatMap { root -> ids.asSequence().map { id -> File(root, "steamapps/compatdata/$id/pfx") } }
|
||||
.firstOrNull { it.isDirectory }
|
||||
}
|
||||
|
||||
fun steamGames(context: Context): List<SteamGame> {
|
||||
fun steamGames(context: Context, strictRead: Boolean = false): List<SteamGame> {
|
||||
val root = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam")
|
||||
val cache = File(root, "appcache/librarycache")
|
||||
val libraries = steamLibraries(context)
|
||||
val out = LinkedHashMap<Int, SteamGame>()
|
||||
for ((library, label) in libraries) {
|
||||
val steamapps = File(library, "steamapps")
|
||||
steamapps.listFiles { f -> f.isFile && f.name.startsWith("appmanifest_") && f.name.endsWith(".acf") }
|
||||
?.sortedBy { it.name }?.forEach { manifest ->
|
||||
val manifests = steamapps.listFiles { f -> f.isFile && f.name.startsWith("appmanifest_") && f.name.endsWith(".acf") }
|
||||
if (strictRead && steamapps.isDirectory) check(manifests != null) { "Steam manifests are unavailable" }
|
||||
manifests?.sortedBy { it.name }?.forEach { manifest ->
|
||||
val appId = manifest.name.removePrefix("appmanifest_").removeSuffix(".acf").toIntOrNull() ?: return@forEach
|
||||
if (appId in NOT_GAMES || out.containsKey(appId)) return@forEach
|
||||
val text = try { manifest.readText() } catch (e: Exception) { return@forEach }
|
||||
val text = try { manifest.readText() } catch (e: Exception) { if (strictRead) throw e; return@forEach }
|
||||
val name = NAME.find(text)?.groupValues?.get(1)?.trim().orEmpty()
|
||||
val flags = STATE.find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||
// StateFlags 4 = fully installed; anything else is downloading, updating or broken.
|
||||
@@ -117,12 +131,26 @@ object Library {
|
||||
appId, name, art, label, hero = hero, lastPlayed = lastPlayed,
|
||||
gameFiles = gameFiles,
|
||||
protonPrefix = protonPrefix(context, appId.toLong(), library),
|
||||
icon = steamCacheImage(cache, appId, listOf("icon.jpg", "icon.png"))
|
||||
?: File(cache, appId.toString()).listFiles()?.firstOrNull { it.isFile && STEAM_ICON.matches(it.name) },
|
||||
)
|
||||
}
|
||||
}
|
||||
return out.values.toList()
|
||||
}
|
||||
|
||||
/** The same installed-game inventory used for links, shortcuts and file exports. */
|
||||
fun launchableGames(context: Context, strictRead: Boolean = false): List<SteamGame> = (steamGames(context, strictRead) + AddedGames.scan(context).map { g ->
|
||||
AddedGameArt.resolve(context, g).let { art ->
|
||||
SteamGame(
|
||||
g.steamAppId ?: g.appId.toInt(), g.name, art.portrait ?: art.header, ADDED, g.gameId,
|
||||
hero = art.hero ?: art.header, gameFiles = g.folder,
|
||||
protonPrefix = protonPrefix(context, g.steamAppId?.toLong() ?: g.appId),
|
||||
icon = art.icon?.takeIf { it.extension.lowercase() != "ico" },
|
||||
)
|
||||
}
|
||||
}).distinctBy { it.gameId }
|
||||
|
||||
/** Steam stores current library art inside hash-named folders under the app's cache dir. */
|
||||
private fun steamCacheImage(cache: File, appId: Int, names: List<String>): File? {
|
||||
val appDir = File(cache, appId.toString())
|
||||
@@ -163,13 +191,20 @@ object Library {
|
||||
)
|
||||
|
||||
/** The emulator's name for a program path from the rail ("ARMSX2"), or null. */
|
||||
fun nameForProgram(program: String?): String? = specs.firstOrNull { it.program == program }?.name
|
||||
fun nameForProgram(program: String?): String? =
|
||||
if (program == com.droiddeck.launcher.runtime.FlatpakManager.LAUNCHER || program == com.droiddeck.launcher.runtime.AppImageManager.LAUNCHER ||
|
||||
program == com.droiddeck.launcher.runtime.UserApps.SCRIPT_LAUNCHER) {
|
||||
com.droiddeck.launcher.session.SessionState.programArgs.firstOrNull()?.let { flatpakNames[it] ?: it.substringAfterLast('.') }
|
||||
} else specs.firstOrNull { it.program == program }?.name
|
||||
|
||||
/** Added apps' names by Flatpak id or directory, as launched: the session only knows that. */
|
||||
val flatpakNames = java.util.concurrent.ConcurrentHashMap<String, String>()
|
||||
|
||||
/**
|
||||
* Whether a program from the rail runs at the panel's own resolution rather than the session's
|
||||
* 720p default. melonDS draws two 256x192 screens on the CPU: the panel's size costs it
|
||||
* nothing, and 720p scaled up to the panel blurs the sharp pixels its screen layout is set
|
||||
* up for (bannerlator-pad-defaults).
|
||||
* up for (droiddeck-pad-defaults).
|
||||
*/
|
||||
fun drawsAtPanel(program: String?): Boolean = specs.any { it.program == program && it.atPanel }
|
||||
|
||||
@@ -306,7 +341,7 @@ object Library {
|
||||
// Dolphin: full screen, drawn inside its own main window, without its "stop the emulation?"
|
||||
// question; no warning boxes either, which wait for a click a controller cannot give
|
||||
// (they still go to Dolphin's log). The guide button is Dolphin's Toggle Fullscreen hotkey
|
||||
// (bannerlator-pad-defaults), which shows Dolphin's window and its settings - so no -b, which
|
||||
// (droiddeck-pad-defaults), which shows Dolphin's window and its settings - so no -b, which
|
||||
// hides that window; closing Dolphin ends the session. Under gamescope Dolphin does not
|
||||
// always see its window as focused, and by default both its hotkeys and the game's
|
||||
// controller then stop: HotkeysRequireFocus off, BackgroundInput on. -C sets a Dolphin.ini
|
||||
@@ -318,10 +353,10 @@ object Library {
|
||||
"-e", guestPath,
|
||||
)
|
||||
// Full screen (LaunchSettings.cpp -f); the settings seed keeps the Getting Started
|
||||
// dialog away (bannerlator-pad-defaults).
|
||||
// dialog away (droiddeck-pad-defaults).
|
||||
"cemu" -> listOf("-f", "-g", guestPath)
|
||||
// Full screen (CLI.cpp --fullscreen); the guide button leaves it for melonDS's menus and
|
||||
// comes back (HK_FullscreenToggle, bannerlator-pad-defaults).
|
||||
// comes back (HK_FullscreenToggle, droiddeck-pad-defaults).
|
||||
"melonds" -> listOf("-f", guestPath)
|
||||
else -> listOf(guestPath)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* The last game list the front end showed, kept in the app's files so the next start draws the
|
||||
* Steam wall on its first frame. Building the list reads every manifest and walks the added-games
|
||||
* folders, which can take a second or more; the fresh list replaces this one when it is ready.
|
||||
*/
|
||||
object LibraryCache {
|
||||
private const val TAG = "LibraryCache"
|
||||
private fun file(context: Context) = File(context.filesDir, "library-cache.json")
|
||||
|
||||
fun load(context: Context): List<Library.SteamGame> = runCatching {
|
||||
val f = file(context)
|
||||
if (!f.isFile) return emptyList()
|
||||
val array = JSONArray(f.readText())
|
||||
List(array.length()) { i ->
|
||||
val o = array.getJSONObject(i)
|
||||
Library.SteamGame(
|
||||
appId = o.getInt("appId"), name = o.getString("name"), art = o.file("art"),
|
||||
library = o.getString("library"), gameId = o.getLong("gameId"), hero = o.file("hero"),
|
||||
lastPlayed = o.optLong("lastPlayed"), gameFiles = o.file("gameFiles"), protonPrefix = o.file("protonPrefix"),
|
||||
icon = o.file("icon"),
|
||||
)
|
||||
}
|
||||
}.getOrElse { e -> Log.w(TAG, "unreadable, ignored: ${e.message}"); emptyList() }
|
||||
|
||||
fun save(context: Context, games: List<Library.SteamGame>) {
|
||||
runCatching {
|
||||
val array = JSONArray()
|
||||
for (g in games) array.put(
|
||||
JSONObject().put("appId", g.appId).put("name", g.name).put("library", g.library).put("gameId", g.gameId)
|
||||
.put("lastPlayed", g.lastPlayed).putFile("art", g.art).putFile("hero", g.hero)
|
||||
.putFile("gameFiles", g.gameFiles).putFile("protonPrefix", g.protonPrefix).putFile("icon", g.icon),
|
||||
)
|
||||
val f = file(context)
|
||||
val tmp = File(f.path + ".tmp")
|
||||
tmp.writeText(array.toString())
|
||||
tmp.renameTo(f)
|
||||
}.onFailure { e -> Log.w(TAG, "not saved: ${e.message}") }
|
||||
}
|
||||
|
||||
private fun JSONObject.file(key: String): File? = optString(key).takeIf { it.isNotEmpty() }?.let(::File)
|
||||
private fun JSONObject.putFile(key: String, f: File?): JSONObject = if (f == null) this else put(key, f.path)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
/**
|
||||
* An all-in-one driver package: one zip carrying both halves of a matched pair, the display
|
||||
* (Android, bionic) driver in one directory and the runtime (Linux, glibc) driver in another,
|
||||
* indexed by a root `manifest.json` - DroidDeck's own DD-Turnip releases (Droid-Deck/Drivers):
|
||||
*
|
||||
* ```
|
||||
* manifest.json {"package", "version", "drivers": {"android": {"path"}, "linux": {"path"}}}
|
||||
* android/ meta.json (AdrenoTools) + libvulkan_freedreno.so
|
||||
* linux/ meta.json + libvulkan_freedreno.so
|
||||
* ```
|
||||
*
|
||||
* Installing one puts each half through its own importer, with that importer's checks, and
|
||||
* remembers the two as one bundle: picking either half sets both, and deleting either deletes both,
|
||||
* so a bundle is always used whole. Zips of the older kind, one driver each, never reach this.
|
||||
*/
|
||||
object DriverBundle {
|
||||
private const val TAG = "DriverBundle"
|
||||
private const val PREFS = "driver_bundles"
|
||||
private const val MANIFEST = "manifest.json"
|
||||
/** Refuses a zip that unpacks to more than this: both halves together are about 30 MB. */
|
||||
private const val MAX_BYTES = 512L shl 20
|
||||
|
||||
class Manifest(val name: String, val version: String, val tag: String, val androidPath: String, val linuxPath: String)
|
||||
|
||||
/** An installed bundle: [androidId] in TurnipDriver's list, [linuxId] in LinuxVulkanDriverManager's. */
|
||||
class Bundle(val id: String, val name: String, val version: String, val androidId: String, val linuxId: String) {
|
||||
/** "DD-Turnip 0.1.0". */
|
||||
val label: String get() = if (version.isEmpty()) name else "$name $version"
|
||||
}
|
||||
|
||||
/** The manifest of a bundle, or null when [json] is not one (both halves, in two directories). */
|
||||
fun parseManifest(json: JSONObject): Manifest? {
|
||||
val drivers = json.optJSONObject("drivers") ?: return null
|
||||
val android = cleanPath(drivers.optJSONObject("android")?.optString("path").orEmpty()) ?: return null
|
||||
val linux = cleanPath(drivers.optJSONObject("linux")?.optString("path").orEmpty()) ?: return null
|
||||
if (android == linux) return null
|
||||
return Manifest(
|
||||
json.optString("package").ifEmpty { "Driver bundle" }, json.optString("version"),
|
||||
json.optString("tag"), android, linux,
|
||||
)
|
||||
}
|
||||
|
||||
private fun cleanPath(path: String): String? {
|
||||
val p = path.trim().trim('/')
|
||||
return p.takeIf { it.isNotEmpty() && it.split('/').none { part -> part.isEmpty() || part == "." || part == ".." } }
|
||||
}
|
||||
|
||||
/** True when the zip at [uri] is a bundle rather than a single driver. */
|
||||
fun isBundle(context: Context, uri: Uri): Boolean = try {
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
ZipInputStream(input).use { zip ->
|
||||
generateSequence { zip.nextEntry }.firstOrNull { !it.isDirectory && it.name == MANIFEST }
|
||||
?.let { parseManifest(JSONObject(zip.readBytes().toString(Charsets.UTF_8))) } != null
|
||||
}
|
||||
} ?: false
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "reading $uri as a bundle", e)
|
||||
false
|
||||
}
|
||||
|
||||
/**
|
||||
* Install both halves of the bundle at [uri]. Both or neither: a half that is refused removes
|
||||
* the one already in. Throws [IllegalArgumentException] with a user-facing reason when the zip
|
||||
* is not a usable bundle, [IOException] on read failures.
|
||||
*/
|
||||
fun install(context: Context, uri: Uri): Bundle {
|
||||
val work = File(context.cacheDir, "bundle-${System.nanoTime()}")
|
||||
val td = TurnipDriver(context)
|
||||
val lm = LinuxVulkanDriverManager(context)
|
||||
var androidId: String? = null
|
||||
try {
|
||||
unzip(context, uri, work)
|
||||
val manifest = File(work, MANIFEST).takeIf { it.isFile }
|
||||
?.let { runCatching { parseManifest(JSONObject(FileUtils.readString(it))) }.getOrNull() }
|
||||
?: throw IllegalArgumentException("Not a driver bundle: no manifest.json naming an android and a linux driver")
|
||||
val androidDir = File(work, manifest.androidPath)
|
||||
val linuxDir = File(work, manifest.linuxPath)
|
||||
|
||||
val androidStage = td.newStagingDir()
|
||||
androidId = try {
|
||||
androidDir.listFiles()?.filter { it.isFile }?.forEach { it.copyTo(File(androidStage, it.name)) }
|
||||
td.adopt(androidStage, manifest.name)
|
||||
} catch (e: Exception) {
|
||||
FileUtils.delete(androidStage)
|
||||
throw e
|
||||
}
|
||||
|
||||
val linuxStage = lm.newStagingDir()
|
||||
val linuxId = try {
|
||||
val library = linuxDir.listFiles()?.firstOrNull { it.isFile && it.name.startsWith("libvulkan_freedreno") && it.name.endsWith(".so") }
|
||||
library?.copyTo(File(linuxStage, LinuxVulkanDriverManager.LIB_NAME))
|
||||
val meta = File(linuxDir, LinuxVulkanDriverManager.META_NAME).takeIf { it.isFile }
|
||||
?.let { runCatching { JSONObject(FileUtils.readString(it)) }.getOrNull() }
|
||||
lm.adopt(linuxStage, library?.name, meta, manifest.name)
|
||||
} catch (e: Exception) {
|
||||
FileUtils.delete(linuxStage)
|
||||
throw e
|
||||
}
|
||||
|
||||
val base = LinuxVulkanDriverManager.sanitizeId(manifest.tag.ifEmpty { "${manifest.name}-${manifest.version}" })
|
||||
val saved = registry(context)
|
||||
var id = base
|
||||
var n = 2
|
||||
while (saved.contains(id)) id = "$base-${n++}"
|
||||
val bundle = Bundle(id, manifest.name, manifest.version, androidId, linuxId)
|
||||
saved.edit().putString(id, JSONObject().put("name", bundle.name).put("version", bundle.version)
|
||||
.put("android", androidId).put("linux", linuxId).toString()).apply()
|
||||
Log.i(TAG, "installed bundle $id: display $androidId, runtime $linuxId")
|
||||
return bundle
|
||||
} catch (e: Exception) {
|
||||
androidId?.let { td.remove(it) }
|
||||
throw e
|
||||
} finally {
|
||||
FileUtils.delete(work)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unzip(context: Context, uri: Uri, dest: File) {
|
||||
val root = dest.canonicalFile
|
||||
var total = 0L
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
ZipInputStream(input).use { zip ->
|
||||
for (entry in generateSequence { zip.nextEntry }) {
|
||||
if (entry.isDirectory) continue
|
||||
val out = File(root, entry.name).canonicalFile
|
||||
if (!out.path.startsWith(root.path + File.separator)) throw IllegalArgumentException("Not a driver bundle: ${entry.name} points outside it")
|
||||
out.parentFile?.mkdirs()
|
||||
out.outputStream().use { sink ->
|
||||
val buf = ByteArray(1 shl 16)
|
||||
while (true) {
|
||||
val r = zip.read(buf)
|
||||
if (r <= 0) break
|
||||
total += r
|
||||
if (total > MAX_BYTES) throw IllegalArgumentException("Not a driver bundle: it unpacks to more than ${MAX_BYTES shr 20} MB")
|
||||
sink.write(buf, 0, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: throw IOException("cannot open $uri")
|
||||
}
|
||||
|
||||
/** Every installed bundle whose two halves are both still there. */
|
||||
fun all(context: Context): List<Bundle> {
|
||||
val td = TurnipDriver(context)
|
||||
val lm = LinuxVulkanDriverManager(context)
|
||||
return registry(context).all.mapNotNull { (id, raw) ->
|
||||
runCatching {
|
||||
val j = JSONObject(raw as String)
|
||||
Bundle(id, j.getString("name"), j.optString("version"), j.getString("android"), j.getString("linux"))
|
||||
}.getOrNull()
|
||||
}.filter { td.isInstalled(it.androidId) && lm.isInstalled(it.linuxId) }.sortedBy { it.id }
|
||||
}
|
||||
|
||||
fun get(context: Context, id: String): Bundle? = all(context).firstOrNull { it.id == id }
|
||||
|
||||
/** The bundle [driverId] is a half of, in the runtime list when [linux], else the display list. */
|
||||
fun containing(context: Context, driverId: String, linux: Boolean): Bundle? =
|
||||
all(context).firstOrNull { (if (linux) it.linuxId else it.androidId) == driverId }
|
||||
|
||||
/** The bundle both drivers are set to right now, if they are one. */
|
||||
fun active(context: Context): Bundle? {
|
||||
val android = SessionPrefs.androidDriver(context)
|
||||
val linux = SessionPrefs.linuxDriver(context)
|
||||
return all(context).firstOrNull { it.androidId == android && it.linuxId == linux }
|
||||
}
|
||||
|
||||
/** Set both halves as the drivers in use. */
|
||||
fun select(context: Context, bundle: Bundle) {
|
||||
SessionPrefs.setAndroidDriver(context, bundle.androidId)
|
||||
SessionPrefs.setLinuxDriver(context, bundle.linuxId)
|
||||
}
|
||||
|
||||
/** Delete both halves; a driver still set to either goes back to its default. */
|
||||
fun remove(context: Context, bundle: Bundle) {
|
||||
TurnipDriver(context).remove(bundle.androidId)
|
||||
LinuxVulkanDriverManager(context).removeDriver(bundle.linuxId)
|
||||
if (SessionPrefs.linuxDriver(context) == bundle.linuxId) SessionPrefs.setLinuxDriver(context, "")
|
||||
registry(context).edit().remove(bundle.id).apply()
|
||||
Log.i(TAG, "removed bundle ${bundle.id}")
|
||||
}
|
||||
|
||||
private fun registry(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import com.droiddeck.launcher.gpu.GpuInfo.Family
|
||||
|
||||
/**
|
||||
* The release drivers as matched pairs: a display build (the app's compositor, bionic) and a Linux
|
||||
* build (the runtime, glibc) that were made to run together. Mixing a runtime driver carrying a
|
||||
* patch with a display driver that lacks it is where the hard-to-read bugs come from, so the app
|
||||
* installs and selects both halves of one pair at once.
|
||||
*
|
||||
* - Banners-Turnip publishes each variant as a set in every release (`Turnip-<tag><variant>.zip`
|
||||
* and `...-Linux.zip`), rebuilt from upstream Mesa as it moves: the pair is one variant of the
|
||||
* newest release that carries both halves.
|
||||
* - WinNative ships its display (`v1.x`) and Linux (`linux-v0.x`) builds as two release lines with
|
||||
* their own numbers: the pair is the newest of each, of one flavour (Balanced or Performance).
|
||||
* - DroidDeck's DD-Turnip ships both halves in one zip per release (DriverBundle): the pair is the
|
||||
* newest bundle, and it can never be mixed.
|
||||
*/
|
||||
object DriverPairs {
|
||||
const val BANNER = "banner"
|
||||
const val BANNER_A8XX = "banner-a8xx"
|
||||
const val BANNER_710 = "banner-710-720-test"
|
||||
const val BANNER_ONEUI = "banner-8g2-oneui"
|
||||
const val WN_BALANCED = "wn-b"
|
||||
const val WN_PERFORMANCE = "wn-p"
|
||||
const val DD_TURNIP = "dd-turnip"
|
||||
/** Each DD-Turnip release is its own pair: "dd-turnip:DD-Turnip-v0.1.0". */
|
||||
fun ddTurnip(tag: String) = "$DD_TURNIP:$tag"
|
||||
|
||||
class DriverPair(
|
||||
val key: String,
|
||||
/** "Banners-Turnip · Adreno 6xx/7xx", "WinNative · Balanced". */
|
||||
val name: String,
|
||||
/** The families the pair is built for; the list only offers it on those unless asked. */
|
||||
val families: Set<Family>,
|
||||
val display: TurnipReleases.Asset?,
|
||||
val linux: TurnipReleases.Asset?,
|
||||
/** Both halves in one zip, instead of [display] and [linux]. */
|
||||
val bundle: TurnipReleases.Asset? = null,
|
||||
) {
|
||||
val complete: Boolean get() = bundle != null || (display != null && linux != null)
|
||||
/** The zips to download for the pair: the bundle, or the two halves. */
|
||||
val assets: List<TurnipReleases.Asset> get() = listOfNotNull(bundle ?: display, if (bundle == null) linux else null)
|
||||
/** "r4 · 1.19 + 0.1.2": the build of each half. */
|
||||
val version: String
|
||||
get() = listOfNotNull(bundle?.tag, display?.tag, linux?.tag).distinct().joinToString(" + ") { it.removePrefix("linux-").removePrefix("DD-Turnip-") }
|
||||
fun suits(gpu: GpuInfo) = gpu.family in families ||
|
||||
(gpu.family == Family.ADRENO_UNKNOWN && (Family.A7XX in families || Family.A8XX in families))
|
||||
}
|
||||
|
||||
private val ALL_ADRENO = setOf(Family.A6XX, Family.A7XX, Family.A7XX_LOW, Family.A8XX, Family.ADRENO_UNKNOWN)
|
||||
|
||||
private fun describe(key: String): Pair<String, Set<Family>> = when {
|
||||
key.startsWith(DD_TURNIP) -> "DroidDeck · DD-Turnip" to ALL_ADRENO
|
||||
else -> describeFixed(key)
|
||||
}
|
||||
|
||||
private fun describeFixed(key: String): Pair<String, Set<Family>> = when (key) {
|
||||
BANNER -> "Banners-Turnip · Adreno 6xx/7xx" to setOf(Family.A6XX, Family.A7XX)
|
||||
BANNER_A8XX -> "Banners-Turnip · Adreno 8xx" to setOf(Family.A8XX)
|
||||
BANNER_710 -> "Banners-Turnip · Adreno 710/720/722 (test)" to setOf(Family.A7XX_LOW)
|
||||
BANNER_ONEUI -> "Banners-Turnip · 8 Gen 2 on One UI" to setOf(Family.A7XX)
|
||||
WN_BALANCED -> "WinNative · Balanced" to ALL_ADRENO
|
||||
WN_PERFORMANCE -> "WinNative · Performance" to ALL_ADRENO
|
||||
else -> key to ALL_ADRENO
|
||||
}
|
||||
|
||||
/** Every pair the last release check found, in the order the list shows them. */
|
||||
fun from(check: TurnipReleases.Check?): List<DriverPair> {
|
||||
val assets = check?.assets.orEmpty().filter { it.pair.isNotEmpty() }
|
||||
val order = listOf(DD_TURNIP, BANNER, BANNER_A8XX, BANNER_ONEUI, BANNER_710, WN_BALANCED, WN_PERFORMANCE)
|
||||
return assets.map { it.pair }.distinct()
|
||||
.sortedBy { key -> order.indexOf(key.substringBefore(':')).let { i -> if (i < 0) order.size else i } }
|
||||
.map { key ->
|
||||
val (name, families) = describe(key)
|
||||
val halves = assets.filter { it.pair == key }
|
||||
halves.firstOrNull { it.bundle }?.let { return@map DriverPair(key, name, families, null, null, it) }
|
||||
// The list keeps the newest release of each half; for Banners-Turnip the two halves
|
||||
// must come from the same release, so a set missing one half in its newest release
|
||||
// is offered incomplete rather than mixed with an older one.
|
||||
val display = halves.firstOrNull { !it.linux }
|
||||
val linux = halves.firstOrNull { it.linux }
|
||||
val sameRelease = !key.startsWith(BANNER) || display?.tag == linux?.tag
|
||||
DriverPair(key, name, families, display, if (sameRelease) linux else null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The pair Auto sets for [gpu], among the [pairs] the last release check found: the newest
|
||||
* official DD-Turnip release on the Adreno 840 (840 and 840v2), WinNative Balanced on the other
|
||||
* 8xx (Banners' A8xx builds still break on some first boots) and on an 840 while no DD-Turnip
|
||||
* release is listed, the OneUI build for an 8 Gen 2 under One UI, the patched test builds for
|
||||
* the 710/720/722, and upstream Banners-Turnip for the rest. Null when nothing fits (not Adreno).
|
||||
*/
|
||||
fun recommendedKey(gpu: GpuInfo, pairs: List<DriverPair>): String? = when (gpu.family) {
|
||||
Family.NOT_ADRENO -> null
|
||||
Family.A8XX -> if (gpu.model == 840) newestDdTurnip(pairs) ?: WN_BALANCED else WN_BALANCED
|
||||
Family.A7XX_LOW -> BANNER_710
|
||||
Family.A7XX -> if (gpu.oneUi8Gen2) BANNER_ONEUI else BANNER
|
||||
Family.A6XX -> BANNER
|
||||
// KGSL gave no model: Android 16 arrived with the 8xx, so a device that new is taken as one.
|
||||
Family.ADRENO_UNKNOWN -> if (android.os.Build.VERSION.SDK_INT >= 36) WN_BALANCED else BANNER
|
||||
}
|
||||
|
||||
/** The complete DD-Turnip pair with the highest version, or null when none is listed. */
|
||||
fun newestDdTurnip(pairs: List<DriverPair>): String? = pairs
|
||||
.filter { it.key.startsWith("$DD_TURNIP:") && it.complete }
|
||||
.maxByOrNull { p ->
|
||||
Regex("""(\d+)\.(\d+)\.(\d+)$""").find(p.key)?.destructured
|
||||
?.let { (major, minor, patch) -> major.toLong() * 1_000_000_000_000 + minor.toLong() * 1_000_000 + patch.toLong() } ?: -1L
|
||||
}?.key
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
|
||||
/**
|
||||
@@ -9,7 +10,7 @@ import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
*
|
||||
* Both engines generate on gamescope's final output inside our compositor, so they work for any
|
||||
* game with nothing changed in the runtime. Win-FG is our own and needs nothing; LSFG needs the
|
||||
* shader chain from the user's copy of Lossless Scaling (see [LsfgNative]). Every compositor
|
||||
* shader chain from the user's copy of Lossless Scaling (see [Lossless]). Every compositor
|
||||
* setter is a value store on the native side, so this can be applied before the compositor is
|
||||
* up and changed live from the main screen while a session runs.
|
||||
*/
|
||||
@@ -26,62 +27,77 @@ object FrameGen {
|
||||
private const val FLOW_SCALE_WINFG = 0.60f
|
||||
private const val FLOW_SCALE_LSFG = 0.80f
|
||||
|
||||
/** LSFG's adaptive targets: it generates as many frames as it takes to reach one. */
|
||||
val ADAPTIVE_TARGETS = listOf(120, 90, 60)
|
||||
|
||||
private const val PREFS = "frame_gen"
|
||||
|
||||
fun engine(context: Context): String =
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString("engine", ENGINE_OFF) ?: ENGINE_OFF
|
||||
|
||||
fun multiplier(context: Context): Int =
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getInt("multiplier", 2).coerceIn(2, 4)
|
||||
|
||||
fun set(context: Context, engine: String, multiplier: Int) {
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||
.putString("engine", engine).putInt("multiplier", multiplier).apply()
|
||||
/** A fixed [multiplier] (2..4), or for LSFG a [target] frame rate (0 = fixed). */
|
||||
data class Mode(val engine: String, val multiplier: Int = 2, val target: Int = 0) {
|
||||
companion object {
|
||||
val OFF = Mode(ENGINE_OFF)
|
||||
}
|
||||
}
|
||||
|
||||
/** "Off", "Win-FG 2×", "LSFG 3×" - for the main screen. */
|
||||
fun label(context: Context): String = label(engine(context), multiplier(context))
|
||||
fun engine(context: Context): String = mode(context).engine
|
||||
|
||||
fun label(engine: String, multiplier: Int): String = when (engine) {
|
||||
ENGINE_WINFG -> "Win-FG $multiplier×"
|
||||
ENGINE_LSFG -> "LSFG $multiplier×"
|
||||
else -> "Off"
|
||||
fun mode(context: Context): Mode {
|
||||
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
val engine = prefs.getString("engine", ENGINE_OFF) ?: ENGINE_OFF
|
||||
val target = prefs.getInt("target", 0)
|
||||
if (engine == ENGINE_LSFG && target in ADAPTIVE_TARGETS) return Mode(engine, target = target)
|
||||
return Mode(engine, prefs.getInt("multiplier", 2).coerceIn(2, 4))
|
||||
}
|
||||
|
||||
fun set(context: Context, mode: Mode) {
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||
.putString("engine", mode.engine).putInt("multiplier", mode.multiplier).putInt("target", mode.target).apply()
|
||||
}
|
||||
|
||||
/** "Off", "Win-FG 2×", "LSFG 3×", "LSFG Adaptive 90". */
|
||||
fun label(context: Context): String = label(context, mode(context))
|
||||
|
||||
fun label(context: Context, mode: Mode): String = when {
|
||||
mode.engine == ENGINE_WINFG -> context.getString(R.string.frame_gen_winfg, mode.multiplier)
|
||||
mode.engine == ENGINE_LSFG && mode.target > 0 -> context.getString(R.string.frame_gen_lsfg_adaptive, mode.target)
|
||||
mode.engine == ENGINE_LSFG -> context.getString(R.string.frame_gen_lsfg, mode.multiplier)
|
||||
else -> context.getString(R.string.frame_gen_off)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the saved setting into the compositor. Returns a message for the user when the
|
||||
* engine cannot run (LSFG without Lossless Scaling), or null.
|
||||
* engine cannot run (LSFG without Lossless Scaling), or null. Blocks while a new or updated
|
||||
* Lossless.dll is prepared.
|
||||
*/
|
||||
fun apply(context: Context, refreshHz: Float): String? {
|
||||
val engine = engine(context)
|
||||
val multiplier = multiplier(context)
|
||||
var problem: String? = null
|
||||
when (engine) {
|
||||
val mode = mode(context)
|
||||
when (mode.engine) {
|
||||
ENGINE_LSFG -> {
|
||||
val status = LsfgNative.ensureCache(context)
|
||||
if (status != LsfgNative.STATUS_OK) {
|
||||
problem = if (status == LsfgNative.STATUS_NOT_INSTALLED)
|
||||
"LSFG needs Lossless Scaling installed from the Steam client"
|
||||
else "LSFG: " + LsfgNative.statusName(status)
|
||||
Log.w(TAG, problem)
|
||||
WaylandCompositor.nativeSetFrameGenArmed(false, 0)
|
||||
val status = Lossless.sync(context)
|
||||
val cache = Lossless.cacheFile(context)
|
||||
if (status != LsfgNative.STATUS_OK || cache == null) {
|
||||
WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0)
|
||||
val problem = context.getString(
|
||||
if (status == LsfgNative.STATUS_NOT_INSTALLED) R.string.lsfg_problem_missing else R.string.lsfg_problem_failed,
|
||||
)
|
||||
Log.w(TAG, "$problem (status $status)")
|
||||
return problem
|
||||
}
|
||||
WaylandCompositor.nativeSetFrameGenEngine(WaylandCompositor.FG_ENGINE_LSFG)
|
||||
WaylandCompositor.nativeSetLsfgCachePath(LsfgNative.cacheFile(context).absolutePath)
|
||||
WaylandCompositor.nativeSetLsfgCachePath(cache.absolutePath)
|
||||
WaylandCompositor.nativeSetFrameGenTuning(FLOW_SCALE_LSFG, refreshHz)
|
||||
WaylandCompositor.nativeSetFrameGenArmed(true, multiplier)
|
||||
WaylandCompositor.nativeSetFrameGenArmed(true, mode.multiplier, mode.target)
|
||||
}
|
||||
ENGINE_WINFG -> {
|
||||
WaylandCompositor.nativeSetFrameGenEngine(WaylandCompositor.FG_ENGINE_WINFG)
|
||||
WaylandCompositor.nativeSetWinFgTuning(WINFG_MODEL, WINFG_PERF_PRESET)
|
||||
WaylandCompositor.nativeSetFrameGenTuning(FLOW_SCALE_WINFG, refreshHz)
|
||||
WaylandCompositor.nativeSetFrameGenArmed(true, multiplier)
|
||||
WaylandCompositor.nativeSetFrameGenArmed(true, mode.multiplier, 0)
|
||||
}
|
||||
else -> WaylandCompositor.nativeSetFrameGenArmed(false, 0)
|
||||
else -> WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0)
|
||||
}
|
||||
Log.i(TAG, "frame generation: $engine x$multiplier at ${refreshHz}Hz")
|
||||
return problem
|
||||
Log.i(TAG, "frame generation: $mode at ${refreshHz}Hz")
|
||||
return null
|
||||
}
|
||||
|
||||
private const val TAG = "FrameGen"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.os.Build
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* What GPU this is, in the terms the driver lists are sorted by. KGSL names the model
|
||||
* ("Adreno740v2", "Adreno825"); the family decides which Turnip builds run on it at all, and the
|
||||
* support level is what the app has actually been tested on (Adreno 725 and up) - an Adreno 610
|
||||
* may start, but it is not "supported" just because it is a Qualcomm chip.
|
||||
*/
|
||||
data class GpuInfo(
|
||||
/** "Adreno 740", or the vendor's own name when this is not an Adreno. */
|
||||
val name: String,
|
||||
/** The three-digit Adreno model (740, 825), or 0 when KGSL does not say. */
|
||||
val model: Int,
|
||||
val family: Family,
|
||||
/** The SoC as the device reports it ("SM8550", "QCS8550"), or "" when it does not. */
|
||||
val soc: String,
|
||||
/** Samsung's One UI on an 8 Gen 2: its Turnip needs the OneUI build, or frames tear and flicker. */
|
||||
val oneUi8Gen2: Boolean,
|
||||
) {
|
||||
enum class Family(val label: String) {
|
||||
A8XX("Adreno 8xx"),
|
||||
/** Adreno 710/720/722: gen 7 cores cut down enough to need their own patches. */
|
||||
A7XX_LOW("Adreno 710/720/722"),
|
||||
A7XX("Adreno 7xx"),
|
||||
A6XX("Adreno 6xx"),
|
||||
/** An Adreno whose model KGSL does not give: treated as the newest family it could be. */
|
||||
ADRENO_UNKNOWN("Adreno"),
|
||||
NOT_ADRENO("Not an Adreno GPU"),
|
||||
}
|
||||
|
||||
enum class Support { TESTED, UNTESTED, UNSUPPORTED }
|
||||
|
||||
val support: Support
|
||||
get() = when {
|
||||
family == Family.NOT_ADRENO -> Support.UNSUPPORTED
|
||||
family == Family.A8XX -> Support.TESTED
|
||||
family == Family.A7XX && model >= 725 -> Support.TESTED
|
||||
else -> Support.UNTESTED
|
||||
}
|
||||
|
||||
/** One line for the device card and the system check. */
|
||||
val supportText: String
|
||||
get() = when (support) {
|
||||
Support.TESTED -> "Supported"
|
||||
Support.UNTESTED -> if (family == Family.A7XX_LOW) "Experimental: its drivers are test builds"
|
||||
else "Below tested hardware (Adreno 725 and newer): it may not run"
|
||||
Support.UNSUPPORTED -> "Not supported: DroidDeck needs an Adreno (Snapdragon) GPU"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun detect(): GpuInfo {
|
||||
val adreno = File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists()
|
||||
val raw = listOf("/sys/class/kgsl/kgsl-3d0/gpu_model", "/sys/class/kgsl/kgsl-3d0/gpu_chipid")
|
||||
.firstNotNullOfOrNull { FileUtils.readString(File(it))?.trim()?.takeIf(String::isNotEmpty) }
|
||||
val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it.isNotBlank() && it != Build.UNKNOWN }.orEmpty() else ""
|
||||
val model = raw?.let { Regex("""(\d{3})""").find(it)?.groupValues?.get(1)?.toIntOrNull() } ?: 0
|
||||
val family = familyOf(adreno, model)
|
||||
val samsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||
return GpuInfo(
|
||||
name = if (!adreno) Build.HARDWARE.ifBlank { "this GPU" } else if (model > 0) "Adreno $model" else "Adreno",
|
||||
model = model, family = family, soc = soc,
|
||||
oneUi8Gen2 = samsung && model == 740,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun familyOf(adreno: Boolean, model: Int): Family = when {
|
||||
!adreno -> Family.NOT_ADRENO
|
||||
model == 0 -> Family.ADRENO_UNKNOWN
|
||||
model >= 800 -> Family.A8XX
|
||||
model in listOf(710, 720, 722) -> Family.A7XX_LOW
|
||||
model >= 700 -> Family.A7XX
|
||||
model >= 600 -> Family.A6XX
|
||||
else -> Family.ADRENO_UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,9 +126,7 @@ public class LinuxVulkanDriverManager {
|
||||
* Linux driver; {@link IOException} on read/extract failures.
|
||||
*/
|
||||
public String installDriver(Uri zipUri, String displayName) throws IOException {
|
||||
File tmpDir = new File(rootDir, ".tmp-" + System.currentTimeMillis());
|
||||
FileUtils.delete(tmpDir);
|
||||
if (!tmpDir.mkdirs()) throw new IOException("cannot create " + tmpDir);
|
||||
File tmpDir = newStagingDir();
|
||||
boolean keep = false;
|
||||
try {
|
||||
String soName = null;
|
||||
@@ -156,9 +154,32 @@ public class LinuxVulkanDriverManager {
|
||||
}
|
||||
}
|
||||
// The zip's own freedreno_icd.aarch64.json is dropped on purpose: its
|
||||
// library_path is relative to itself, and we write an absolute one below.
|
||||
// library_path is relative to itself, and adopt() writes an absolute one.
|
||||
}
|
||||
}
|
||||
String id = adopt(tmpDir, soName, zipMeta, displayName);
|
||||
keep = true;
|
||||
return id;
|
||||
} finally {
|
||||
if (!keep) FileUtils.delete(tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
/** An empty directory beside the installed drivers, for a driver's files before {@link #adopt}. */
|
||||
File newStagingDir() throws IOException {
|
||||
File tmpDir = new File(rootDir, ".tmp-" + System.nanoTime());
|
||||
FileUtils.delete(tmpDir);
|
||||
if (!tmpDir.mkdirs()) throw new IOException("cannot create " + tmpDir);
|
||||
return tmpDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a staged driver ({@link #LIB_NAME} in {@code tmpDir}, copied from {@code soName} in
|
||||
* the zip), write its manifest and meta.json, and move it in. Returns the id; the caller
|
||||
* deletes {@code tmpDir} when this throws.
|
||||
*/
|
||||
String adopt(File tmpDir, String soName, JSONObject zipMeta, String displayName) throws IOException {
|
||||
try {
|
||||
if (soName == null) {
|
||||
throw new IllegalArgumentException("No libvulkan_freedreno*.so in this zip. An Android "
|
||||
+ "(AdrenoTools) or -Wayland Turnip zip is not a Linux runtime driver.");
|
||||
@@ -212,13 +233,10 @@ public class LinuxVulkanDriverManager {
|
||||
if (!FileUtils.writeString(new File(tmpDir, META_NAME), meta.toString(2))) throw new IOException("cannot write meta.json");
|
||||
|
||||
if (!tmpDir.renameTo(dir)) throw new IOException("cannot move into " + dir);
|
||||
keep = true;
|
||||
Log.i(TAG, "imported Linux Vulkan driver " + id + " (" + soName + ", minGlibc=" + minGlibc + ") -> " + dir);
|
||||
return id;
|
||||
} catch (org.json.JSONException e) {
|
||||
throw new IOException("manifest write failed: " + e.getMessage());
|
||||
} finally {
|
||||
if (!keep) FileUtils.delete(tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.GameStorage
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import java.io.File
|
||||
import java.security.DigestOutputStream
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* LSFG's shader chain, from the user's own Lossless Scaling (Steam app 993090).
|
||||
*
|
||||
* The copy the Steam client installs, in any library it installs to, is taken by
|
||||
* itself and taken again whenever the client updates it. Once the session has seen the account
|
||||
* own the app, a Lossless.dll can also be imported by hand; it stays until the Steam copy changes
|
||||
* or another is imported. The DLL is copied in and its shaders are built into a cache named after
|
||||
* its contents, so a different DLL hands the compositor a different path and its engine reloads.
|
||||
* Everything here blocks: call it off the main thread.
|
||||
*/
|
||||
object Lossless {
|
||||
enum class Source { NONE, STEAM, IMPORT }
|
||||
|
||||
enum class ImportResult { UPDATED, UNCHANGED, NOT_OWNED, INVALID, FAILED }
|
||||
|
||||
data class State(val owned: Boolean, val ready: Boolean, val source: Source) {
|
||||
companion object {
|
||||
val NONE = State(owned = false, ready = false, source = Source.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "Lossless"
|
||||
private const val PREFS = "lossless"
|
||||
private const val KEY_OWNED = "owned"
|
||||
private const val KEY_ID = "id"
|
||||
private const val KEY_SOURCE = "source"
|
||||
private const val KEY_STEAM_STAMP = "steamStamp"
|
||||
private const val STEAM_DLL = "steamapps/common/Lossless Scaling/Lossless.dll"
|
||||
private const val UNCHANGED = -1
|
||||
|
||||
fun state(context: Context): State {
|
||||
val source = prefs(context).getString(KEY_SOURCE, null)
|
||||
?.let { name -> Source.entries.firstOrNull { it.name == name } } ?: Source.NONE
|
||||
return State(owned(context), cacheFile(context)?.isFile == true, source)
|
||||
}
|
||||
|
||||
/** Recorded once: by the session's ownership check (a flag in the guest's home) or a Steam copy. */
|
||||
fun owned(context: Context): Boolean {
|
||||
if (prefs(context).getBoolean(KEY_OWNED, false)) return true
|
||||
if (!ownedFlag(context).exists()) return false
|
||||
markOwned(context)
|
||||
return true
|
||||
}
|
||||
|
||||
fun cacheFile(context: Context): File? = prefs(context).getString(KEY_ID, null)?.let { cacheFor(context, it) }
|
||||
|
||||
/** Takes a new or updated Steam copy and rebuilds a missing or outdated cache. A [LsfgNative] status. */
|
||||
@Synchronized
|
||||
fun sync(context: Context): Int {
|
||||
val steam = steamDll(context)
|
||||
if (steam != null) {
|
||||
markOwned(context)
|
||||
val stamp = "${steam.length()}:${steam.lastModified()}"
|
||||
if (stamp != prefs(context).getString(KEY_STEAM_STAMP, null)) {
|
||||
val status = adopt(context, steam, Source.STEAM)
|
||||
if (status != LsfgNative.STATUS_OK && status != UNCHANGED) {
|
||||
Log.w(TAG, "Steam copy $steam not taken: ${statusName(status)}")
|
||||
}
|
||||
prefs(context).edit().putString(KEY_STEAM_STAMP, stamp).apply()
|
||||
}
|
||||
}
|
||||
return ensureBuilt(context)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun import(context: Context, dll: File): ImportResult {
|
||||
if (!owned(context)) return ImportResult.NOT_OWNED
|
||||
return when (adopt(context, dll, Source.IMPORT)) {
|
||||
UNCHANGED -> ImportResult.UNCHANGED
|
||||
LsfgNative.STATUS_OK -> ImportResult.UPDATED
|
||||
LsfgNative.STATUS_UNREADABLE_FILE, LsfgNative.STATUS_NOT_PORTABLE_EXECUTABLE,
|
||||
LsfgNative.STATUS_MISSING_SHADERS -> ImportResult.INVALID
|
||||
else -> ImportResult.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
fun message(context: Context, result: ImportResult): String = context.getString(
|
||||
when (result) {
|
||||
ImportResult.UPDATED -> R.string.lsfg_imported
|
||||
ImportResult.UNCHANGED -> R.string.lsfg_import_unchanged
|
||||
ImportResult.NOT_OWNED -> R.string.lsfg_import_not_owned
|
||||
ImportResult.INVALID -> R.string.lsfg_import_invalid
|
||||
ImportResult.FAILED -> R.string.lsfg_import_failed
|
||||
},
|
||||
)
|
||||
|
||||
private fun ensureBuilt(context: Context): Int {
|
||||
val id = prefs(context).getString(KEY_ID, null) ?: return LsfgNative.STATUS_NOT_INSTALLED
|
||||
val cache = cacheFor(context, id)
|
||||
if (usable(cache)) return LsfgNative.STATUS_OK
|
||||
val dll = storedDll(context)
|
||||
if (!dll.isFile) return LsfgNative.STATUS_NOT_INSTALLED
|
||||
return build(dll, cache)
|
||||
}
|
||||
|
||||
private fun adopt(context: Context, source: File, from: Source): Int {
|
||||
val staged = File(dir(context).apply { mkdirs() }, "Lossless.dll.staged")
|
||||
try {
|
||||
val id = copyHashing(source, staged)
|
||||
if (id == prefs(context).getString(KEY_ID, null) && usable(cacheFor(context, id))) return UNCHANGED
|
||||
val cache = cacheFor(context, id)
|
||||
var status = LsfgNative.nativeValidateDll(staged.path)
|
||||
if (status == LsfgNative.STATUS_OK) status = build(staged, cache)
|
||||
if (status != LsfgNative.STATUS_OK) {
|
||||
cache.delete()
|
||||
return status
|
||||
}
|
||||
if (!staged.renameTo(storedDll(context))) {
|
||||
cache.delete()
|
||||
return LsfgNative.STATUS_CACHE_UNUSABLE
|
||||
}
|
||||
prefs(context).edit().putString(KEY_ID, id).putString(KEY_SOURCE, from.name).apply()
|
||||
removeStale(context, id)
|
||||
Log.i(TAG, "Lossless.dll from $source ($from) ready as $id")
|
||||
return LsfgNative.STATUS_OK
|
||||
} catch (e: java.io.IOException) {
|
||||
Log.w(TAG, "could not copy $source", e)
|
||||
return LsfgNative.STATUS_UNREADABLE_FILE
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "could not take $source", t)
|
||||
return LsfgNative.STATUS_CACHE_UNUSABLE
|
||||
} finally {
|
||||
staged.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun build(dll: File, cache: File): Int = try {
|
||||
val status = LsfgNative.nativeBuildCache(dll.path, cache.path, true)
|
||||
Log.i(TAG, "shader cache ${cache.name}: ${statusName(status)}" +
|
||||
if (status == LsfgNative.STATUS_OK) " (${LsfgNative.nativeVariantName(LsfgNative.nativeCacheVariant(cache.path))})" else "")
|
||||
status
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "shader cache ${cache.name}", t)
|
||||
LsfgNative.STATUS_CACHE_UNUSABLE
|
||||
}
|
||||
|
||||
private fun usable(cache: File): Boolean = cache.isFile &&
|
||||
runCatching { LsfgNative.nativeCacheVariant(cache.path) != LsfgNative.VARIANT_NONE }.getOrDefault(false)
|
||||
|
||||
private fun copyHashing(source: File, target: File): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
source.inputStream().use { input ->
|
||||
DigestOutputStream(target.outputStream(), digest).use { output -> input.copyTo(output) }
|
||||
}
|
||||
return digest.digest().take(8).joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
private fun removeStale(context: Context, keep: String) {
|
||||
File(context.filesDir, "lsfg-native").deleteRecursively()
|
||||
dir(context).listFiles { f -> f.name.startsWith("shaders-") && f.name != cacheFor(context, keep).name }
|
||||
?.forEach { it.delete() }
|
||||
}
|
||||
|
||||
/** The newest copy in any library the client installs to: its own, the card's, the Games folders. */
|
||||
private fun steamDll(context: Context): File? {
|
||||
val libraries = listOfNotNull(
|
||||
File(LinuxRuntime.rootDir(context), "root/.local/share/Steam"),
|
||||
GameStorage.effective(context)?.path?.let(::File),
|
||||
) + SessionPrefs.addedGamesDirs(context).map(::File)
|
||||
return libraries.map { File(it, STEAM_DLL) }.filter { it.isFile }.maxByOrNull { it.lastModified() }
|
||||
}
|
||||
|
||||
private fun markOwned(context: Context) {
|
||||
if (!prefs(context).getBoolean(KEY_OWNED, false)) prefs(context).edit().putBoolean(KEY_OWNED, true).apply()
|
||||
}
|
||||
|
||||
private fun statusName(status: Int): String =
|
||||
runCatching { LsfgNative.nativeStatusName(status) }.getOrDefault("status $status")
|
||||
|
||||
private fun ownedFlag(context: Context) = File(LinuxRuntime.rootDir(context), "root/.config/droiddeck/lossless-owned")
|
||||
private fun dir(context: Context) = File(context.filesDir, "lsfg")
|
||||
private fun storedDll(context: Context) = File(dir(context), "Lossless.dll")
|
||||
private fun cacheFor(context: Context, id: String) = File(dir(context), "shaders-$id.cache")
|
||||
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
}
|
||||
@@ -1,99 +1,30 @@
|
||||
package com.droiddeck.launcher.gpu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* LSFG frame generation's shader chain: extracted from the user's own copy of Lossless Scaling
|
||||
* and cached on device.
|
||||
*
|
||||
* <p>The 25 compute shaders are not redistributable. They live as resources inside the user's
|
||||
* paid {@code Lossless.dll} (Steam app 993090), which in this app the user installs from the
|
||||
* Steam client itself - so the DLL sits in the runtime's own Steam library. It is mapped
|
||||
* read-only and parsed as data, never loaded or executed. Extraction is slow enough to be worth
|
||||
* doing once (the DXBC chain is translated to SPIR-V), so the result is cached, keyed on the
|
||||
* DLL's size and hash. Call {@link #ensureCache} off the main thread.
|
||||
*
|
||||
* <p>Ported from Bannerlator's LsfgNative; the native side is the same lsfg_jni.cpp compiled into
|
||||
* the compositor library.
|
||||
* The native half of LSFG's shader cache (lsfg_jni.cpp, compiled into the compositor library):
|
||||
* {@code Lossless.dll} is mapped read-only and parsed as data, never loaded or executed, and its
|
||||
* DXBC chain translated to SPIR-V. {@link Lossless} decides when; these calls block.
|
||||
*/
|
||||
public final class LsfgNative {
|
||||
private static final String TAG = "LsfgNative";
|
||||
|
||||
static { System.loadLibrary("bannerwayland"); }
|
||||
final class LsfgNative {
|
||||
static { System.loadLibrary("droiddeckwayland"); }
|
||||
|
||||
private LsfgNative() {}
|
||||
|
||||
// Mirrors lsfg::DllStatus.
|
||||
public static final int STATUS_OK = 0;
|
||||
public static final int STATUS_NOT_INSTALLED = 1;
|
||||
public static final int STATUS_UNREADABLE_FILE = 2;
|
||||
public static final int STATUS_NOT_PORTABLE_EXECUTABLE = 3;
|
||||
public static final int STATUS_MISSING_SHADERS = 4;
|
||||
public static final int STATUS_TRANSLATION_FAILED = 5;
|
||||
public static final int STATUS_CACHE_UNUSABLE = 6;
|
||||
static final int STATUS_OK = 0;
|
||||
static final int STATUS_NOT_INSTALLED = 1;
|
||||
static final int STATUS_UNREADABLE_FILE = 2;
|
||||
static final int STATUS_NOT_PORTABLE_EXECUTABLE = 3;
|
||||
static final int STATUS_MISSING_SHADERS = 4;
|
||||
static final int STATUS_TRANSLATION_FAILED = 5;
|
||||
static final int STATUS_CACHE_UNUSABLE = 6;
|
||||
|
||||
private static native int nativeValidateDll(String dllPath);
|
||||
private static native int nativeDllVariant(String dllPath, boolean preferFp16);
|
||||
private static native int nativeBuildCache(String dllPath, String cachePath, boolean preferFp16);
|
||||
private static native boolean nativeCacheMatchesSource(String cachePath, String dllPath);
|
||||
private static native int nativeCacheVariant(String cachePath);
|
||||
private static native String nativeStatusName(int status);
|
||||
private static native String nativeVariantName(int variant);
|
||||
// Mirrors lsfg::Variant.
|
||||
static final int VARIANT_NONE = 0;
|
||||
|
||||
/** Where Lossless Scaling lands when installed from the client inside the runtime. */
|
||||
public static File losslessDll(Context context) {
|
||||
return new File(LinuxRuntime.rootDir(context),
|
||||
"root/.local/share/Steam/steamapps/common/Lossless Scaling/Lossless.dll");
|
||||
}
|
||||
|
||||
/** Where the translated SPIR-V chain is cached. */
|
||||
public static File cacheFile(Context context) {
|
||||
return new File(context.getFilesDir(), "lsfg-native/shaders.cache");
|
||||
}
|
||||
|
||||
public static boolean isInstalled(Context context) {
|
||||
return losslessDll(context).isFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure a usable cache exists for the installed DLL, building it if the DLL is new or
|
||||
* changed. Returns a {@code STATUS_*} code; {@link #STATUS_OK} means the engine can start.
|
||||
*/
|
||||
public static int ensureCache(Context context) {
|
||||
File dll = losslessDll(context);
|
||||
if (!dll.isFile()) return STATUS_NOT_INSTALLED;
|
||||
File cache = cacheFile(context);
|
||||
try {
|
||||
if (cache.isFile() && nativeCacheMatchesSource(cache.getPath(), dll.getPath())) {
|
||||
return STATUS_OK;
|
||||
}
|
||||
int status = nativeValidateDll(dll.getPath());
|
||||
if (status != STATUS_OK) {
|
||||
Log.w(TAG, "Lossless.dll rejected: " + statusName(status));
|
||||
return status;
|
||||
}
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
cache.getParentFile().mkdirs();
|
||||
status = nativeBuildCache(dll.getPath(), cache.getPath(), true);
|
||||
Log.i(TAG, "cache build: " + statusName(status)
|
||||
+ (status == STATUS_OK ? " (" + nativeVariantName(nativeCacheVariant(cache.getPath())) + ")" : ""));
|
||||
return status;
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "ensureCache", t);
|
||||
return STATUS_CACHE_UNUSABLE;
|
||||
}
|
||||
}
|
||||
|
||||
public static String statusName(int status) {
|
||||
try {
|
||||
return nativeStatusName(status);
|
||||
} catch (Throwable t) {
|
||||
return "status " + status;
|
||||
}
|
||||
}
|
||||
static native int nativeValidateDll(String dllPath);
|
||||
static native int nativeBuildCache(String dllPath, String cachePath, boolean preferFp16);
|
||||
static native int nativeCacheVariant(String cachePath);
|
||||
static native String nativeStatusName(int status);
|
||||
static native String nativeVariantName(int variant);
|
||||
}
|
||||
Loaded 100 of 350 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user