mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-17 20:57:11 -04:00
Compare commits
185
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef261d9724 | ||
|
|
9310d19857 | ||
|
|
4454a6d39a | ||
|
|
453f6f5180 | ||
|
|
6d0484941e | ||
|
|
cf47910881 | ||
|
|
4f0f0cf5b1 | ||
|
|
a78d5cb600 | ||
|
|
206c8a8846 | ||
|
|
bf17ce1151 | ||
|
|
ef38c975b9 | ||
|
|
61953c4f7b | ||
|
|
1f13285ab8 | ||
|
|
1bc23ac169 | ||
|
|
50f50b16e0 | ||
|
|
adc86fc7df | ||
|
|
f3fd3a2307 | ||
|
|
1e877fa602 | ||
|
|
906009a33a | ||
|
|
08559a02f8 | ||
|
|
1d93626453 | ||
|
|
9d0989374b | ||
|
|
3278afaa1a | ||
|
|
bf221ffbbd | ||
|
|
c636eea355 | ||
|
|
ffaa906cd1 | ||
|
|
d61af961c1 | ||
|
|
bc01b77752 | ||
|
|
a1b01a04a8 | ||
|
|
b44235c495 | ||
|
|
b80d5db176 | ||
|
|
d3c0d258b7 | ||
|
|
9dcdbf245a | ||
|
|
369e218afb | ||
|
|
119d24bf6b | ||
|
|
2a905510d5 | ||
|
|
da29919b5a | ||
|
|
05608ac4d4 | ||
|
|
135b84a501 | ||
|
|
e7cbba37d1 | ||
|
|
d9bb5e15b0 | ||
|
|
46cdc32cf1 | ||
|
|
387572e733 | ||
|
|
9f46c7a50b | ||
|
|
0607bb0b3a | ||
|
|
b89d87cbde | ||
|
|
06e06b9673 | ||
|
|
b896e7a3b9 | ||
|
|
c91daa9147 | ||
|
|
31ea194f3d | ||
|
|
472bb28c88 | ||
|
|
eaa4a7a047 | ||
|
|
4fa467c800 | ||
|
|
beb96e6f0c | ||
|
|
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 | ||
|
|
0e1f6f15a1 | ||
|
|
48054a86ab | ||
|
|
38d658abf5 | ||
|
|
5547391cfc | ||
|
|
05933a5ee3 | ||
|
|
c96f2d1070 | ||
|
|
f58bf9b23c | ||
|
|
d59734e375 | ||
|
|
8cfd54a739 |
No files matched your search
@@ -6,6 +6,7 @@ on:
|
||||
push:
|
||||
paths:
|
||||
- 'tools/aaudio-sink/**'
|
||||
- 'tools/directaudio-relay/**'
|
||||
- '.github/workflows/build-aaudio-sink.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -25,6 +26,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
||||
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/out"
|
||||
tools/directaudio-relay/build.sh "$PWD/out"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: audio-sink-modules
|
||||
|
||||
@@ -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
|
||||
+122
-8
@@ -28,7 +28,7 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
PRELOAD_CACHE_VERSION: v2
|
||||
PRELOAD_CACHE_VERSION: v3
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -57,6 +57,22 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Version bumps are isolated
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin main
|
||||
base=$(git merge-base origin/main HEAD)
|
||||
diff=$(git diff -U0 "$base..HEAD" -- app/build.gradle)
|
||||
grep -Eq '^[+-][[:space:]]*version(Code|Name)[[:space:]]' <<<"$diff" || exit 0
|
||||
other_files=$(git diff --name-only "$base..HEAD" | grep -v '^app/build.gradle$' || true)
|
||||
other_gradle=$(grep -E '^[+-]' <<<"$diff" | grep -Ev '^(---|\+\+\+)' | grep -Ev '^[+-][[:space:]]*version(Code|Name)[[:space:]]' || true)
|
||||
if [[ -n "$other_files" || -n "$other_gradle" ]]; then
|
||||
echo "::error::versionCode/versionName changes must be isolated in their own PR"
|
||||
echo "A version PR may only change the versionCode/versionName lines in app/build.gradle." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Merge onto main, or say what conflicts
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
@@ -157,7 +173,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/linuxfs/ssbs/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*', 'tools/msitools/**') }}
|
||||
|
||||
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
@@ -176,7 +192,17 @@ 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"
|
||||
# Force-SSBS adapter, preloaded into a game's Wine only when the game asks for it.
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libssbs.so"
|
||||
mkdir -p "$D/usr/local/bin"
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard"
|
||||
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
|
||||
@@ -184,7 +210,7 @@ jobs:
|
||||
done
|
||||
if [ -f tools/gamescope/release.env ]; then
|
||||
. tools/gamescope/release.env
|
||||
gh release download "${GAMESCOPE_TAG}" -R "${GITHUB_REPOSITORY}" -p gamescope.tzst -O gamescope.tzst
|
||||
gh release download "${GAMESCOPE_TAG}" -R "${GAMESCOPE_REPO:-${GITHUB_REPOSITORY}}" -p gamescope.tzst -O gamescope.tzst
|
||||
echo "${GAMESCOPE_SHA256} gamescope.tzst" | sha256sum -c -
|
||||
tar --use-compress-program=unzstd -xf gamescope.tzst -C "$D"
|
||||
test -f "$D/usr/local/bin/gamescope"
|
||||
@@ -198,6 +224,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
|
||||
@@ -208,12 +240,36 @@ 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"
|
||||
# The Windows components installer's tools (tools/msitools/packages.txt).
|
||||
T="$D/usr/local/lib/droiddeck-msitools"
|
||||
mkdir -p "$T" msi-pkgs
|
||||
grep -v '^#' tools/msitools/packages.txt | while read -r sum url; do
|
||||
[ -n "$url" ] || continue
|
||||
curl -fsSL --retry 3 -o msi-pkgs/pkg.tar.xz "$url"
|
||||
echo "$sum msi-pkgs/pkg.tar.xz" | sha256sum -c -
|
||||
tar -xJf msi-pkgs/pkg.tar.xz -C msi-pkgs
|
||||
rm msi-pkgs/pkg.tar.xz
|
||||
done
|
||||
install -m644 msi-pkgs/usr/bin/msiinfo "$T/msiinfo"
|
||||
install -m644 msi-pkgs/usr/bin/cabextract "$T/cabextract"
|
||||
for so in libmsi-1.0.so.0 libgsf-1.so.114 libgcab-1.0.so.0; do
|
||||
cp -L "msi-pkgs/usr/lib/$so" "$T/$so"
|
||||
done
|
||||
for f in NOTICE GPL-2 GPL-3 LGPL-2.1; do
|
||||
install -Dm644 "tools/msitools/licenses/$f" "$D/usr/local/share/licenses/droiddeck-msitools/$f"
|
||||
done
|
||||
rm -rf msi-pkgs
|
||||
ls -l "$T"
|
||||
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop "$D/usr/local/bin/droiddeck-desktop"
|
||||
install -Dm644 tools/linuxfs/desktop/droiddeck-gpu "$D/usr/local/bin/droiddeck-gpu"
|
||||
install -Dm644 tools/linuxfs/desktop/droiddeck-desktop-gpu "$D/usr/local/bin/droiddeck-desktop-gpu"
|
||||
@@ -223,6 +279,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
|
||||
@@ -249,6 +345,11 @@ jobs:
|
||||
mv pulseaudio.tzst app/src/main/assets/pulseaudio.tzst
|
||||
tar --use-compress-program=unzstd -tf app/src/main/assets/pulseaudio.tzst | grep -c '\.so$'
|
||||
rm -rf pa-bundle sink-out
|
||||
- name: Build the DirectAudio relay
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
||||
tools/directaudio-relay/build.sh app/src/main/jniLibs/arm64-v8a
|
||||
|
||||
- name: Cache proot
|
||||
id: proot
|
||||
@@ -287,11 +388,23 @@ 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 drmPrimeFDToHandle drmCloseBufferHandle; 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"
|
||||
# Zink's GEM_CLOSE is caught at ioctl(); an exported drmIoctl made NMS replay old frames.
|
||||
if grep -qx drmIoctl <<<"$SESSION"; then
|
||||
echo "::error::libblsession.so exports drmIoctl"; exit 1
|
||||
fi
|
||||
SSBS=$(syms "$D/libssbs.so")
|
||||
for sym in sigaction signal bsd_signal sysv_signal; do
|
||||
grep -qx "$sym" <<<"$SSBS" || { echo "::error::libssbs.so does not export $sym"; exit 1; }
|
||||
done
|
||||
test -f "$D/usr/local/bin/droiddeck-session"
|
||||
test -f "$D/usr/local/bin/droiddeck-proton-extra"
|
||||
test -f "$D/usr/local/bin/droiddeck-msi-install"
|
||||
for f in msiinfo cabextract libmsi-1.0.so.0 libgsf-1.so.114 libgcab-1.0.so.0; do
|
||||
test -f "$D/usr/local/lib/droiddeck-msitools/$f" || { echo "::error::droiddeck-msitools/$f missing"; exit 1; }
|
||||
done
|
||||
|
||||
- name: Cache the CMake build tree
|
||||
uses: actions/cache@v4
|
||||
@@ -310,6 +423,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'
|
||||
|
||||
@@ -88,7 +88,8 @@ jobs:
|
||||
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")
|
||||
|
||||
@@ -114,36 +115,39 @@ jobs:
|
||||
sleep 30
|
||||
done
|
||||
[[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; }
|
||||
# GitHub leaves a run's pull_requests empty when its head is in a fork, so the build is
|
||||
# tied to this PR by its head repository, branch and commit instead.
|
||||
run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id")
|
||||
jq -e --arg head "$head" --arg 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
|
||||
.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/$GITHUB_REPOSITORY/releases/latest" --jq .tag_name)
|
||||
mkdir -p "$RUNNER_TEMP/stable" "$RUNNER_TEMP/pr"
|
||||
gh release download "$stable_tag" -R "$GITHUB_REPOSITORY" \
|
||||
-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
|
||||
' <<<"$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"
|
||||
|
||||
@@ -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
|
||||
@@ -14,6 +14,8 @@ 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/jniLibs/arm64-v8a/libdirectaudiorelay.so
|
||||
app/src/main/assets/droiddeck-esync/
|
||||
|
||||
# Kotlin compiler session files (build-tool state, never source).
|
||||
.kotlin/
|
||||
|
||||
@@ -11,11 +11,13 @@ DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big
|
||||
|
||||
<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/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; see [docs/development/flatpak.md](docs/development/flatpak.md).
|
||||
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts.
|
||||
|
||||
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
|
||||
|
||||
@@ -31,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). 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. 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.
|
||||
+17
-2
@@ -79,8 +79,8 @@ 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 11
|
||||
versionName "0.3.1"
|
||||
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
|
||||
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
|
||||
buildConfigField "int", "PR_NUMBER", "${prNumber}"
|
||||
@@ -165,6 +165,8 @@ android {
|
||||
testOptions {
|
||||
// Plain JVM tests: a stray android.util.Log call returns a default instead of throwing.
|
||||
unitTests.returnDefaultValues = true
|
||||
// Robolectric tests start activities whose text is now all string resources.
|
||||
unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -176,10 +178,22 @@ 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'
|
||||
implementation 'org.apache.commons:commons-compress:1.27.1'
|
||||
// Bannerlator's Windows components come as .tar.xz (WinComponents).
|
||||
implementation 'org.tukaani:xz:1.10'
|
||||
// The UI: every menu, dialog and overlay is Compose. The session's SurfaceView and the
|
||||
// on-screen pad are Views underneath it, which is what they are in every Compose app.
|
||||
implementation platform('androidx.compose:compose-bom:2024.02.00')
|
||||
@@ -190,6 +204,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.
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<!-- Debug builds only. Android grants DUMP to adb shell; release APKs have no bridge. -->
|
||||
<activity
|
||||
android:name=".agent.AgentStartActivity"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="true"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:theme="@style/SessionTheme" />
|
||||
<provider
|
||||
android:name=".agent.AgentBridgeProvider"
|
||||
android:authorities="${applicationId}.agent"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:grantUriPermissions="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -4,7 +4,6 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- Session logs go to Downloads so a failed run can be handed over as one folder. -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@@ -13,6 +12,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
|
||||
@@ -30,6 +33,7 @@
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -56,6 +60,20 @@
|
||||
<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>
|
||||
<intent-filter>
|
||||
<action android:name="com.droiddeck.launcher.LAUNCH_GAME" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Android owns the Home key. This small translucent Home handler routes the resulting
|
||||
@@ -108,10 +126,19 @@
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".agent.AgentStartActivity"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="true"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:theme="@style/SessionTheme" />
|
||||
|
||||
<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"
|
||||
@@ -157,5 +184,11 @@
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
<provider
|
||||
android:name=".agent.AgentBridgeProvider"
|
||||
android:authorities="${applicationId}.agent"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:grantUriPermissions="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -44,6 +44,11 @@ add_subdirectory(waylandcomp)
|
||||
add_library(sdnative SHARED sdnative/sdnative.c)
|
||||
target_link_libraries(sdnative log)
|
||||
|
||||
# The device info window's Vulkan section: the phone's own driver, asked once through the system
|
||||
# loader (opened with dlopen, so a phone without Vulkan still loads the app).
|
||||
add_library(deviceinfo SHARED deviceinfo/vkinfo.c)
|
||||
target_link_libraries(deviceinfo dl)
|
||||
|
||||
# Rebuild Termux terminal-emulator's PTY JNI helper with the app's NDK instead of carrying its
|
||||
# prebuilt libtermux.so with a redundant libstdc++.so dependency.
|
||||
add_library(termux SHARED termux/termux.c)
|
||||
@@ -0,0 +1,112 @@
|
||||
// What the phone's own Vulkan driver says about its GPU, for the app's device info window: one
|
||||
// instance on the system loader, the first physical device's properties, a few limits and the
|
||||
// extensions DXVK and VKD3D-Proton lean on. Returned as "key=value" lines; nothing is kept open.
|
||||
#include <dlfcn.h>
|
||||
#include <jni.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#define VK_NO_PROTOTYPES
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#define LINE(...) do { if (used < sizeof(out)) used += snprintf(out + used, sizeof(out) - used, __VA_ARGS__); } while (0)
|
||||
|
||||
static const char *const EXTENSIONS[] = {
|
||||
"VK_EXT_robustness2", "VK_EXT_transform_feedback", "VK_EXT_custom_border_color",
|
||||
"VK_KHR_dynamic_rendering", "VK_EXT_extended_dynamic_state3", "VK_EXT_descriptor_indexing",
|
||||
"VK_KHR_timeline_semaphore", "VK_EXT_mesh_shader", "VK_KHR_ray_query",
|
||||
};
|
||||
|
||||
static const char *type_name(VkPhysicalDeviceType type) {
|
||||
switch (type) {
|
||||
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "integrated GPU";
|
||||
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "discrete GPU";
|
||||
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "virtual GPU";
|
||||
case VK_PHYSICAL_DEVICE_TYPE_CPU: return "CPU";
|
||||
default: return "other";
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz) {
|
||||
static char out[4096];
|
||||
size_t used = 0;
|
||||
out[0] = 0;
|
||||
void *lib = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (!lib) { LINE("error=no Vulkan loader\n"); return (*env)->NewStringUTF(env, out); }
|
||||
PFN_vkGetInstanceProcAddr gipa = (PFN_vkGetInstanceProcAddr) dlsym(lib, "vkGetInstanceProcAddr");
|
||||
PFN_vkCreateInstance create = gipa ? (PFN_vkCreateInstance) gipa(NULL, "vkCreateInstance") : NULL;
|
||||
PFN_vkEnumerateInstanceVersion instanceVersion = gipa ? (PFN_vkEnumerateInstanceVersion) gipa(NULL, "vkEnumerateInstanceVersion") : NULL;
|
||||
if (!create) { LINE("error=no vkCreateInstance\n"); dlclose(lib); return (*env)->NewStringUTF(env, out); }
|
||||
uint32_t loader = VK_API_VERSION_1_0;
|
||||
if (instanceVersion) instanceVersion(&loader);
|
||||
LINE("loader=%u.%u.%u\n", VK_API_VERSION_MAJOR(loader), VK_API_VERSION_MINOR(loader), VK_API_VERSION_PATCH(loader));
|
||||
|
||||
VkApplicationInfo app = { .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "DroidDeck device info",
|
||||
.apiVersion = loader >= VK_API_VERSION_1_1 ? VK_API_VERSION_1_1 : VK_API_VERSION_1_0 };
|
||||
VkInstanceCreateInfo info = { .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app };
|
||||
VkInstance instance = VK_NULL_HANDLE;
|
||||
if (create(&info, NULL, &instance) != VK_SUCCESS) { LINE("error=vkCreateInstance failed\n"); dlclose(lib); return (*env)->NewStringUTF(env, out); }
|
||||
|
||||
#define GET(name) PFN_##name name = (PFN_##name) gipa(instance, #name)
|
||||
GET(vkDestroyInstance); GET(vkEnumeratePhysicalDevices); GET(vkGetPhysicalDeviceProperties);
|
||||
GET(vkGetPhysicalDeviceProperties2); GET(vkGetPhysicalDeviceMemoryProperties);
|
||||
GET(vkEnumerateDeviceExtensionProperties); GET(vkGetPhysicalDeviceFeatures);
|
||||
uint32_t count = 1;
|
||||
VkPhysicalDevice device = VK_NULL_HANDLE;
|
||||
VkResult listed = vkEnumeratePhysicalDevices ? vkEnumeratePhysicalDevices(instance, &count, &device) : VK_ERROR_INITIALIZATION_FAILED;
|
||||
if ((listed != VK_SUCCESS && listed != VK_INCOMPLETE) || count == 0 || !device || !vkGetPhysicalDeviceProperties) {
|
||||
LINE("error=no Vulkan device\n");
|
||||
} else {
|
||||
VkPhysicalDeviceProperties p;
|
||||
vkGetPhysicalDeviceProperties(device, &p);
|
||||
LINE("device=%s\n", p.deviceName);
|
||||
LINE("type=%s\n", type_name(p.deviceType));
|
||||
LINE("api=%u.%u.%u\n", VK_API_VERSION_MAJOR(p.apiVersion), VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion));
|
||||
// Qualcomm packs its driver version as 10.10.12 bits, the same layout as VK_MAKE_VERSION.
|
||||
LINE("driver_version=%u.%u.%u\n", p.driverVersion >> 22, (p.driverVersion >> 12) & 0x3ff, p.driverVersion & 0xfff);
|
||||
LINE("vendor_id=0x%04x\ndevice_id=0x%08x\n", p.vendorID, p.deviceID);
|
||||
LINE("max_image_2d=%u\n", p.limits.maxImageDimension2D);
|
||||
|
||||
// Driver name and build string (Vulkan 1.2 or VK_KHR_driver_properties).
|
||||
uint32_t extCount = 0;
|
||||
if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL);
|
||||
static VkExtensionProperties exts[512];
|
||||
if (extCount > 512) extCount = 512;
|
||||
if (extCount && vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, exts);
|
||||
int driverProps = p.apiVersion >= VK_API_VERSION_1_2;
|
||||
for (uint32_t i = 0; i < extCount; i++)
|
||||
if (!strcmp(exts[i].extensionName, "VK_KHR_driver_properties")) driverProps = 1;
|
||||
if (driverProps && vkGetPhysicalDeviceProperties2) {
|
||||
VkPhysicalDeviceDriverProperties driver = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES };
|
||||
VkPhysicalDeviceProperties2 p2 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &driver };
|
||||
vkGetPhysicalDeviceProperties2(device, &p2);
|
||||
LINE("driver_name=%s\n", driver.driverName);
|
||||
LINE("driver_info=%s\n", driver.driverInfo);
|
||||
LINE("conformance=%u.%u.%u.%u\n", driver.conformanceVersion.major, driver.conformanceVersion.minor,
|
||||
driver.conformanceVersion.subminor, driver.conformanceVersion.patch);
|
||||
}
|
||||
LINE("extension_count=%u\n", extCount);
|
||||
for (size_t e = 0; e < sizeof(EXTENSIONS) / sizeof(*EXTENSIONS); e++) {
|
||||
int has = 0;
|
||||
for (uint32_t i = 0; i < extCount && !has; i++) has = !strcmp(exts[i].extensionName, EXTENSIONS[e]);
|
||||
LINE("ext.%s=%s\n", EXTENSIONS[e], has ? "yes" : "no");
|
||||
}
|
||||
if (vkGetPhysicalDeviceFeatures) {
|
||||
VkPhysicalDeviceFeatures f;
|
||||
vkGetPhysicalDeviceFeatures(device, &f);
|
||||
LINE("geometry_shader=%s\ntessellation_shader=%s\ntexture_bc=%s\n",
|
||||
f.geometryShader ? "yes" : "no", f.tessellationShader ? "yes" : "no", f.textureCompressionBC ? "yes" : "no");
|
||||
}
|
||||
if (vkGetPhysicalDeviceMemoryProperties) {
|
||||
VkPhysicalDeviceMemoryProperties m;
|
||||
vkGetPhysicalDeviceMemoryProperties(device, &m);
|
||||
VkDeviceSize largest = 0;
|
||||
for (uint32_t i = 0; i < m.memoryHeapCount; i++)
|
||||
if ((m.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) && m.memoryHeaps[i].size > largest) largest = m.memoryHeaps[i].size;
|
||||
LINE("device_heap_mb=%llu\n", (unsigned long long) (largest >> 20));
|
||||
}
|
||||
}
|
||||
if (vkDestroyInstance) vkDestroyInstance(instance, NULL);
|
||||
dlclose(lib);
|
||||
return (*env)->NewStringUTF(env, out);
|
||||
}
|
||||
@@ -54,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.
|
||||
@@ -1717,6 +1717,81 @@ __attribute__((visibility("hidden"))) static std::string deck_listing_for(const
|
||||
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);
|
||||
@@ -1876,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;
|
||||
@@ -1914,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;
|
||||
@@ -1927,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)) {
|
||||
@@ -1947,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;
|
||||
@@ -2792,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,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;
|
||||
@@ -544,13 +544,13 @@ static void cursor_publish_shm(struct wl_shm_buffer *shm, int hx, int hy) {
|
||||
/* 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 banner_cursor_serial(void) {
|
||||
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);
|
||||
@@ -650,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 */
|
||||
@@ -667,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
|
||||
@@ -750,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) {
|
||||
@@ -785,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;
|
||||
@@ -801,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
|
||||
@@ -828,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)");
|
||||
@@ -852,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 */
|
||||
@@ -1132,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);
|
||||
@@ -1182,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);
|
||||
@@ -1434,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) {}
|
||||
@@ -1611,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,
|
||||
@@ -1621,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;
|
||||
@@ -1664,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);
|
||||
@@ -1699,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 */
|
||||
@@ -1728,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]; }
|
||||
@@ -1848,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
|
||||
@@ -1864,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.
|
||||
@@ -1874,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 */
|
||||
};
|
||||
@@ -1889,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; }
|
||||
@@ -1940,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;
|
||||
@@ -1949,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;
|
||||
}
|
||||
}
|
||||
@@ -1985,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;
|
||||
}
|
||||
}
|
||||
@@ -2005,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2021,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;
|
||||
}
|
||||
@@ -2045,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);
|
||||
}
|
||||
@@ -2056,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");
|
||||
}
|
||||
}
|
||||
@@ -2071,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) {
|
||||
@@ -2101,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. */
|
||||
@@ -2118,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");
|
||||
}
|
||||
@@ -2145,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
|
||||
@@ -2185,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++;
|
||||
@@ -2236,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();
|
||||
@@ -2251,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 */
|
||||
@@ -2360,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) {
|
||||
@@ -2439,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
|
||||
@@ -2492,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) {
|
||||
@@ -2560,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();
|
||||
}
|
||||
@@ -2580,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);
|
||||
@@ -2642,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) {
|
||||
@@ -2723,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);
|
||||
}
|
||||
|
||||
@@ -2754,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);
|
||||
}
|
||||
@@ -2773,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,
|
||||
@@ -2880,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);
|
||||
}
|
||||
@@ -3024,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 */
|
||||
@@ -3103,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);
|
||||
}
|
||||
@@ -3167,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;
|
||||
@@ -3210,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));
|
||||
@@ -3225,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));
|
||||
@@ -3233,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));
|
||||
@@ -3243,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));
|
||||
@@ -3251,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;
|
||||
@@ -3268,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 */
|
||||
|
||||
@@ -3301,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
|
||||
@@ -3313,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",
|
||||
@@ -3329,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. */
|
||||
@@ -3337,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));
|
||||
@@ -3363,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);
|
||||
@@ -3426,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);
|
||||
@@ -3458,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);
|
||||
@@ -3475,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,9 +11,9 @@
|
||||
#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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -16,20 +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 int banner_cursor_serial(void);
|
||||
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;
|
||||
@@ -39,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 */
|
||||
@@ -80,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;
|
||||
@@ -111,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;
|
||||
@@ -124,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 : "");
|
||||
@@ -134,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);
|
||||
@@ -144,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);
|
||||
@@ -153,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);
|
||||
@@ -166,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 : "");
|
||||
@@ -195,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;
|
||||
@@ -272,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).
|
||||
@@ -291,7 +291,7 @@ 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;
|
||||
}
|
||||
@@ -300,27 +300,27 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot(
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSerial(JNIEnv *env, jclass clazz) {
|
||||
(void)env; (void)clazz;
|
||||
return banner_cursor_serial();
|
||||
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. */
|
||||
@@ -329,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)" : "");
|
||||
}
|
||||
|
||||
@@ -355,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) {
|
||||
@@ -363,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) {
|
||||
@@ -452,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);
|
||||
}
|
||||
|
||||
@@ -466,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);
|
||||
}
|
||||
|
||||
@@ -487,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);
|
||||
}
|
||||
@@ -496,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);
|
||||
}
|
||||
|
||||
@@ -536,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);
|
||||
}
|
||||
@@ -544,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. */
|
||||
@@ -651,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);
|
||||
}
|
||||
|
||||
@@ -660,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);
|
||||
}
|
||||
|
||||
@@ -670,14 +670,14 @@ 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
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -11,14 +11,23 @@ import com.droiddeck.launcher.session.GpuClockPin
|
||||
* sandboxes are started through (BwrapSpawner).
|
||||
*/
|
||||
class App : Application() {
|
||||
override fun attachBaseContext(base: android.content.Context) =
|
||||
super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(base))
|
||||
|
||||
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
com.droiddeck.launcher.core.AppLanguage.systemChanged(newConfig)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CrashHandler.install(this)
|
||||
// Before anything else touches the session prefs: a new install's first read decides it.
|
||||
// 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
|
||||
@@ -20,6 +21,7 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
var compChecking by mutableStateOf(false)
|
||||
var compBusy by mutableStateOf<String?>(null)
|
||||
var compDownloads by mutableStateOf<Map<String, Int>>(emptyMap())
|
||||
private var shownDefault: String? = null
|
||||
|
||||
/** Reads the Protons (and on first open saves their originals) off the UI thread; the Nightlies list comes from its cache. */
|
||||
fun refreshComponents(snapshotFirst: Boolean = false) {
|
||||
@@ -28,19 +30,40 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
runCatching { ComponentsManager.applyQueued(activity) }
|
||||
val snap = runCatching { ComponentsManager.snapshot(activity) }.onFailure { Log.w(TAG, "components", it) }.getOrNull()
|
||||
val cat = runCatching { ComponentsManager.catalog(activity, false) }.getOrNull()
|
||||
val chosen = snap?.let { s -> runCatching { ProtonDefault.selectedId(activity, s.protons.map { it.proton }) }.getOrNull() }
|
||||
ui.post {
|
||||
compSnapshot = snap ?: ComponentsManager.Snapshot(emptyList(), emptyList())
|
||||
if (cat != null && cat.fetchedAt > 0) { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
|
||||
if (compProton == null || snap?.protons?.none { it.proton.id == compProton } == true) compProton = snap?.protons?.firstOrNull()?.proton?.id
|
||||
if (chosen != null && chosen != shownDefault) {
|
||||
shownDefault = chosen
|
||||
compProton = chosen
|
||||
} else if (compProton == null || snap?.protons?.none { it.proton.id == compProton } == true) {
|
||||
compProton = chosen ?: snap?.protons?.firstOrNull()?.proton?.id
|
||||
}
|
||||
}
|
||||
}, "components").start()
|
||||
}
|
||||
|
||||
fun chooseProton(id: String) {
|
||||
val proton = compSnapshot?.protons?.firstOrNull { it.proton.id == id }?.proton ?: return
|
||||
compProton = id
|
||||
Thread({
|
||||
val message = runCatching {
|
||||
when (ProtonDefault.request(activity, proton)) {
|
||||
ProtonDefault.Outcome.LIVE -> activity.getString(R.string.comp_default_live, proton.name)
|
||||
ProtonDefault.Outcome.NEXT_START -> activity.getString(R.string.comp_default_next, proton.name)
|
||||
ProtonDefault.Outcome.NOT_RUNNABLE -> activity.getString(R.string.comp_default_not_runnable, proton.name)
|
||||
}
|
||||
}.getOrElse { e -> activity.getString(R.string.comp_default_failed, e.message ?: e.javaClass.simpleName) }
|
||||
ui.post { android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show() }
|
||||
}, "components-default").start()
|
||||
}
|
||||
|
||||
fun componentAction(label: String, work: () -> String) {
|
||||
if (compBusy != null) return
|
||||
compBusy = label
|
||||
Thread({
|
||||
val message = runCatching(work).getOrElse { e -> "$label failed: ${e.message ?: e.javaClass.simpleName}" }
|
||||
val message = runCatching(work).getOrElse { e -> activity.getString(R.string.main_action_failed, label, e.message ?: e.javaClass.simpleName) }
|
||||
ui.post {
|
||||
compBusy = null
|
||||
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
@@ -56,7 +79,7 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
val cat = runCatching { ComponentsManager.catalog(activity, true) }.getOrNull()
|
||||
ui.post {
|
||||
compChecking = false
|
||||
if (cat == null || cat.items.isEmpty()) android.widget.Toast.makeText(activity, "The Nightlies could not be reached", android.widget.Toast.LENGTH_LONG).show()
|
||||
if (cat == null || cat.items.isEmpty()) android.widget.Toast.makeText(activity, activity.getString(R.string.comp_nightlies_unreachable), android.widget.Toast.LENGTH_LONG).show()
|
||||
else { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
|
||||
}
|
||||
}, "components-catalog").start()
|
||||
@@ -68,8 +91,8 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
Thread({
|
||||
val message = runCatching {
|
||||
val pkg = ComponentsManager.download(activity, item) { pc -> ui.post { if (compDownloads.containsKey(item.file)) compDownloads = compDownloads + (item.file to pc) } }
|
||||
"Stored ${pkg.version}"
|
||||
}.getOrElse { e -> "Download failed: ${e.message ?: e.javaClass.simpleName}" }
|
||||
activity.getString(R.string.comp_stored, pkg.version)
|
||||
}.getOrElse { e -> activity.getString(R.string.comp_download_failed, e.message ?: e.javaClass.simpleName) }
|
||||
ui.post {
|
||||
compDownloads = compDownloads - item.file
|
||||
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_SHORT).show()
|
||||
|
||||
@@ -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
|
||||
@@ -18,6 +19,8 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
var deckyChecking by mutableStateOf(false)
|
||||
var deckyStage by mutableStateOf<String?>(null)
|
||||
var deckyPercent by mutableIntStateOf(-1)
|
||||
/** True while the stage is the plugin's binary download (DeckyManager.downloadingBinary). */
|
||||
var deckyDownloadingBinary by mutableStateOf(false)
|
||||
var deckySupervisor by mutableStateOf(false)
|
||||
var deckyReleaseRequest = 0
|
||||
|
||||
@@ -40,15 +43,33 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
|
||||
fun installDecky(release: DeckyManager.Release) {
|
||||
if (deckyStage != null || SessionState.running) return
|
||||
deckyStage = "Starting…"; deckyPercent = -1
|
||||
deckyStage = activity.getString(R.string.store_starting); deckyPercent = -1
|
||||
Thread({
|
||||
val problem = runCatching {
|
||||
DeckyManager.install(activity, release) { label, value -> ui.post { deckyStage = label; deckyPercent = value } }
|
||||
}.getOrElse { error -> "Decky install failed: ${error.message ?: error.javaClass.simpleName}" }
|
||||
}.getOrElse { error -> activity.getString(R.string.decky_install_failed, error.message ?: error.javaClass.simpleName) }
|
||||
ui.post {
|
||||
deckyStage = null; deckyPercent = -1; deckyInstalled = DeckyManager.installed(activity)
|
||||
if (problem != null) android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "install-decky").start()
|
||||
}
|
||||
|
||||
fun importPluginZip(archive: File) {
|
||||
if (deckyStage != null || SessionState.running || deckyInstalled == null) return
|
||||
deckyStage = activity.getString(R.string.decky_plugin_starting); deckyPercent = -1
|
||||
Thread({
|
||||
val problem = runCatching {
|
||||
DeckyManager.installPluginZip(activity, archive) { label, value ->
|
||||
val binary = DeckyManager.downloadingBinary
|
||||
ui.post { deckyStage = label; deckyPercent = value; deckyDownloadingBinary = binary }
|
||||
}
|
||||
}.getOrElse { error -> activity.getString(R.string.decky_plugin_failed, error.message ?: error.javaClass.simpleName) }
|
||||
ui.post {
|
||||
deckyStage = null; deckyPercent = -1; deckyDownloadingBinary = false
|
||||
val message = problem ?: activity.getString(R.string.decky_plugin_installed)
|
||||
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "import-decky-plugin").start()
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
/** The latest Banners-Turnip release as each driver menu offers it (see [refreshReleaseRows]). */
|
||||
var linuxDownloads by mutableStateOf<List<com.droiddeck.launcher.ui.DownloadRow>>(emptyList())
|
||||
var androidDownloads by mutableStateOf<List<com.droiddeck.launcher.ui.DownloadRow>>(emptyList())
|
||||
var releaseStatus by mutableStateOf("Not checked yet - tap refresh to look for new drivers")
|
||||
// Empty until the first refresh: the activity has no context yet while its fields are made.
|
||||
var releaseStatus by mutableStateOf("")
|
||||
var releaseChecking by mutableStateOf(false)
|
||||
var canRestoreBundled by mutableStateOf(false)
|
||||
/** Asset name -> download percent, while it downloads. */
|
||||
@@ -57,10 +58,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
private var autoCheckedThisProcess = false
|
||||
|
||||
fun state() = GpuDriversState(
|
||||
gpuName = gpu.name, gpuFamily = gpu.family.label, soc = gpu.soc, supportText = gpu.supportText,
|
||||
gpuName = gpu.displayName(activity), gpuFamily = gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity),
|
||||
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,
|
||||
autoStatus = autoStatus, releaseStatus = releaseStatus.ifEmpty { activity.getString(R.string.drivers_not_checked) }, checking = releaseChecking,
|
||||
linuxRows = linuxRows, linuxSelected = linuxSelected, androidRows = androidRows, androidSelected = androidSelected,
|
||||
linuxDownloads = linuxDownloads, androidDownloads = androidDownloads, canRestoreBundled = canRestoreBundled,
|
||||
activeBundle = activeBundle,
|
||||
@@ -73,10 +74,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
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)
|
||||
DriverRow(id, it.label, activity.getString(if (linux) R.string.drivers_bundle_sets_display else R.string.drivers_bundle_sets_runtime), true, DriverRow.BUNDLE)
|
||||
}
|
||||
linuxRows = LinuxVulkanDriver.optionValues(activity).map { id ->
|
||||
if (id.isEmpty()) DriverRow("", "Runtime default", "the Turnip built into the runtime", false)
|
||||
if (id.isEmpty()) DriverRow("", activity.getString(R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false)
|
||||
else bundleRow(id, linux = true) ?: DriverRow(
|
||||
id, lm.getDriverName(id),
|
||||
listOfNotNull(
|
||||
@@ -92,8 +93,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val auto = td.autoId()
|
||||
androidRows = buildList {
|
||||
add(DriverRow(
|
||||
TurnipDriver.AUTO, "Auto - picked by GPU",
|
||||
if (auto == "system") "system Vulkan: no bundled build for this GPU" else "${td.displayName(auto)} (bundled)",
|
||||
TurnipDriver.AUTO, activity.getString(R.string.gpu_auto_by_gpu),
|
||||
if (auto == "system") activity.getString(R.string.drivers_system_vulkan) else activity.getString(R.string.drivers_bundled, td.displayName(auto)),
|
||||
false,
|
||||
))
|
||||
for (id in td.visibleBundled()) add(DriverRow(id, td.displayName(id), td.driverVersion(id), true, DriverRow.BUNDLED))
|
||||
@@ -109,7 +110,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
/** 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" } ?: "")
|
||||
val label = activity.getString(if (mode == SessionPrefs.GPU_DRIVERS_AUTO) R.string.common_auto else R.string.drivers_manual)
|
||||
return active?.let { activity.getString(R.string.drivers_summary, label, it) } ?: label
|
||||
}
|
||||
|
||||
/** The pairs the last check found, with what is installed and in use; this GPU's first. */
|
||||
@@ -125,7 +127,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
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),
|
||||
detail = if (!p.complete) activity.getString(R.string.drivers_half_published) else activity.getString(R.string.drivers_pair_size, mb),
|
||||
recommended = p.key == recommended, suits = p.suits(gpu), complete = p.complete,
|
||||
installed = displayId != null && linuxId != null,
|
||||
active = displayId != null && linuxId != null &&
|
||||
@@ -153,11 +155,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
if (!force && autoCheckedThisProcess) return
|
||||
autoCheckedThisProcess = true
|
||||
if (DriverPairs.recommendedKey(gpu, emptyList()) == null) {
|
||||
autoStatus = "No drivers to set: ${gpu.supportText.lowercase()}"
|
||||
autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText(activity).lowercase())
|
||||
return
|
||||
}
|
||||
releaseChecking = true
|
||||
autoStatus = "Checking for the latest drivers…"
|
||||
autoStatus = activity.getString(R.string.drivers_checking_latest)
|
||||
Thread({
|
||||
val cached = TurnipReleases.cached(activity)
|
||||
val stale = cached == null || System.currentTimeMillis() - cached.checkedAt > 24 * 3_600_000L ||
|
||||
@@ -172,9 +174,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
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)" } ?: "")
|
||||
row?.active == true -> autoStatus = problem?.let { activity.getString(R.string.drivers_up_to_date_check_failed, it) }
|
||||
?: activity.getString(R.string.upd_current_status)
|
||||
pair == null || !pair.complete ->
|
||||
autoStatus = problem?.let { "Couldn't check: $it" } ?: "The recommended drivers aren't published right now"
|
||||
autoStatus = problem?.let { activity.getString(R.string.upd_couldnt_check, it) } ?: activity.getString(R.string.drivers_recommended_unpublished)
|
||||
else -> installPair(pair, auto = true)
|
||||
}
|
||||
}
|
||||
@@ -197,7 +200,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
if (!pair.complete) return
|
||||
pairBusy = pair.key
|
||||
pairPercent = 0
|
||||
if (auto) autoStatus = "Downloading ${pair.name} ${pair.version}…"
|
||||
if (auto) autoStatus = activity.getString(R.string.drivers_downloading_pair, pair.name, pair.version)
|
||||
Thread({
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
@@ -217,7 +220,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
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 ""
|
||||
val restart = displayChanged && CompositorHost.isStarted
|
||||
if (auto) {
|
||||
val previous = SessionPrefs.gpuAutoInstalled(activity)
|
||||
for (id in previous - setOf(displayId, linuxId)) {
|
||||
@@ -230,14 +233,18 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
TurnipReleases.forget(activity, id)
|
||||
}
|
||||
SessionPrefs.setGpuAutoInstalled(activity, setOf(displayId, linuxId))
|
||||
autoStatus = (if (downloaded) "Updated to" else "Switched to") + " ${pair.name} ${pair.version}.$restart"
|
||||
autoStatus = activity.getString(
|
||||
if (downloaded) { if (restart) R.string.drivers_updated_to_restart else R.string.drivers_updated_to }
|
||||
else { if (restart) R.string.drivers_switched_to_restart else R.string.drivers_switched_to },
|
||||
pair.name, pair.version,
|
||||
)
|
||||
} else {
|
||||
android.widget.Toast.makeText(activity, "Using ${pair.name} ${pair.version}.$restart", android.widget.Toast.LENGTH_LONG).show()
|
||||
android.widget.Toast.makeText(activity, activity.getString(if (restart) R.string.drivers_using_restart else R.string.drivers_using, pair.name, pair.version), 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"
|
||||
val why = if (e is IllegalArgumentException) e.message else activity.getString(R.string.comp_download_failed, e.message)
|
||||
if (auto) autoStatus = why ?: activity.getString(R.string.user_apps_download_failed)
|
||||
else android.widget.Toast.makeText(activity, why, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
refreshDrivers()
|
||||
@@ -298,7 +305,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
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")
|
||||
throw IllegalArgumentException(activity.getString(R.string.drivers_not_a_driver))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,10 +314,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
e.message
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "driver import", e)
|
||||
"Import failed: ${e.message}"
|
||||
activity.getString(R.string.drivers_import_failed, e.message)
|
||||
}
|
||||
ui.post {
|
||||
val done = bundle?.let { useBundle(it) } ?: "Imported ${name ?: "driver"}"
|
||||
val done = bundle?.let { useBundle(it) }
|
||||
?: name?.let { activity.getString(R.string.comp_imported, it) } ?: activity.getString(R.string.drivers_imported_unnamed)
|
||||
android.widget.Toast.makeText(
|
||||
activity, problem ?: done,
|
||||
if (problem != null || bundle != null) android.widget.Toast.LENGTH_LONG else android.widget.Toast.LENGTH_SHORT,
|
||||
@@ -340,8 +348,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
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 ""
|
||||
return activity.getString(
|
||||
if (displayChanged && CompositorHost.isStarted) R.string.drivers_using_bundle_restart else R.string.drivers_using_bundle,
|
||||
bundle.label,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,7 +364,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
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()
|
||||
android.widget.Toast.makeText(activity, activity.getString(R.string.drivers_deleted_bundle, bundle.label), android.widget.Toast.LENGTH_SHORT).show()
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
@@ -367,7 +377,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
if (SessionPrefs.androidDriver(activity) == id) SessionPrefs.setAndroidDriver(activity, TurnipDriver.AUTO)
|
||||
}
|
||||
TurnipReleases.forget(activity, id)
|
||||
android.widget.Toast.makeText(activity, "Deleted ${id}", android.widget.Toast.LENGTH_SHORT).show()
|
||||
android.widget.Toast.makeText(activity, activity.getString(R.string.drivers_deleted, id), android.widget.Toast.LENGTH_SHORT).show()
|
||||
refreshDrivers()
|
||||
}
|
||||
|
||||
@@ -378,26 +388,28 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
.filter { it.bundle || it.linux == linux }
|
||||
.filter { a -> installedIdOf(a) == null }
|
||||
.map { a ->
|
||||
val mb = "%.1f MB".format(a.size / 1_048_576.0)
|
||||
val mb = activity.getString(R.string.common_size_mb, a.size / 1_048_576.0)
|
||||
com.droiddeck.launcher.ui.DownloadRow(a.name, "${a.source} ${a.tag}", "${a.label} · $mb", releaseProgress[a.name])
|
||||
}
|
||||
linuxDownloads = rows(linux = true)
|
||||
androidDownloads = rows(linux = false)
|
||||
if (!releaseChecking) releaseStatus = when (check) {
|
||||
null -> "Not checked yet - tap refresh to look for new drivers"
|
||||
else -> "Latest: " + check.latest.joinToString(" · ") { "${it.first} ${it.second}" } +
|
||||
(if (check.failed.isEmpty()) "" else " · ${check.failed.joinToString()} unreachable") +
|
||||
" · checked ${ago(check.checkedAt)}"
|
||||
null -> activity.getString(R.string.drivers_not_checked)
|
||||
else -> {
|
||||
val latest = check.latest.joinToString(" · ") { "${it.first} ${it.second}" }
|
||||
if (check.failed.isEmpty()) activity.getString(R.string.drivers_release_status, latest, ago(check.checkedAt))
|
||||
else activity.getString(R.string.drivers_release_status_failed, latest, check.failed.joinToString(), ago(check.checkedAt))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ago(t: Long): String {
|
||||
val m = ((System.currentTimeMillis() - t) / 60_000).coerceAtLeast(0)
|
||||
return when {
|
||||
m < 1 -> "just now"
|
||||
m < 60 -> "$m min ago"
|
||||
m < 48 * 60 -> "${m / 60} h ago"
|
||||
else -> "${m / (24 * 60)} days ago"
|
||||
m < 1 -> activity.getString(R.string.upd_just_now)
|
||||
m < 60 -> activity.resources.getQuantityString(R.plurals.drivers_ago_minutes, m.toInt(), m)
|
||||
m < 48 * 60 -> activity.resources.getQuantityString(R.plurals.drivers_ago_hours, (m / 60).toInt(), m / 60)
|
||||
else -> activity.resources.getQuantityString(R.plurals.drivers_ago_days, (m / (24 * 60)).toInt(), m / (24 * 60))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,16 +417,16 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
fun checkLatestTurnip() {
|
||||
if (releaseChecking) return
|
||||
releaseChecking = true
|
||||
releaseStatus = "Checking Banners-Turnip, WinNative and DroidDeck…"
|
||||
releaseStatus = activity.getString(R.string.drivers_checking_sources)
|
||||
Thread({
|
||||
val problem = try { TurnipReleases.refresh(activity); null } catch (e: Exception) {
|
||||
Log.w(TAG, "latest Turnip check", e); e.message ?: "check failed"
|
||||
Log.w(TAG, "latest Turnip check", e); e.message ?: activity.getString(R.string.drivers_check_failed)
|
||||
}
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
if (problem != null) releaseStatus = "Couldn't check: $problem"
|
||||
if (problem != null) releaseStatus = activity.getString(R.string.upd_couldnt_check, problem)
|
||||
}
|
||||
}, "turnip-release-check").start()
|
||||
}
|
||||
@@ -433,13 +445,16 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
e.message
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "release driver download", e)
|
||||
"Download failed: ${e.message}"
|
||||
activity.getString(R.string.comp_download_failed, e.message)
|
||||
}
|
||||
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"
|
||||
val installed = activity.getString(
|
||||
if (asset.bundle) R.string.drivers_installed_pick_either else R.string.drivers_installed_pick,
|
||||
asset.name.removeSuffix(".zip"),
|
||||
)
|
||||
android.widget.Toast.makeText(
|
||||
activity, problem ?: "Installed ${asset.name.removeSuffix(".zip")} - $pick",
|
||||
activity, problem ?: installed,
|
||||
android.widget.Toast.LENGTH_LONG,
|
||||
).show()
|
||||
refreshDrivers()
|
||||
|
||||
@@ -7,6 +7,11 @@ import com.droiddeck.launcher.session.SessionService
|
||||
|
||||
/** Receives the system Home launch and routes it without consuming KEYCODE_HOME. */
|
||||
class HomeActivity : Activity() {
|
||||
override fun attachBaseContext(newBase: android.content.Context) {
|
||||
super.attachBaseContext(newBase)
|
||||
com.droiddeck.launcher.core.AppLanguage.applyTo(this, newBase)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
routeHome()
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -24,7 +24,7 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va
|
||||
if (protonBusyId != null || SessionState.running) return
|
||||
ProtonExtras.unqueue(activity, tool)
|
||||
protonBusyId = id
|
||||
protonStage = "Starting…"
|
||||
protonStage = activity.getString(R.string.store_starting)
|
||||
protonPercent = -1
|
||||
Thread({
|
||||
val problem = ProtonExtras.install(activity, tool) { label, value ->
|
||||
@@ -44,7 +44,7 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va
|
||||
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
|
||||
if (protonBusyId != null || SessionState.running) return
|
||||
protonBusyId = id
|
||||
protonStage = "Removing ${tool.name}…"
|
||||
protonStage = activity.getString(R.string.main_removing_named, tool.name)
|
||||
protonPercent = -1
|
||||
Thread({
|
||||
ProtonExtras.remove(activity, tool)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -46,7 +46,7 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
|
||||
onAllowInstalls = {
|
||||
askPermission = false
|
||||
runCatching { activity.startActivity(SelfInstaller.permissionIntent(activity)) }
|
||||
.onFailure { error = "Couldn't open Android's \"Install unknown apps\" setting"; pending = null }
|
||||
.onFailure { error = activity.getString(R.string.upd_unknown_apps_failed); pending = null }
|
||||
},
|
||||
onDismissPermission = { askPermission = false; pending = null },
|
||||
)
|
||||
@@ -77,7 +77,7 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
|
||||
catalog = it
|
||||
follow = AppUpdates.follow(activity, it)
|
||||
error = null
|
||||
}.onFailure { if (force) error = "Couldn't check: ${it.message ?: it.javaClass.simpleName}" }
|
||||
}.onFailure { if (force) error = activity.getString(R.string.upd_couldnt_check, it.message ?: it.javaClass.simpleName) }
|
||||
}
|
||||
}, "app-updates").start()
|
||||
}
|
||||
@@ -90,20 +90,20 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
|
||||
|
||||
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 }
|
||||
val apk = r.apk ?: run { error = activity.getString(R.string.upd_no_download); return }
|
||||
AppUpdates.installBlock(r)?.let { error = it.message(activity); return }
|
||||
if (SessionState.running) { error = activity.getString(R.string.upd_stop_first); return }
|
||||
if (!SelfInstaller.canInstall(activity)) { pending = r; askPermission = true; return }
|
||||
error = null
|
||||
stage = "Downloading"
|
||||
stage = activity.getString(R.string.upd_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 }
|
||||
ui.post { stage = activity.getString(R.string.upd_stage_installing); percent = -1 }
|
||||
SelfInstaller.install(activity, file)
|
||||
}
|
||||
result.onFailure { e -> ui.post { stage = null; error = e.message ?: "The update failed" } }
|
||||
result.onFailure { e -> ui.post { stage = null; error = e.message ?: activity.getString(R.string.upd_failed) } }
|
||||
}, "app-update-install").start()
|
||||
}
|
||||
|
||||
@@ -114,14 +114,14 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
|
||||
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"
|
||||
if (confirm == null) { stage = null; error = activity.getString(R.string.upd_not_offered); return }
|
||||
stage = activity.getString(R.string.upd_stage_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))
|
||||
error = SelfInstaller.describeFailure(activity, status, intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -15,6 +15,7 @@ import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntimeInstaller
|
||||
import com.droiddeck.launcher.session.SessionArtifacts
|
||||
import com.droiddeck.launcher.session.SessionEvents
|
||||
import com.droiddeck.launcher.session.SessionLogShare
|
||||
import com.droiddeck.launcher.session.SessionPhase
|
||||
import com.droiddeck.launcher.session.SessionPaths
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
@@ -50,6 +51,7 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
val response = try {
|
||||
when (method) {
|
||||
"state" -> state(context).put("ok", true)
|
||||
"logs" -> exportLogs(context)
|
||||
"start" -> error("USE_DROIDDECKCTL", "Start sessions with tools/droiddeckctl so Android launches a visible Activity")
|
||||
"stop" -> stop(context)
|
||||
"resume" -> resume(context)
|
||||
@@ -63,7 +65,7 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
|
||||
private fun state(context: Context): JSONObject {
|
||||
val runtimeVersion = LinuxRuntimeInstaller.installedVersion(context)
|
||||
val dir = SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: latestSessionDirectory()
|
||||
val dir = SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: SessionPaths.sessionFolders(context).lastOrNull()
|
||||
val session = JSONObject()
|
||||
.put("id", SessionState.sessionId ?: dir?.name ?: JSONObject.NULL)
|
||||
.put("phase", SessionState.phase.name)
|
||||
@@ -73,6 +75,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())
|
||||
@@ -100,10 +103,12 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
.put("session", session)
|
||||
}
|
||||
|
||||
private fun latestSessionDirectory(): File? {
|
||||
val parent = LinuxRuntime.debugLogDir()
|
||||
return parent.listFiles { file -> file.isDirectory && file.name.startsWith("session-") }
|
||||
?.maxWithOrNull(compareBy<File> { it.lastModified() }.thenBy { it.name })
|
||||
private fun exportLogs(context: Context): JSONObject {
|
||||
val dir = SessionState.logDirectory ?: SessionLogShare.latest(context)
|
||||
?: return error("NO_SESSION_LOGS", "No session logs are available")
|
||||
val zip = SessionLogShare.zipFolder(context, dir)
|
||||
?: return error("NO_SESSION_LOGS", "No diagnostic logs are available")
|
||||
return JSONObject().put("ok", true).put("sessionId", dir.name).put("path", zip.absolutePath)
|
||||
}
|
||||
|
||||
private fun stop(context: Context): JSONObject {
|
||||
File renamed without changes.
@@ -143,8 +143,7 @@ public class PulseAudioComponent extends SessionPart {
|
||||
config.add("load-module module-directaudio-sink sink_name=DirectAudio socket=\"" + relaySocketPath + "\" performance_mode=1 adaptive=1 volume=1.0");
|
||||
config.add("set-default-sink DirectAudio");
|
||||
} else {
|
||||
// The classic sink: the adaptive AAudio module 0.1.5 shipped, unchanged.
|
||||
config.add("load-module module-aaudio-classic-sink sink_name=AAudioSink performance_mode=1 adaptive=1 volume=1.0");
|
||||
config.add("load-module module-aaudio-sink sink_name=AAudioSink performance_mode=1 adaptive=1 volume=1.0");
|
||||
config.add("set-default-sink AAudioSink");
|
||||
}
|
||||
if (micFifoPath != null && !micFifoPath.isEmpty()) {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.LocaleList
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* The language the app shows itself in.
|
||||
*
|
||||
* Nothing chosen (the default) means the system's: Android's own language, or the per-app language
|
||||
* Android 13+ lets a user set for DroidDeck in its settings, exactly as Android resolves it. Picking
|
||||
* one in Setup overrides that until the user picks "System default" again. Each activity and service
|
||||
* applies the choice to its own context ([wrap], [applyTo]), so it works on every Android version
|
||||
* the app runs on rather than only where the platform's per-app language API exists.
|
||||
*
|
||||
* Steam follows the same choice: [SteamLanguage] turns [effective] into the client's language.
|
||||
*/
|
||||
object AppLanguage {
|
||||
/** The stored value meaning "follow the system". */
|
||||
const val SYSTEM = ""
|
||||
|
||||
/** The languages the app ships, as BCP 47 tags, in the order the picker lists them. */
|
||||
val supported: List<String> = listOf("en", "es", "fr", "ru", "ja", "ko", "zh-CN", "zh-TW", "zh-HK")
|
||||
|
||||
private const val PREFS = "language"
|
||||
private const val KEY = "app_language"
|
||||
|
||||
/** The locale Android gave the process before any choice here was applied. */
|
||||
@Volatile private var systemLocale: Locale? = null
|
||||
|
||||
/** The stored choice: one of [supported], or [SYSTEM]. */
|
||||
fun chosen(context: Context): String =
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, SYSTEM)
|
||||
?.takeIf { it in supported } ?: SYSTEM
|
||||
|
||||
fun setChosen(context: Context, tag: String) {
|
||||
val value = if (tag in supported) tag else SYSTEM
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY, value).commit()
|
||||
val locale = value.takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag) ?: system(context)
|
||||
Locale.setDefault(locale)
|
||||
// Code that reads strings through the application context sees the new language too.
|
||||
val app = context.applicationContext
|
||||
@Suppress("DEPRECATION")
|
||||
app.resources.updateConfiguration(
|
||||
Configuration(app.resources.configuration).apply { setLocales(LocaleList(locale)) },
|
||||
app.resources.displayMetrics,
|
||||
)
|
||||
}
|
||||
|
||||
/** The locale Android chose for the app, ignoring the choice made here. */
|
||||
fun system(context: Context): Locale =
|
||||
systemLocale ?: context.resources.configuration.locales[0] ?: Locale.getDefault()
|
||||
|
||||
/** The locale the app is shown in: the chosen language, or else the system's. */
|
||||
fun effective(context: Context): Locale =
|
||||
chosen(context).takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag) ?: system(context)
|
||||
|
||||
/** Remembers the system's locale; call with the untouched base context, before [wrap]. */
|
||||
fun noteSystem(base: Context) {
|
||||
if (systemLocale == null) systemLocale = base.resources.configuration.locales[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* [base], showing the chosen language: for an Application's or a Service's attachBaseContext.
|
||||
* Only the locale is overridden, so screen size, density and night mode keep following the
|
||||
* device as they change.
|
||||
*/
|
||||
fun wrap(base: Context): Context {
|
||||
noteSystem(base)
|
||||
val tag = chosen(base).ifEmpty { return base }
|
||||
val locale = Locale.forLanguageTag(tag)
|
||||
Locale.setDefault(locale)
|
||||
return base.createConfigurationContext(override(locale))
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the chosen language to an activity; call from its attachBaseContext, after super.
|
||||
* applyOverrideConfiguration keeps the locale through rotation and window resizes, which an
|
||||
* activity here handles itself rather than being recreated.
|
||||
*/
|
||||
fun applyTo(activity: Activity, base: Context) {
|
||||
noteSystem(base)
|
||||
val tag = chosen(base).ifEmpty { return }
|
||||
val locale = Locale.forLanguageTag(tag)
|
||||
Locale.setDefault(locale)
|
||||
activity.applyOverrideConfiguration(override(locale))
|
||||
}
|
||||
|
||||
/** Follows a change of the system's language while the process lives (Application.onConfigurationChanged). */
|
||||
fun systemChanged(config: Configuration) {
|
||||
config.locales[0]?.let { systemLocale = it }
|
||||
}
|
||||
|
||||
// A new Configuration's fontScale is 1, which as an override would undo the user's font size;
|
||||
// 0 leaves it undefined, so only the locale is changed.
|
||||
private fun override(locale: Locale) = Configuration().apply { fontScale = 0f; setLocales(LocaleList(locale)) }
|
||||
|
||||
/** Which of the app's languages [locale] reads as, or null when the app does not ship it. */
|
||||
fun supportedTag(locale: Locale): String? = when (locale.language) {
|
||||
"en", "es", "fr", "ru", "ja", "ko" -> locale.language
|
||||
"zh" -> when {
|
||||
locale.script == "Hans" -> "zh-CN"
|
||||
locale.country == "HK" || locale.country == "MO" -> "zh-HK"
|
||||
locale.script == "Hant" || locale.country == "TW" -> "zh-TW"
|
||||
else -> "zh-CN"
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
/** [locale]'s language named in itself, without its region unless the app tells them apart. */
|
||||
fun displayName(locale: Locale): String =
|
||||
supportedTag(locale)?.let(::nativeName) ?: locale.getDisplayLanguage(locale).replaceFirstChar { it.titlecase(locale) }
|
||||
|
||||
/** A language's name in that language, for the picker: what a reader of it looks for. */
|
||||
fun nativeName(tag: String): String = when (tag) {
|
||||
"en" -> "English"
|
||||
"es" -> "Español"
|
||||
"fr" -> "Français"
|
||||
"ru" -> "Русский"
|
||||
"ja" -> "日本語"
|
||||
"ko" -> "한국어"
|
||||
"zh-CN" -> "简体中文"
|
||||
"zh-TW" -> "繁體中文(台灣)"
|
||||
"zh-HK" -> "繁體中文(香港)"
|
||||
else -> Locale.forLanguageTag(tag).let { it.getDisplayName(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object AppUiPrefs {
|
||||
const val DEFAULT_SCALE = 100
|
||||
val scales = listOf(75, 85, 100, 115, 125, 150)
|
||||
|
||||
internal fun normalizeScale(percent: Int): Int = percent.takeIf { it in scales } ?: DEFAULT_SCALE
|
||||
|
||||
fun scale(context: Context): Int = normalizeScale(
|
||||
context.getSharedPreferences("app-ui", Context.MODE_PRIVATE).getInt("scale", DEFAULT_SCALE),
|
||||
)
|
||||
|
||||
fun setScale(context: Context, percent: Int) {
|
||||
context.getSharedPreferences("app-ui", Context.MODE_PRIVATE).edit()
|
||||
.putInt("scale", normalizeScale(percent)).apply()
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import com.droiddeck.launcher.runtime.DesktopCatalog
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntimeInstaller
|
||||
import com.droiddeck.launcher.session.EsyncPacks
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
@@ -107,9 +108,12 @@ object DeviceReport {
|
||||
h("Display")
|
||||
k("Session output", SessionState.outputSize?.let { "${it.first}x${it.second}" })
|
||||
k("Session refresh", String.format(Locale.US, "%.2f Hz", SessionState.refreshHz))
|
||||
k("Shape setting", SessionPrefs.shapeMode(context))
|
||||
k("Custom resolution (Steam)", SessionPrefs.customResolution(context, SessionService.MODE_STEAM)?.let { "${it.first}x${it.second}" } ?: "off")
|
||||
k("Custom resolution (Desktop)", SessionPrefs.customResolution(context, SessionService.MODE_DESKTOP)?.let { "${it.first}x${it.second}" } ?: "off")
|
||||
val panelSize = com.droiddeck.launcher.session.SessionDisplay.panelSize(context)
|
||||
for ((label, mode) in listOf("Resolution (Steam)" to SessionService.MODE_STEAM, "Resolution (Desktop)" to SessionService.MODE_DESKTOP)) {
|
||||
val choice = SessionPrefs.resolutionChoice(context, mode, panelSize)
|
||||
val size = com.droiddeck.launcher.session.SessionDisplay.resolveChoice(panelSize, choice)
|
||||
k(label, "$choice (${size.first}x${size.second})")
|
||||
}
|
||||
k("Foldable", context.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle"))
|
||||
|
||||
h("Drivers")
|
||||
@@ -150,13 +154,18 @@ 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))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.droiddeck.launcher.R
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -17,8 +19,8 @@ object DeviceSupport {
|
||||
File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists()
|
||||
|
||||
/** The chip as the device names it, for the card that explains the refusal. */
|
||||
fun gpuName(): String {
|
||||
fun gpuName(context: Context): String {
|
||||
val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it.isNotBlank() && it != Build.UNKNOWN } else null
|
||||
return soc?.let { "$it (${Build.HARDWARE})" } ?: Build.HARDWARE.ifBlank { "this GPU" }
|
||||
return soc?.let { "$it (${Build.HARDWARE})" } ?: Build.HARDWARE.ifBlank { context.getString(R.string.gpu_this_gpu) }
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -25,14 +25,24 @@ object GameEnvironment {
|
||||
fun validValue(value: String) = '\u0000' !in value && value.length <= 8192
|
||||
fun validScope(scope: String) = scope.isEmpty() || (Regex("[1-9][0-9]*").matches(scope) && scope.toLongOrNull()?.let { it in 1..4294967295L } == true)
|
||||
|
||||
fun defaults(preset: String): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
fun defaults(preset: String, forceSsbs: Boolean = false): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
"MESA_SHADER_CACHE_DISABLE" to "false",
|
||||
"VKD3D_FEATURE_LEVEL" to "12_2",
|
||||
"VKD3D_SHADER_MODEL" to "6_9",
|
||||
// .NET 7 and later reserve 256 GB of address space for the GC heap at startup, more than an
|
||||
// Android process has free, and the runtime fails to start. 32 GB is plenty for a game.
|
||||
"DOTNET_GCRegionRange" to "0x800000000",
|
||||
// .NET's W^X writes JIT code through a second mapping that FEX does not watch, so FEX keeps
|
||||
// running the stale translation and the game hangs at startup.
|
||||
"DOTNET_EnableWriteXorExecute" to "0",
|
||||
).apply {
|
||||
FexPreset.env(preset).forEach { put(it.substringBefore('='), it.substringAfter('=')) }
|
||||
// Steam settings' Force SSBS: droiddeck-game-env preloads libssbs.so into the game's Wine.
|
||||
if (forceSsbs) put(FORCE_SSBS, "1")
|
||||
}
|
||||
|
||||
fun effective(config: Config, preset: String, scope: String): Map<String, String?> =
|
||||
defaults(preset) + config.shared + if (scope.isEmpty()) emptyMap() else config.games[scope].orEmpty()
|
||||
const val FORCE_SSBS = "DROIDDECK_FORCE_SSBS"
|
||||
|
||||
fun effective(config: Config, preset: String, scope: String, forceSsbs: Boolean = false): Map<String, String?> =
|
||||
defaults(preset, forceSsbs) + config.shared + if (scope.isEmpty()) emptyMap() else config.games[scope].orEmpty()
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
@@ -36,11 +36,16 @@ object LogRedactor {
|
||||
* WebAPI key is caught by [WEBAPI_KEY]. The value class excludes `<` and `>` so running this
|
||||
* twice over an already-scrubbed line changes nothing.
|
||||
*/
|
||||
private val SECRET_KV = Regex(
|
||||
"(?i)\\b(access[_-]?token|refresh[_-]?token|auth[_-]?token|authtoken|token|authcode|" +
|
||||
private const val SECRET_KEYS =
|
||||
"access[_-]?token|refresh[_-]?token|auth[_-]?token|authtoken|token|authcode|" +
|
||||
"auth[_-]?ticket|ticket|sessionid|steamloginsecure|webapikey|api[_-]?key|" +
|
||||
"machine[_-]?auth(?:[_-]?token)?|machineauth|password|passwd|pwd|secret)" +
|
||||
"(\\s*[=:]\\s*|=)([^\\s\"'<>&;,]{4,})"
|
||||
"machine[_-]?auth(?:[_-]?token)?|machineauth|password|passwd|pwd|secret"
|
||||
private val SECRET_KV = Regex(
|
||||
"(?i)\\b($SECRET_KEYS)" +
|
||||
"([\"']?\\s*[=:]\\s*[\"']?)([^\\s\"'<>&;,]{1,})"
|
||||
)
|
||||
private val QUOTED_SECRET = Regex(
|
||||
"(?i)\\b($SECRET_KEYS)([\"']?\\s*[=:]\\s*)(\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*')"
|
||||
)
|
||||
/** A Steam Guard code, only where the text around it says that is what it is. */
|
||||
private val GUARD_CODE = Regex(
|
||||
@@ -53,6 +58,7 @@ object LogRedactor {
|
||||
"(?i)\\b(jwt|token|ticket|sessionid|steamloginsecure|machineauth)\\b[\\s=:]*([A-Za-z0-9+/=_\\-]{12,})"
|
||||
)
|
||||
private val LONG_TOKEN = Regex("[A-Za-z0-9_\\-]{88,}")
|
||||
private val AUTHORIZATION = Regex("(?i)(\\b(?:proxy-)?authorization[\"']?\\s*[:=]\\s*[\"']?)[^\\r\\n\"']+")
|
||||
private val STEAMID64 = Regex("\\b(76561)(\\d{8})(\\d{4})\\b")
|
||||
private val STEAMID3 = Regex("\\[U:1:(\\d+)]")
|
||||
/** "external address 2607:..." / "external IP: 203.0.113.9" - the client stating ours. */
|
||||
@@ -95,7 +101,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"))
|
||||
}
|
||||
|
||||
@@ -179,22 +185,6 @@ object LogRedactor {
|
||||
return out
|
||||
}
|
||||
|
||||
/** A file worth scrubbing: text (no NUL in its first 4 KB) and not huge. */
|
||||
fun isText(file: java.io.File): Boolean = try {
|
||||
file.isFile && file.length() < 64L * 1024 * 1024 && file.inputStream().use { input ->
|
||||
val buf = ByteArray(4096)
|
||||
val n = input.read(buf)
|
||||
n <= 0 || (0 until n).none { buf[it].toInt() == 0 }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
/** [src]'s lines, scrubbed, to [out]. */
|
||||
fun scrubTo(src: java.io.File, out: java.io.Writer) {
|
||||
src.forEachLine { line -> out.write(redact(line)); out.write("\n") }
|
||||
}
|
||||
|
||||
/** [line] with every credential shape replaced. Null- and exception-safe by construction. */
|
||||
fun redact(line: String): String {
|
||||
if (line.isEmpty()) return line
|
||||
@@ -203,6 +193,11 @@ object LogRedactor {
|
||||
out = GUID.replace(out, "<redacted:guid>")
|
||||
out = JWT_LABELLED.replace(out) { "${it.groupValues[1]}<redacted:jwt>" }
|
||||
out = JWT_BASE64.replace(out, "<redacted:jwt>")
|
||||
out = AUTHORIZATION.replace(out) { "${it.groupValues[1]}<redacted:authorization>" }
|
||||
out = QUOTED_SECRET.replace(out) {
|
||||
val quote = it.groupValues[3].first()
|
||||
"${it.groupValues[1]}${it.groupValues[2]}$quote<redacted:token>$quote"
|
||||
}
|
||||
out = SECRET_KV.replace(out) { "${it.groupValues[1]}${it.groupValues[2]}<redacted:token>" }
|
||||
out = GUARD_CODE.replace(out) { "${it.groupValues[1]}<redacted:code>" }
|
||||
out = WEBAPI_KEY.replace(out, "<redacted:key>")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.droiddeck.launcher.core
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import com.droiddeck.launcher.R
|
||||
|
||||
/** Android's phantom process monitor can kill the many child processes used by a Steam session. */
|
||||
enum class PhantomProcessStatus {
|
||||
@@ -98,29 +99,30 @@ object PhantomProcessLimit {
|
||||
fun blocksSteam(status: PhantomProcessStatus): Boolean =
|
||||
status != PhantomProcessStatus.NOT_APPLICABLE && status != PhantomProcessStatus.DISABLED
|
||||
|
||||
fun title(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.ENABLED -> "Child-process limit is on"
|
||||
PhantomProcessStatus.UNSET -> "Child-process limit is unset"
|
||||
PhantomProcessStatus.UNREADABLE -> "Child-process limit could not be checked"
|
||||
PhantomProcessStatus.DISABLED -> "Child-process limit is off"
|
||||
PhantomProcessStatus.NOT_APPLICABLE -> "Child-process limit not required"
|
||||
fun title(context: Context, status: PhantomProcessStatus): String = context.getString(when (status) {
|
||||
PhantomProcessStatus.ENABLED -> R.string.phantom_title_on
|
||||
PhantomProcessStatus.UNSET -> R.string.phantom_title_unset
|
||||
PhantomProcessStatus.UNREADABLE -> R.string.phantom_title_unreadable
|
||||
PhantomProcessStatus.DISABLED -> R.string.phantom_title_off
|
||||
PhantomProcessStatus.NOT_APPLICABLE -> R.string.phantom_title_not_required
|
||||
})
|
||||
|
||||
fun instructions(context: Context, status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.ENABLED, PhantomProcessStatus.UNSET, PhantomProcessStatus.UNREADABLE ->
|
||||
context.getString(R.string.phantom_instructions_fix, fixSentence(context))
|
||||
PhantomProcessStatus.DISABLED -> context.getString(R.string.phantom_instructions_off)
|
||||
PhantomProcessStatus.NOT_APPLICABLE -> context.getString(R.string.phantom_instructions_not_applicable)
|
||||
}
|
||||
|
||||
fun instructions(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.ENABLED, PhantomProcessStatus.UNSET, PhantomProcessStatus.UNREADABLE -> fixSentence() + " If that is not possible, connect this device to a computer with ADB and run the command below. Return here and check again."
|
||||
PhantomProcessStatus.DISABLED -> "Steam sessions can start."
|
||||
PhantomProcessStatus.NOT_APPLICABLE -> "This Android version does not use this limit."
|
||||
fun gateInstructions(context: Context, status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.ENABLED, PhantomProcessStatus.UNSET, PhantomProcessStatus.UNREADABLE -> fixSentence(context)
|
||||
else -> instructions(context, status)
|
||||
}
|
||||
|
||||
fun gateInstructions(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.ENABLED, PhantomProcessStatus.UNSET, PhantomProcessStatus.UNREADABLE -> fixSentence()
|
||||
else -> instructions(status)
|
||||
}
|
||||
|
||||
fun fixSentence(sdk: Int = Build.VERSION.SDK_INT): String =
|
||||
if (hasDeveloperToggle(sdk)) "Turn on “Disable child process restrictions” in Developer options, and keep Developer options on afterwards."
|
||||
else "This Android version has no switch for it. Use Wireless debugging to change it from this device."
|
||||
fun fixSentence(context: Context, sdk: Int = Build.VERSION.SDK_INT): String =
|
||||
context.getString(if (hasDeveloperToggle(sdk)) R.string.phantom_fix_toggle else R.string.phantom_fix_no_toggle)
|
||||
|
||||
/** English: for the device report. */
|
||||
fun reportValue(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.DISABLED -> "disabled (good - the OS will not kill the session's children)"
|
||||
PhantomProcessStatus.ENABLED -> "ENABLED (the OS may kill the session with no log; turn off Restrict child processes)"
|
||||
|
||||
@@ -49,7 +49,7 @@ object SessionLogCapture {
|
||||
) { line ->
|
||||
synchronized(this) {
|
||||
try {
|
||||
writer?.apply { write(line); newLine(); flush() }
|
||||
if (writer === out) out.apply { write(line); newLine(); flush() }
|
||||
} catch (e: Exception) {
|
||||
// A full or unmounted card must not take the session with it.
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.droiddeck.launcher.core
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import com.droiddeck.launcher.R
|
||||
import com.flyfishxu.kadb.Kadb
|
||||
import com.flyfishxu.kadb.cert.KadbCert
|
||||
import java.io.File
|
||||
@@ -37,21 +38,21 @@ object WirelessAdbFix {
|
||||
|
||||
val identity = KadbCert.get(notAfter = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(3650))
|
||||
KadbCert.set(identity.first, identity.second)
|
||||
writeAtomically(cert, identity.first)
|
||||
writeAtomically(key, identity.second)
|
||||
writeAtomically(context, cert, identity.first)
|
||||
writeAtomically(context, key, identity.second)
|
||||
}
|
||||
|
||||
private fun writeAtomically(destination: File, contents: ByteArray) {
|
||||
private fun writeAtomically(context: Context, destination: File, contents: ByteArray) {
|
||||
val temporary = File(destination.parentFile, "${destination.name}.tmp")
|
||||
temporary.writeBytes(contents)
|
||||
check(temporary.renameTo(destination)) { "Could not save Wireless debugging identity" }
|
||||
check(temporary.renameTo(destination)) { context.getString(R.string.adbfix_save_failed) }
|
||||
}
|
||||
|
||||
suspend fun pair(context: Context, host: String, port: Int, pairingCode: String) {
|
||||
loadOrCreateIdentity(context.applicationContext)
|
||||
Kadb.pair(host, port, pairingCode, "DroidDeck")
|
||||
writeAtomically(File(context.noBackupFilesDir, HOST_FILE), host.toByteArray())
|
||||
writeAtomically(File(context.noBackupFilesDir, CONNECTION_FILE), ByteArray(0))
|
||||
writeAtomically(context, File(context.noBackupFilesDir, HOST_FILE), host.toByteArray())
|
||||
writeAtomically(context, File(context.noBackupFilesDir, CONNECTION_FILE), ByteArray(0))
|
||||
}
|
||||
|
||||
private fun savedConnection(context: Context): SavedConnection? {
|
||||
@@ -73,6 +74,7 @@ object WirelessAdbFix {
|
||||
|
||||
private fun saveConnection(context: Context, host: String, port: Int) {
|
||||
writeAtomically(
|
||||
context,
|
||||
File(context.applicationContext.noBackupFilesDir, CONNECTION_FILE),
|
||||
"$host\n$port".toByteArray(),
|
||||
)
|
||||
@@ -171,11 +173,11 @@ object WirelessAdbFix {
|
||||
Kadb.create(host, port).use { adb ->
|
||||
PhantomProcessLimit.shellCommands(enabled).forEach { command ->
|
||||
val change = adb.shell(command)
|
||||
check(change.exitCode == 0) { change.allOutput.ifBlank { "ADB command failed (${change.exitCode})" } }
|
||||
check(change.exitCode == 0) { change.allOutput.ifBlank { context.getString(R.string.adbfix_command_failed, change.exitCode) } }
|
||||
}
|
||||
val result = adb.shell(PhantomProcessLimit.verifyCommand())
|
||||
check(result.exitCode == 0 && PhantomProcessLimit.verified(result.output, enabled)) {
|
||||
"Android did not confirm the child-process limit was changed: ${result.allOutput.trim()}"
|
||||
context.getString(R.string.adbfix_not_confirmed, result.allOutput.trim())
|
||||
}
|
||||
}
|
||||
if (PhantomProcessLimit.usesDeviceConfig()) PhantomProcessLimit.rememberAndroid12(context, !enabled)
|
||||
@@ -186,7 +188,7 @@ object WirelessAdbFix {
|
||||
fun setUsingSavedPairing(context: Context, enabled: Boolean) {
|
||||
val appContext = context.applicationContext
|
||||
val saved = savedConnection(appContext)
|
||||
?: error("No Wireless debugging pairing is saved")
|
||||
?: error(appContext.getString(R.string.adbfix_no_pairing))
|
||||
val cachedPort = saved.port
|
||||
if (cachedPort != null) {
|
||||
val cachedResult = runCatching {
|
||||
@@ -201,7 +203,7 @@ object WirelessAdbFix {
|
||||
}
|
||||
} else {
|
||||
val discoveredPort = findConnectPort(appContext, saved.host)
|
||||
?: error("Enter the current Wireless debugging IP address & Port")
|
||||
?: error(appContext.getString(R.string.adbfix_enter_address))
|
||||
setChildProcessLimit(appContext, saved.host, discoveredPort, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class WirelessAdbPairingService : Service() {
|
||||
override fun attachBaseContext(newBase: android.content.Context) =
|
||||
super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(newBase))
|
||||
|
||||
sealed interface Stage {
|
||||
data object Idle : Stage
|
||||
data object Waiting : Stage
|
||||
@@ -35,7 +38,7 @@ class WirelessAdbPairingService : Service() {
|
||||
private var pairingHost: String? = null
|
||||
private var pairingPort: Int? = null
|
||||
@Volatile private var working = false
|
||||
private val timeout = Runnable { finish(Stage.Failed("Timed out waiting for the pairing pop-up. Try again from DroidDeck.")) }
|
||||
private val timeout = Runnable { finish(Stage.Failed(getString(R.string.adbpair_timed_out))) }
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
@@ -75,13 +78,13 @@ class WirelessAdbPairingService : Service() {
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo) = Unit
|
||||
override fun onDiscoveryStopped(serviceType: String) = Unit
|
||||
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||
main.post { finish(Stage.Failed("Could not look for the pairing pop-up ($errorCode). Check that Wi-Fi is on.")) }
|
||||
main.post { finish(Stage.Failed(getString(R.string.adbpair_discovery_failed_code, errorCode))) }
|
||||
}
|
||||
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) = Unit
|
||||
}
|
||||
runCatching { manager.discoverServices("$PAIRING_TYPE.", NsdManager.PROTOCOL_DNS_SD, listener) }
|
||||
.onSuccess { nsd = manager; discovery = listener }
|
||||
.onFailure { finish(Stage.Failed("Could not look for the pairing pop-up: ${it.localizedMessage}")) }
|
||||
.onFailure { finish(Stage.Failed(getString(R.string.adbpair_discovery_failed, it.localizedMessage.toString()))) }
|
||||
}
|
||||
|
||||
private fun resolveListener() = object : NsdManager.ResolveListener {
|
||||
@@ -107,34 +110,34 @@ class WirelessAdbPairingService : Service() {
|
||||
return
|
||||
}
|
||||
if (code.length != 6) {
|
||||
show(Stage.CodeNeeded("Enter all 6 digits of the code"))
|
||||
show(Stage.CodeNeeded(getString(R.string.adbpair_six_digits)))
|
||||
return
|
||||
}
|
||||
working = true
|
||||
show(Stage.Working("Pairing…"))
|
||||
show(Stage.Working(getString(R.string.adbpair_pairing)))
|
||||
Thread({
|
||||
val paired = runCatching { kotlinx.coroutines.runBlocking { WirelessAdbFix.pair(this@WirelessAdbPairingService, WirelessAdbFix.LOOPBACK, port, code) } }
|
||||
if (paired.isFailure) {
|
||||
main.post {
|
||||
working = false
|
||||
pairingPort = null
|
||||
show(Stage.CodeNeeded("That code didn't work. Open the pairing pop-up again and enter its new code."))
|
||||
show(Stage.CodeNeeded(getString(R.string.adbpair_code_rejected)))
|
||||
}
|
||||
return@Thread
|
||||
}
|
||||
main.post {
|
||||
stopDiscovery()
|
||||
show(Stage.Working("Paired. Connecting…"))
|
||||
show(Stage.Working(getString(R.string.adbpair_connecting)))
|
||||
}
|
||||
val result = runCatching {
|
||||
val connectPort = WirelessAdbFix.localConnectPort(this)
|
||||
?: error("Paired, but the Wireless debugging port was not found. Keep Wireless debugging on and try again.")
|
||||
main.post { show(Stage.Working("Applying the setting…")) }
|
||||
?: error(getString(R.string.adbpair_port_not_found))
|
||||
main.post { show(Stage.Working(getString(R.string.adbpair_applying))) }
|
||||
WirelessAdbFix.setChildProcessLimit(this, WirelessAdbFix.LOOPBACK, connectPort, false)
|
||||
}
|
||||
main.post {
|
||||
working = false
|
||||
finish(result.exceptionOrNull()?.let { Stage.Failed(it.localizedMessage ?: "Wireless debugging command failed") } ?: Stage.Done)
|
||||
finish(result.exceptionOrNull()?.let { Stage.Failed(it.localizedMessage ?: getString(R.string.adbpair_command_failed)) } ?: Stage.Done)
|
||||
}
|
||||
}, "wireless-adb-pairing").start()
|
||||
}
|
||||
@@ -174,9 +177,9 @@ class WirelessAdbPairingService : Service() {
|
||||
|
||||
private fun notification(stage: Stage): Notification {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager?.createNotificationChannel(NotificationChannel(CHANNEL_ID, "Wireless debugging setup",
|
||||
manager?.createNotificationChannel(NotificationChannel(CHANNEL_ID, getString(R.string.adbpair_channel_name),
|
||||
NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
description = "Asks for the Wireless debugging pairing code while Settings is open"
|
||||
description = getString(R.string.adbpair_channel_description)
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
enableVibration(false)
|
||||
@@ -193,41 +196,40 @@ class WirelessAdbPairingService : Service() {
|
||||
.setCategory(Notification.CATEGORY_STATUS)
|
||||
when (stage) {
|
||||
Stage.Waiting, Stage.Idle -> builder
|
||||
.setContentTitle("Pair Wireless debugging")
|
||||
.setContentText("Turn on Wireless debugging, then tap “Pair device with pairing code”.")
|
||||
.setStyle(Notification.BigTextStyle().bigText(
|
||||
"Turn on Wireless debugging, then tap “Pair device with pairing code”. Stay in Settings: the code goes in this notification."))
|
||||
.setContentTitle(getString(R.string.adbpair_title_pair))
|
||||
.setContentText(getString(R.string.adbpair_waiting_text))
|
||||
.setStyle(Notification.BigTextStyle().bigText(getString(R.string.adbpair_waiting_long)))
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.addAction(Notification.Action.Builder(null, "Cancel", cancel).build())
|
||||
.addAction(Notification.Action.Builder(null, getString(R.string.common_cancel), cancel).build())
|
||||
is Stage.CodeNeeded -> {
|
||||
val reply = PendingIntent.getService(this, 2,
|
||||
Intent(this, WirelessAdbPairingService::class.java).setAction(ACTION_CODE),
|
||||
if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
else PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val input = RemoteInput.Builder(KEY_CODE).setLabel("6-digit pairing code").build()
|
||||
val text = stage.error ?: "Tap Enter code and type the 6 digits shown in Settings."
|
||||
val input = RemoteInput.Builder(KEY_CODE).setLabel(getString(R.string.adbpair_code_label)).build()
|
||||
val text = stage.error ?: getString(R.string.adbpair_code_hint)
|
||||
builder
|
||||
.setContentTitle(if (stage.error == null) "Enter the Wi-Fi pairing code" else "Try the code again")
|
||||
.setContentTitle(getString(if (stage.error == null) R.string.adbpair_title_code else R.string.adbpair_title_retry))
|
||||
.setContentText(text)
|
||||
.setStyle(Notification.BigTextStyle().bigText(text))
|
||||
.setOngoing(true)
|
||||
.addAction(Notification.Action.Builder(null, "Enter code", reply).addRemoteInput(input).build())
|
||||
.addAction(Notification.Action.Builder(null, "Cancel", cancel).build())
|
||||
.addAction(Notification.Action.Builder(null, getString(R.string.adbpair_enter_code), reply).addRemoteInput(input).build())
|
||||
.addAction(Notification.Action.Builder(null, getString(R.string.common_cancel), cancel).build())
|
||||
}
|
||||
is Stage.Working -> builder
|
||||
.setContentTitle("Setting up Steam")
|
||||
.setContentTitle(getString(R.string.adbpair_title_working))
|
||||
.setContentText(stage.step)
|
||||
.setProgress(0, 0, true)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
Stage.Done -> builder
|
||||
.setContentTitle("Steam is ready")
|
||||
.setContentText("Tap to go back to DroidDeck. You can turn Wireless debugging off now.")
|
||||
.setStyle(Notification.BigTextStyle().bigText("Tap to go back to DroidDeck. You can turn Wireless debugging off now."))
|
||||
.setContentTitle(getString(R.string.adbpair_title_done))
|
||||
.setContentText(getString(R.string.adbpair_done_text))
|
||||
.setStyle(Notification.BigTextStyle().bigText(getString(R.string.adbpair_done_text)))
|
||||
.setAutoCancel(true)
|
||||
is Stage.Failed -> builder
|
||||
.setContentTitle("Wireless debugging setup stopped")
|
||||
.setContentTitle(getString(R.string.adbpair_title_failed))
|
||||
.setContentText(stage.error)
|
||||
.setStyle(Notification.BigTextStyle().bigText(stage.error))
|
||||
.setAutoCancel(true)
|
||||
|
||||
@@ -56,6 +56,8 @@ data class RailItem(
|
||||
val selected: Boolean,
|
||||
/** >0 shows a small accent count badge on the item's icon (expanded and collapsed). */
|
||||
val badge: Int = 0,
|
||||
/** The under-icon label when the rail is collapsed, for names too long to fit; null derives one. */
|
||||
val shortLabel: String? = null,
|
||||
// onClick stays LAST so existing call sites can pass it as a trailing lambda; badge is an
|
||||
// optional named param before it (default 0).
|
||||
val onClick: () -> Unit,
|
||||
@@ -73,7 +75,6 @@ private fun collapsedLabel(label: String): String {
|
||||
when (label) {
|
||||
"ENVIROMENT" -> return "ENVIRON" // the app's existing (mis)spelling, kept uppercase
|
||||
"WIN COMPONENTS" -> return "WIN COMP"
|
||||
"Downloads" -> return "Downlds"
|
||||
}
|
||||
if (label.contains(' ')) return label // multi-word → wraps to 2 lines
|
||||
return if (label.length > 9) label.take(8) + "…" else label
|
||||
@@ -317,7 +318,7 @@ fun CollapsibleRail(
|
||||
RailItemIcon(item, iconTint)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
collapsedLabel(item.label),
|
||||
item.shortLabel ?: collapsedLabel(item.label),
|
||||
color = labelColor,
|
||||
fontSize = 7.5.sp,
|
||||
lineHeight = 8.5.sp,
|
||||
|
||||
@@ -15,6 +15,11 @@ import java.io.File
|
||||
|
||||
/** The File Manager as a screen of its own, from the Files tile on the main screen. */
|
||||
class FileManagerActivity : ComponentActivity() {
|
||||
override fun attachBaseContext(newBase: android.content.Context) {
|
||||
super.attachBaseContext(newBase)
|
||||
com.droiddeck.launcher.core.AppLanguage.applyTo(this, newBase)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
@@ -22,7 +27,7 @@ class FileManagerActivity : ComponentActivity() {
|
||||
}
|
||||
val initialDir = intent.getStringExtra(EXTRA_START_DIR)?.let(::File)
|
||||
setContent {
|
||||
DroidDeckTheme {
|
||||
DroidDeckTheme(appScale = com.droiddeck.launcher.core.AppUiPrefs.scale(this)) {
|
||||
Surface(modifier = Modifier.fillMaxSize().systemBarsPadding(), color = MaterialTheme.colorScheme.background) {
|
||||
FileManagerScreen(initialDir = initialDir)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.Checklist
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.FileCopy
|
||||
@@ -61,6 +62,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import coil.compose.AsyncImage
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -107,7 +109,7 @@ private fun FileContextMenuItems(
|
||||
) {
|
||||
val isDir = file.isDirectory
|
||||
DropdownMenuItem(
|
||||
text = { Text("Select") },
|
||||
text = { Text(stringResource(R.string.fm_select)) },
|
||||
leadingIcon = { Icon(Icons.Filled.Checklist, null, tint = MaterialTheme.colorScheme.primary) },
|
||||
onClick = { onDismissMenu(); onSelect() },
|
||||
)
|
||||
@@ -115,7 +117,7 @@ private fun FileContextMenuItems(
|
||||
// Properties: basic info + Read-only / Hidden toggles, for ANY file or folder (handy for config
|
||||
// files like .txt/.cfg/.ini). Kept near the top since it's a common reason to open this menu.
|
||||
DropdownMenuItem(
|
||||
text = { Text("Properties") },
|
||||
text = { Text(stringResource(R.string.fm_properties)) },
|
||||
leadingIcon = { Icon(Icons.Filled.Info, null, tint = MaterialTheme.colorScheme.primary) },
|
||||
onClick = { onDismissMenu(); onProperties() },
|
||||
)
|
||||
@@ -123,7 +125,7 @@ private fun FileContextMenuItems(
|
||||
// Favorites are directories - only folders get the pin toggle.
|
||||
if (isDir) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (isFavorite) "Remove from Favorites" else "Add to Favorites") },
|
||||
text = { Text(if (isFavorite) stringResource(R.string.fm_favorite_remove) else stringResource(R.string.fm_favorite_add)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
if (isFavorite) Icons.Filled.Star else Icons.Filled.StarBorder,
|
||||
@@ -136,25 +138,25 @@ private fun FileContextMenuItems(
|
||||
MenuItemDivider()
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text("Rename") },
|
||||
text = { Text(stringResource(R.string.fm_rename)) },
|
||||
leadingIcon = { Icon(Icons.Filled.Edit, null, tint = MaterialTheme.colorScheme.primary) },
|
||||
onClick = { onDismissMenu(); onRename() },
|
||||
)
|
||||
MenuItemDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
text = { Text(stringResource(R.string.fm_copy)) },
|
||||
leadingIcon = { Icon(Icons.Filled.FileCopy, null, tint = MaterialTheme.colorScheme.primary) },
|
||||
onClick = { onDismissMenu(); onCopy() },
|
||||
)
|
||||
MenuItemDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Cut") },
|
||||
text = { Text(stringResource(R.string.fm_cut)) },
|
||||
leadingIcon = { Icon(Icons.Filled.ContentCut, null, tint = MaterialTheme.colorScheme.primary) },
|
||||
onClick = { onDismissMenu(); onCut() },
|
||||
)
|
||||
MenuItemDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Delete") },
|
||||
text = { Text(stringResource(R.string.common_delete)) },
|
||||
leadingIcon = { Icon(Icons.Filled.Delete, null, tint = MaterialTheme.colorScheme.primary) },
|
||||
onClick = { onDismissMenu(); onDelete() },
|
||||
)
|
||||
@@ -184,6 +186,7 @@ internal fun FileItemRow(
|
||||
onProperties: () -> Unit = {},
|
||||
) {
|
||||
val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) }
|
||||
val context = LocalContext.current
|
||||
val isDir = file.isDirectory
|
||||
val isExe = !isDir && file.name.lowercase().let { it.endsWith(".exe") || it.endsWith(".bat") || it.endsWith(".msi") || it.endsWith(".sh") }
|
||||
// Image files show a real thumbnail instead of the generic file icon (handy when picking a
|
||||
@@ -276,7 +279,7 @@ internal fun FileItemRow(
|
||||
)
|
||||
Text(
|
||||
text = buildString {
|
||||
if (!isDir) append(FileOps.formatBytes(file.length())).append(" \u2022 ")
|
||||
if (!isDir) append(FileOps.formatBytes(context, file.length())).append(" \u2022 ")
|
||||
append(dateFormat.format(Date(file.lastModified())))
|
||||
},
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -285,7 +288,7 @@ internal fun FileItemRow(
|
||||
}
|
||||
if (showActions) Box {
|
||||
IconButton(onClick = onMenu) {
|
||||
Icon(Icons.Filled.MoreVert, "Actions", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp))
|
||||
Icon(Icons.Filled.MoreVert, stringResource(R.string.fm_actions), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = menuExpanded,
|
||||
@@ -310,6 +313,47 @@ internal fun FileItemRow(
|
||||
}
|
||||
}
|
||||
|
||||
// The ".." row at the top of a folder: one level up, styled like a folder row so it reads as part
|
||||
// of the list and takes controller focus the same way.
|
||||
@Composable
|
||||
internal fun ParentFolderRow(compact: Boolean = false, onTap: () -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 3.dp)
|
||||
.clickable(onClick = onTap),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = if (compact) 3.dp else 8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowUpward,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(if (compact) 24.dp else 36.dp),
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "..",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.fm_up_one_level),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dedicated Favorites list that replaces the file list while the star toggle is on.
|
||||
// Reads the store keyed on [favTick] so it recomposes after any pin/unpin.
|
||||
@Composable
|
||||
@@ -338,7 +382,7 @@ internal fun FavoritesList(
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Favorites",
|
||||
text = stringResource(R.string.fm_favorites),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
@@ -351,7 +395,7 @@ internal fun FavoritesList(
|
||||
Icon(Icons.Filled.PushPin, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "Pin current folder",
|
||||
text = stringResource(R.string.fm_pin_current),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 13.sp,
|
||||
)
|
||||
@@ -366,7 +410,7 @@ internal fun FavoritesList(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "No favorites yet - pin a folder with its ⋮ menu to jump back here fast.",
|
||||
text = stringResource(R.string.fm_no_favorites),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
@@ -431,7 +475,7 @@ private fun FavoriteCard(
|
||||
// Origin line: coloured drive badge + source description.
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = loc.driveLabel,
|
||||
text = stringResource(loc.driveLabel),
|
||||
color = badgeFg,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -443,9 +487,9 @@ private fun FavoriteCard(
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
text = when (loc.storage) {
|
||||
FavStorage.INTERNAL -> "Internal storage"
|
||||
FavStorage.SD -> "SD card"
|
||||
FavStorage.OTHER -> "Storage"
|
||||
FavStorage.INTERNAL -> stringResource(R.string.games_internal)
|
||||
FavStorage.SD -> stringResource(R.string.fm_drive_sd)
|
||||
FavStorage.OTHER -> stringResource(R.string.fm_drive_storage)
|
||||
},
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
@@ -464,7 +508,7 @@ private fun FavoriteCard(
|
||||
IconButton(onClick = onUnpin) {
|
||||
Icon(
|
||||
Icons.Filled.Star,
|
||||
contentDescription = "Remove from favorites",
|
||||
contentDescription = stringResource(R.string.fm_unpin_favorite),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(22.dp),
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@ import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.annotation.StringRes
|
||||
import com.droiddeck.launcher.R
|
||||
import java.io.File
|
||||
|
||||
// The File Manager's non-UI helpers: sorting, paths, conflict choices and favourite locations.
|
||||
@@ -77,7 +79,7 @@ enum class FavStorage { INTERNAL, SD, OTHER }
|
||||
|
||||
data class FavLocation(
|
||||
val storage: FavStorage,
|
||||
val driveLabel: String, // "Internal", "SD card", or "Storage"
|
||||
@StringRes val driveLabel: Int, // Internal, SD card, or Storage
|
||||
val displayPath: String // the unix absolute path
|
||||
)
|
||||
|
||||
@@ -87,15 +89,20 @@ fun describeLocation(file: File): FavLocation {
|
||||
|
||||
val internal = "/storage/emulated/0"
|
||||
if (abs == internal || abs.startsWith("$internal/")) {
|
||||
return FavLocation(FavStorage.INTERNAL, "Internal", abs)
|
||||
return FavLocation(FavStorage.INTERNAL, R.string.fm_drive_internal, abs)
|
||||
}
|
||||
|
||||
if (abs.startsWith("/storage/")) {
|
||||
val name = abs.removePrefix("/storage/").substringBefore('/')
|
||||
if (name.isNotEmpty() && name != "emulated" && name != "self") {
|
||||
return FavLocation(FavStorage.SD, "SD card", abs)
|
||||
return FavLocation(FavStorage.SD, R.string.fm_drive_sd, abs)
|
||||
}
|
||||
}
|
||||
|
||||
return FavLocation(FavStorage.OTHER, "Storage", abs)
|
||||
// The Linux system's root (the app's files/linuxfs, the sessions' /) and anything under it.
|
||||
if (Regex("/files/linuxfs(/|$)").containsMatchIn(abs)) {
|
||||
return FavLocation(FavStorage.OTHER, R.string.fm_drive_linux, abs)
|
||||
}
|
||||
|
||||
return FavLocation(FavStorage.OTHER, R.string.fm_drive_storage, abs)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
@@ -41,6 +42,7 @@ import androidx.compose.material.icons.filled.ViewList
|
||||
import androidx.compose.material.icons.filled.StarBorder
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material.icons.filled.Smartphone
|
||||
import androidx.compose.material.icons.filled.Terminal
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Description
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
@@ -76,6 +78,9 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.droiddeck.launcher.R
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
@@ -140,7 +145,14 @@ fun FileManagerScreen(
|
||||
val name = abs.removePrefix("/storage/").substringBefore('/')
|
||||
if (name.isNotEmpty() && name != "emulated" && name != "self") File("/storage/$name") else rootDir
|
||||
}
|
||||
else -> rootDir
|
||||
// A game's own folder or Proton prefix lives in the app's private storage, which has no
|
||||
// volume above it: floor at the app's files dir so up/back still reach the library roots.
|
||||
else -> {
|
||||
val appFiles = runCatching { context.filesDir.canonicalPath }.getOrNull()
|
||||
generateSequence(rootDir) { it.parentFile }
|
||||
.firstOrNull { appFiles != null && runCatching { it.canonicalPath }.getOrNull() == appFiles }
|
||||
?: rootDir
|
||||
}
|
||||
}
|
||||
mutableStateOf(vol)
|
||||
}
|
||||
@@ -229,7 +241,9 @@ fun FileManagerScreen(
|
||||
loadDirectory(dir)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) { openDrive(rootDir) }
|
||||
// Open the start dir without openDrive(): that pins the floor to the start dir itself, which
|
||||
// greyed out up/back whenever the manager was opened at a game's folder.
|
||||
LaunchedEffect(Unit) { loadDirectory(rootDir) }
|
||||
|
||||
// System/gesture Back: while the Favorites view is open it closes that first; otherwise
|
||||
// it goes up one directory. Only at the current drive's root with Favorites closed is it
|
||||
@@ -243,6 +257,14 @@ fun FileManagerScreen(
|
||||
if (parent != null && parent.exists()) loadDirectory(parent)
|
||||
}
|
||||
|
||||
// One level up, never above the current drive's root: shared by the toolbar arrow and the
|
||||
// ".." row at the top of the list.
|
||||
val canGoUp = currentDir != currentRoot && currentDir.parentFile?.exists() == true
|
||||
fun goUp() {
|
||||
val parent = currentDir.parentFile
|
||||
if (currentDir != currentRoot && parent != null && parent.exists()) loadDirectory(parent)
|
||||
}
|
||||
|
||||
// Resolve a non-colliding destination in [dir] for [name] (foo.txt -> "foo (1).txt").
|
||||
fun uniqueDestination(dir: File, name: String): File {
|
||||
var candidate = File(dir, name)
|
||||
@@ -261,11 +283,11 @@ fun FileManagerScreen(
|
||||
fun performDelete(file: File) {
|
||||
scope.launch {
|
||||
isOperationRunning = true
|
||||
operationLabel = "Deleting..."
|
||||
operationLabel = context.getString(R.string.fm_op_deleting)
|
||||
val ok = withContext(Dispatchers.IO) { FileOps.delete(file) }
|
||||
isOperationRunning = false
|
||||
loadDirectory(currentDir, resetScroll = false)
|
||||
if (!ok) Toast.makeText(context, "Delete failed", Toast.LENGTH_SHORT).show()
|
||||
if (!ok) Toast.makeText(context, R.string.fm_delete_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +310,7 @@ fun FileManagerScreen(
|
||||
operationJob = scope.launch {
|
||||
operationProgress = 0f
|
||||
operationDeterminate = true
|
||||
operationLabel = if (cut) "Moving..." else "Copying..."
|
||||
operationLabel = context.getString(if (cut) R.string.fm_op_moving else R.string.fm_op_copying)
|
||||
isOperationRunning = true
|
||||
|
||||
var applyToAll: ConflictChoice? = null
|
||||
@@ -324,10 +346,10 @@ fun FileManagerScreen(
|
||||
}
|
||||
|
||||
// Progress is per item; with a batch the label carries the overall position.
|
||||
operationLabel = buildString {
|
||||
append(if (cut) "Moving" else "Copying")
|
||||
if (sources.size > 1) append(" ${done + 1}/${sources.size}")
|
||||
append(" - ").append(src.name)
|
||||
operationLabel = if (sources.size > 1) {
|
||||
context.getString(if (cut) R.string.fm_op_moving_batch else R.string.fm_op_copying_batch, done + 1, sources.size, src.name)
|
||||
} else {
|
||||
context.getString(if (cut) R.string.fm_op_moving_item else R.string.fm_op_copying_item, src.name)
|
||||
}
|
||||
var lastPct = -1
|
||||
val onProgress = FileOps.ProgressCallback { copied, total ->
|
||||
@@ -355,9 +377,9 @@ fun FileManagerScreen(
|
||||
loadDirectory(currentDir, resetScroll = false)
|
||||
|
||||
val message = when {
|
||||
failed > 0 -> "$done done, $failed failed"
|
||||
skipped > 0 -> "$done done, $skipped skipped"
|
||||
sources.size > 1 -> "$done items ${if (cut) "moved" else "copied"}"
|
||||
failed > 0 -> context.getString(R.string.fm_paste_failed, done, failed)
|
||||
skipped > 0 -> context.getString(R.string.fm_paste_skipped, done, skipped)
|
||||
sources.size > 1 -> context.resources.getQuantityString(if (cut) R.plurals.fm_paste_moved else R.plurals.fm_paste_copied, done, done)
|
||||
else -> null
|
||||
}
|
||||
if (message != null) Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
@@ -367,32 +389,32 @@ fun FileManagerScreen(
|
||||
fun performRename(file: File, newName: String) {
|
||||
val target = File(file.parentFile, newName)
|
||||
if (target.exists()) {
|
||||
Toast.makeText(context, "\"$newName\" already exists", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, context.getString(R.string.fm_already_exists, newName), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
isOperationRunning = true
|
||||
operationLabel = "Renaming..."
|
||||
operationLabel = context.getString(R.string.fm_op_renaming)
|
||||
val ok = withContext(Dispatchers.IO) { file.renameTo(target) }
|
||||
isOperationRunning = false
|
||||
loadDirectory(currentDir, resetScroll = false)
|
||||
if (!ok) Toast.makeText(context, "Rename failed", Toast.LENGTH_SHORT).show()
|
||||
if (!ok) Toast.makeText(context, R.string.fm_rename_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun createFolder(parent: File, name: String) {
|
||||
val target = File(parent, name)
|
||||
if (target.exists()) {
|
||||
Toast.makeText(context, "\"$name\" already exists", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, context.getString(R.string.fm_already_exists, name), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
isOperationRunning = true
|
||||
operationLabel = "Creating folder..."
|
||||
operationLabel = context.getString(R.string.fm_op_creating_folder)
|
||||
val ok = withContext(Dispatchers.IO) { target.mkdirs() }
|
||||
isOperationRunning = false
|
||||
loadDirectory(currentDir, resetScroll = false)
|
||||
if (!ok) Toast.makeText(context, "Could not create folder", Toast.LENGTH_SHORT).show()
|
||||
if (!ok) Toast.makeText(context, R.string.fm_create_folder_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,6 +431,9 @@ fun FileManagerScreen(
|
||||
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
val drives = remember(storageTick) { StorageRoots.list(context) }
|
||||
val linuxRoot = remember(storageTick) {
|
||||
com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context).takeIf { File(it, "usr").isDirectory }
|
||||
}
|
||||
|
||||
// ── Dialogs ──
|
||||
|
||||
@@ -416,12 +441,12 @@ fun FileManagerScreen(
|
||||
var folderName by remember { mutableStateOf("") }
|
||||
OutlinedAlertDialog(
|
||||
onDismissRequest = { showNewFolderDialog = false },
|
||||
title = { Text("New Folder") },
|
||||
title = { Text(stringResource(R.string.fm_new_folder)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = folderName,
|
||||
onValueChange = { folderName = it },
|
||||
label = { Text("Folder name") },
|
||||
label = { Text(stringResource(R.string.fm_folder_name)) },
|
||||
singleLine = true,
|
||||
)
|
||||
},
|
||||
@@ -429,9 +454,9 @@ fun FileManagerScreen(
|
||||
TextButton(onClick = {
|
||||
showNewFolderDialog = false
|
||||
if (folderName.isNotBlank()) createFolder(currentDir, folderName)
|
||||
}) { Text("Create") }
|
||||
}) { Text(stringResource(R.string.fm_create)) }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { showNewFolderDialog = false }) { Text("Cancel") } },
|
||||
dismissButton = { TextButton(onClick = { showNewFolderDialog = false }) { Text(stringResource(R.string.common_cancel)) } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -439,12 +464,12 @@ fun FileManagerScreen(
|
||||
var newName by remember(renameTarget) { mutableStateOf(renameTarget?.name ?: "") }
|
||||
OutlinedAlertDialog(
|
||||
onDismissRequest = { renameTarget = null },
|
||||
title = { Text("Rename") },
|
||||
title = { Text(stringResource(R.string.fm_rename)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = newName,
|
||||
onValueChange = { newName = it },
|
||||
label = { Text("New name") },
|
||||
label = { Text(stringResource(R.string.fm_new_name)) },
|
||||
singleLine = true,
|
||||
)
|
||||
},
|
||||
@@ -453,9 +478,9 @@ fun FileManagerScreen(
|
||||
val file = renameTarget
|
||||
renameTarget = null
|
||||
if (file != null && newName.isNotBlank()) performRename(file, newName)
|
||||
}) { Text("Rename") }
|
||||
}) { Text(stringResource(R.string.fm_rename)) }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { renameTarget = null }) { Text("Cancel") } },
|
||||
dismissButton = { TextButton(onClick = { renameTarget = null }) { Text(stringResource(R.string.common_cancel)) } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -473,15 +498,15 @@ fun FileManagerScreen(
|
||||
val file = selectedEntry ?: return
|
||||
OutlinedAlertDialog(
|
||||
onDismissRequest = { selectedEntry = null },
|
||||
title = { Text("Delete?") },
|
||||
text = { Text("Delete \"${file.name}\" permanently?") },
|
||||
title = { Text(stringResource(R.string.fm_delete_title)) },
|
||||
text = { Text(stringResource(R.string.fm_delete_text, file.name)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
selectedEntry = null
|
||||
performDelete(file)
|
||||
}) { Text("Delete") }
|
||||
}) { Text(stringResource(R.string.common_delete)) }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { selectedEntry = null }) { Text("Cancel") } },
|
||||
dismissButton = { TextButton(onClick = { selectedEntry = null }) { Text(stringResource(R.string.common_cancel)) } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -489,10 +514,10 @@ fun FileManagerScreen(
|
||||
val victims = pendingBulkDelete
|
||||
OutlinedAlertDialog(
|
||||
onDismissRequest = { pendingBulkDelete = emptyList() },
|
||||
title = { Text("Delete ${victims.size} item${if (victims.size == 1) "" else "s"}?") },
|
||||
title = { Text(pluralStringResource(R.plurals.fm_delete_items_title, victims.size, victims.size)) },
|
||||
text = {
|
||||
Column {
|
||||
Text("This can't be undone.")
|
||||
Text(stringResource(R.string.fm_cannot_undo))
|
||||
Spacer(Modifier.height(6.dp))
|
||||
// Name a few so an accidental Select-All is obvious before it's too late.
|
||||
victims.take(5).forEach {
|
||||
@@ -500,7 +525,7 @@ fun FileManagerScreen(
|
||||
}
|
||||
if (victims.size > 5) {
|
||||
Text(
|
||||
"…and ${victims.size - 5} more",
|
||||
pluralStringResource(R.plurals.fm_and_more, victims.size - 5, victims.size - 5),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
@@ -516,19 +541,19 @@ fun FileManagerScreen(
|
||||
isOperationRunning = true
|
||||
var failed = 0
|
||||
victims.forEachIndexed { i, f ->
|
||||
operationLabel = "Deleting ${i + 1}/${victims.size} - ${f.name}"
|
||||
operationLabel = context.getString(R.string.fm_op_deleting_batch, i + 1, victims.size, f.name)
|
||||
if (!withContext(Dispatchers.IO) { FileOps.delete(f) }) failed++
|
||||
}
|
||||
isOperationRunning = false
|
||||
operationJob = null
|
||||
loadDirectory(currentDir, resetScroll = false)
|
||||
if (failed > 0) {
|
||||
Toast.makeText(context, "$failed couldn't be deleted", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, context.resources.getQuantityString(R.plurals.fm_delete_some_failed, failed, failed), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}) { Text("Delete", color = MaterialTheme.colorScheme.error) }
|
||||
}) { Text(stringResource(R.string.common_delete), color = MaterialTheme.colorScheme.error) }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { pendingBulkDelete = emptyList() }) { Text("Cancel") } },
|
||||
dismissButton = { TextButton(onClick = { pendingBulkDelete = emptyList() }) { Text(stringResource(R.string.common_cancel)) } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -537,12 +562,12 @@ fun FileManagerScreen(
|
||||
val isDir = conflict.isDirectory
|
||||
OutlinedAlertDialog(
|
||||
onDismissRequest = { pendingConflict = null },
|
||||
title = { Text("\"${conflict.name}\" already exists") },
|
||||
title = { Text(stringResource(R.string.fm_already_exists, conflict.name)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
if (isDir) "Merge adds and replaces files inside the existing folder."
|
||||
else "Overwrite replaces the existing file.",
|
||||
if (isDir) stringResource(R.string.fm_conflict_merge_hint)
|
||||
else stringResource(R.string.fm_conflict_overwrite_hint),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
@@ -553,15 +578,15 @@ fun FileManagerScreen(
|
||||
checked = conflictApplyToAll,
|
||||
onCheckedChange = { conflictApplyToAll = it },
|
||||
)
|
||||
Text("Apply to all conflicts", fontSize = 12.sp)
|
||||
Text(stringResource(R.string.fm_conflict_apply_all), fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
listOf(
|
||||
(if (isDir) ConflictChoice.MERGE else ConflictChoice.OVERWRITE) to
|
||||
(if (isDir) "Merge" else "Overwrite"),
|
||||
ConflictChoice.KEEP_BOTH to "Keep both",
|
||||
ConflictChoice.SKIP to "Skip",
|
||||
(if (isDir) stringResource(R.string.fm_conflict_merge) else stringResource(R.string.fm_conflict_overwrite)),
|
||||
ConflictChoice.KEEP_BOTH to stringResource(R.string.fm_conflict_keep_both),
|
||||
ConflictChoice.SKIP to stringResource(R.string.fm_conflict_skip),
|
||||
).forEach { (choice, label) ->
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -573,7 +598,7 @@ fun FileManagerScreen(
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { conflictChoice = ConflictChoice.SKIP; pendingConflict = null }) {
|
||||
Text("Cancel")
|
||||
Text(stringResource(R.string.common_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -603,7 +628,7 @@ fun FileManagerScreen(
|
||||
) {
|
||||
Icon(Icons.Filled.Folder, null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Select this folder")
|
||||
Text(stringResource(R.string.fm_select_this_folder))
|
||||
}
|
||||
}
|
||||
// ── Path bar ──
|
||||
@@ -614,15 +639,11 @@ fun FileManagerScreen(
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
val parent = currentDir.parentFile
|
||||
// Don't climb above the current drive's root.
|
||||
if (currentDir != currentRoot && parent != null && parent.exists()) loadDirectory(parent)
|
||||
}, enabled = currentDir != currentRoot) {
|
||||
Icon(Icons.Filled.ArrowBack, "Back", tint = MaterialTheme.colorScheme.primary)
|
||||
IconButton(onClick = { goUp() }, enabled = canGoUp) {
|
||||
Icon(Icons.Filled.ArrowBack, stringResource(R.string.fm_back), tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
|
||||
val currentDriveLabel = describeLocation(currentDir).driveLabel
|
||||
val currentDriveLabel = stringResource(describeLocation(currentDir).driveLabel)
|
||||
// Dim the drive chip while the Favorites list is open (it's not the active context).
|
||||
val driveChipAlpha = if (showFavorites) 0.45f else 1f
|
||||
Box {
|
||||
@@ -666,13 +687,23 @@ fun FileManagerScreen(
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"${drive.label} is mounted but not readable right now",
|
||||
context.getString(R.string.fm_drive_not_readable, drive.label),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
linuxRoot?.let { root ->
|
||||
MenuItemDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.fm_drive_linux)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Filled.Terminal, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp))
|
||||
},
|
||||
onClick = { showDriveMenu = false; openDrive(root) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,7 +711,7 @@ fun FileManagerScreen(
|
||||
|
||||
if (showFavorites) {
|
||||
Text(
|
||||
text = "Favorites",
|
||||
text = stringResource(R.string.fm_favorites),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
@@ -718,7 +749,7 @@ fun FileManagerScreen(
|
||||
) {
|
||||
Icon(Icons.Filled.CreateNewFolder, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(5.dp))
|
||||
Text("New Folder", color = MaterialTheme.colorScheme.onBackground, fontSize = 12.sp)
|
||||
Text(stringResource(R.string.fm_new_folder), color = MaterialTheme.colorScheme.onBackground, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
@@ -727,19 +758,19 @@ fun FileManagerScreen(
|
||||
}) {
|
||||
Icon(
|
||||
if (gridView) Icons.Filled.ViewList else Icons.Filled.GridView,
|
||||
if (gridView) "List view" else "Grid view",
|
||||
if (gridView) stringResource(R.string.fm_list_view) else stringResource(R.string.fm_grid_view),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { showSearch = !showSearch; if (!showSearch) searchQuery = "" }) {
|
||||
Icon(Icons.Filled.Search, "Search", tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Icon(Icons.Filled.Search, stringResource(R.string.fm_search), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { showSortMenu = true }) {
|
||||
Icon(Icons.Filled.Sort, "Sort", tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Icon(Icons.Filled.Sort, stringResource(R.string.fm_sort), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
DropdownMenu(expanded = showSortMenu, onDismissRequest = { showSortMenu = false }) {
|
||||
listOf("name" to "Name", "date" to "Date modified", "size" to "Size")
|
||||
listOf("name" to stringResource(R.string.fm_sort_name), "date" to stringResource(R.string.fm_sort_date), "size" to stringResource(R.string.fm_prop_size))
|
||||
.forEach { (key, label) ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
@@ -758,7 +789,7 @@ fun FileManagerScreen(
|
||||
}
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (compactRows) "Comfortable rows" else "Compact rows") },
|
||||
text = { Text(if (compactRows) stringResource(R.string.fm_rows_comfortable) else stringResource(R.string.fm_rows_compact)) },
|
||||
onClick = {
|
||||
compactRows = !compactRows
|
||||
browsePrefs.edit().putBoolean("fmCompactRows", compactRows).apply()
|
||||
@@ -766,7 +797,7 @@ fun FileManagerScreen(
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (showHidden) "Hide hidden files" else "Show hidden files") },
|
||||
text = { Text(if (showHidden) stringResource(R.string.fm_hide_hidden) else stringResource(R.string.fm_show_hidden)) },
|
||||
onClick = {
|
||||
showHidden = !showHidden
|
||||
browsePrefs.edit().putBoolean("fmShowHidden", showHidden).apply()
|
||||
@@ -781,9 +812,9 @@ fun FileManagerScreen(
|
||||
// Star toggle: open/close the dedicated Favorites list.
|
||||
IconButton(onClick = { showFavorites = !showFavorites }) {
|
||||
if (showFavorites) {
|
||||
Icon(Icons.Filled.Star, "Hide favorites", tint = MaterialTheme.colorScheme.primary)
|
||||
Icon(Icons.Filled.Star, stringResource(R.string.fm_hide_favorites), tint = MaterialTheme.colorScheme.primary)
|
||||
} else {
|
||||
Icon(Icons.Filled.StarBorder, "Show favorites", tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Icon(Icons.Filled.StarBorder, stringResource(R.string.fm_show_favorites), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -794,7 +825,7 @@ fun FileManagerScreen(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
singleLine = true,
|
||||
placeholder = { Text("Filter this folder", fontSize = 13.sp) },
|
||||
placeholder = { Text(stringResource(R.string.fm_filter_folder), fontSize = 13.sp) },
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
@@ -821,7 +852,7 @@ fun FileManagerScreen(
|
||||
)
|
||||
if (freeSpace > 0) {
|
||||
Text(
|
||||
"${FileOps.formatBytes(freeSpace)} free",
|
||||
stringResource(R.string.fm_free_space, FileOps.formatBytes(context, freeSpace)),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
@@ -842,7 +873,7 @@ fun FileManagerScreen(
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
"${selectedPaths.size} selected",
|
||||
stringResource(R.string.fm_selected_count, selectedPaths.size),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -855,7 +886,7 @@ fun FileManagerScreen(
|
||||
else entries.map { it.absolutePath }.toSet()
|
||||
},
|
||||
contentPadding = selBarPadding,
|
||||
) { Text(if (selectedPaths.size == entries.size) "None" else "All", fontSize = 12.sp) }
|
||||
) { Text(if (selectedPaths.size == entries.size) stringResource(R.string.fm_select_none) else stringResource(R.string.fm_select_all), fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
enabled = selectedPaths.isNotEmpty(),
|
||||
@@ -866,7 +897,7 @@ fun FileManagerScreen(
|
||||
selectedPaths = emptySet()
|
||||
},
|
||||
contentPadding = selBarPadding,
|
||||
) { Text("Copy", fontSize = 12.sp) }
|
||||
) { Text(stringResource(R.string.fm_copy), fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
enabled = selectedPaths.isNotEmpty(),
|
||||
@@ -877,19 +908,19 @@ fun FileManagerScreen(
|
||||
selectedPaths = emptySet()
|
||||
},
|
||||
contentPadding = selBarPadding,
|
||||
) { Text("Cut", fontSize = 12.sp) }
|
||||
) { Text(stringResource(R.string.fm_cut), fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
enabled = selectedPaths.isNotEmpty(),
|
||||
onClick = { pendingBulkDelete = entries.filter { it.absolutePath in selectedPaths } },
|
||||
contentPadding = selBarPadding,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
|
||||
) { Text("Delete", color = MaterialTheme.colorScheme.error, fontSize = 12.sp) }
|
||||
) { Text(stringResource(R.string.common_delete), color = MaterialTheme.colorScheme.error, fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
onClick = { selectionMode = false; selectedPaths = emptySet() },
|
||||
contentPadding = selBarPadding,
|
||||
) { Text("Done", fontSize = 12.sp) }
|
||||
) { Text(stringResource(R.string.fm_done), fontSize = 12.sp) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,16 +934,16 @@ fun FileManagerScreen(
|
||||
.clickable { performPaste() }
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(Icons.Filled.ContentPaste, "Paste", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp))
|
||||
Icon(Icons.Filled.ContentPaste, stringResource(R.string.fm_paste), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
val what = if (clipboardFiles.size == 1) clipboardFiles.first().name
|
||||
else "${clipboardFiles.size} items"
|
||||
else pluralStringResource(R.plurals.fm_items, clipboardFiles.size, clipboardFiles.size)
|
||||
Text(
|
||||
"Paste $what${if (isCutOperation) " (move)" else ""} here",
|
||||
if (isCutOperation) stringResource(R.string.fm_paste_move_here, what) else stringResource(R.string.fm_paste_here, what),
|
||||
color = MaterialTheme.colorScheme.onBackground, fontSize = 13.sp, modifier = Modifier.weight(1f),
|
||||
)
|
||||
TextButton(onClick = { clipboardFiles = emptyList(); isCutOperation = false }) {
|
||||
Text("Cancel", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp)
|
||||
Text(stringResource(R.string.common_cancel), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -944,7 +975,7 @@ fun FileManagerScreen(
|
||||
isOperationRunning = false
|
||||
operationDeterminate = false
|
||||
loadDirectory(currentDir, resetScroll = false)
|
||||
}) { Text("Cancel", fontSize = 12.sp) }
|
||||
}) { Text(stringResource(R.string.common_cancel), fontSize = 12.sp) }
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
@@ -970,38 +1001,39 @@ fun FileManagerScreen(
|
||||
// shown in pick mode (the themed picker keeps its slim layout). Built each recompose (cheap)
|
||||
// so it tracks the current drive/favourites without stale click lambdas.
|
||||
val fmRailState = rememberRailState("filemanager")
|
||||
fun locItem(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, dir: File) =
|
||||
RailItem(label, icon, !showFavorites && currentRoot.absolutePath == dir.absolutePath) {
|
||||
fun locItem(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, dir: File, shortLabel: String? = null) =
|
||||
RailItem(label, icon, !showFavorites && currentRoot.absolutePath == dir.absolutePath, shortLabel = shortLabel) {
|
||||
showFavorites = false; openDrive(dir)
|
||||
}
|
||||
val storageItems = buildList {
|
||||
add(locItem("Internal", Icons.Filled.Smartphone, File("/storage/emulated/0")))
|
||||
add(locItem(stringResource(R.string.fm_drive_internal), Icons.Filled.Smartphone, File("/storage/emulated/0")))
|
||||
drives.filter { it.removable }.forEach { d ->
|
||||
add(RailItem(d.label, Icons.Filled.SdStorage, !showFavorites && currentRoot.absolutePath == d.dir.absolutePath) {
|
||||
showFavorites = false; if (d.readable) openDrive(d.dir)
|
||||
})
|
||||
}
|
||||
// The Linux system the sessions run in: its own / (the desktop's root), once installed.
|
||||
linuxRoot?.let { add(locItem(stringResource(R.string.fm_drive_linux), Icons.Filled.Terminal, it)) }
|
||||
}
|
||||
val quickItems = buildList {
|
||||
File("/storage/emulated/0/Download").takeIf { it.isDirectory }?.let { add(locItem("Downloads", Icons.Filled.Download, it)) }
|
||||
File("/storage/emulated/0/Download").takeIf { it.isDirectory }?.let { add(locItem(stringResource(R.string.fm_rail_downloads), Icons.Filled.Download, it, stringResource(R.string.fm_rail_downloads_short))) }
|
||||
// The ROMs folder chosen on the main screen: what the session shows as /root/ROMs.
|
||||
SessionPrefs.romsDir(context).takeIf { it.isNotEmpty() }?.let(::File)?.takeIf { it.isDirectory }
|
||||
?.let { add(locItem("ROMs", Icons.Filled.SportsEsports, it)) }
|
||||
File("/storage/emulated/0/Download/DroidDeck").takeIf { it.isDirectory }?.let { add(locItem("Session logs", Icons.Filled.Description, it)) }
|
||||
File("/storage/emulated/0/Pictures").takeIf { it.isDirectory }?.let { add(locItem("Pictures", Icons.Filled.Image, it)) }
|
||||
?.let { add(locItem(stringResource(R.string.fm_rail_roms), Icons.Filled.SportsEsports, it)) }
|
||||
File("/storage/emulated/0/Pictures").takeIf { it.isDirectory }?.let { add(locItem(stringResource(R.string.fm_rail_pictures), Icons.Filled.Image, it)) }
|
||||
}
|
||||
val favItems = remember(favTick) { FavoritesStore.list(context).map(::File).filter { it.exists() } }
|
||||
.map { d -> RailItem(d.name, Icons.Filled.Star, false) { showFavorites = false; openDrive(d) } }
|
||||
val locationSections = buildList {
|
||||
add(RailSection("STORAGE", storageItems))
|
||||
if (quickItems.isNotEmpty()) add(RailSection("QUICK", quickItems))
|
||||
if (favItems.isNotEmpty()) add(RailSection("FAVORITES", favItems))
|
||||
add(RailSection(stringResource(R.string.fm_rail_storage), storageItems))
|
||||
if (quickItems.isNotEmpty()) add(RailSection(stringResource(R.string.fm_rail_quick), quickItems))
|
||||
if (favItems.isNotEmpty()) add(RailSection(stringResource(R.string.fm_rail_favorites), favItems))
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||
// The slim picker keeps its layout: no rail.
|
||||
if (!pickMode) {
|
||||
CollapsibleRail(state = fmRailState, title = "Files", sections = locationSections, outlinedItems = true)
|
||||
CollapsibleRail(state = fmRailState, title = stringResource(R.string.fm_rail_title), sections = locationSections, outlinedItems = true)
|
||||
}
|
||||
Box(modifier = Modifier.weight(1f).fillMaxSize()) {
|
||||
// ── Favorites list OR file list ──
|
||||
@@ -1012,7 +1044,7 @@ fun FileManagerScreen(
|
||||
onPinCurrent = {
|
||||
FavoritesStore.add(context, currentDir.absolutePath)
|
||||
favTick++
|
||||
Toast.makeText(context, "Added \"${currentDir.name}\" to Favorites", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, context.getString(R.string.fm_favorite_added, currentDir.name), Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onJump = { dir ->
|
||||
showFavorites = false
|
||||
@@ -1021,7 +1053,7 @@ fun FileManagerScreen(
|
||||
onUnpin = { dir ->
|
||||
FavoritesStore.remove(context, dir.absolutePath)
|
||||
favTick++
|
||||
Toast.makeText(context, "Removed \"${dir.name}\" from Favorites", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, context.getString(R.string.fm_favorite_removed, dir.name), Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
@@ -1041,6 +1073,9 @@ fun FileManagerScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
) {
|
||||
if (canGoUp) item(key = "..", span = { GridItemSpan(maxLineSpan) }) {
|
||||
ParentFolderRow(compact = compactRows, onTap = { goUp() })
|
||||
}
|
||||
items(shownEntries, key = { it.absolutePath }) { file ->
|
||||
val isFav = remember(file.absolutePath, favTick) {
|
||||
FavoritesStore.isFavorite(context, file.absolutePath)
|
||||
@@ -1095,8 +1130,8 @@ fun FileManagerScreen(
|
||||
showMenuFor = null
|
||||
Toast.makeText(
|
||||
context,
|
||||
if (nowFav) "Added \"${file.name}\" to Favorites"
|
||||
else "Removed \"${file.name}\" from Favorites",
|
||||
if (nowFav) context.getString(R.string.fm_favorite_added, file.name)
|
||||
else context.getString(R.string.fm_favorite_removed, file.name),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
},
|
||||
@@ -1106,13 +1141,16 @@ fun FileManagerScreen(
|
||||
}
|
||||
} else
|
||||
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
|
||||
if (canGoUp) item(key = "..") {
|
||||
ParentFolderRow(compact = compactRows, onTap = { goUp() })
|
||||
}
|
||||
if (entries.isEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text("Empty directory", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(stringResource(R.string.fm_empty_directory), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1173,8 +1211,8 @@ fun FileManagerScreen(
|
||||
showMenuFor = null
|
||||
Toast.makeText(
|
||||
context,
|
||||
if (nowFav) "Added \"${file.name}\" to Favorites"
|
||||
else "Removed \"${file.name}\" from Favorites",
|
||||
if (nowFav) context.getString(R.string.fm_favorite_added, file.name)
|
||||
else context.getString(R.string.fm_favorite_removed, file.name),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.droiddeck.launcher.files
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.R
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
@@ -19,9 +21,9 @@ object FileOps {
|
||||
/** Byte-accurate copy progress (copied/total bytes), reported as the copy proceeds. */
|
||||
fun interface ProgressCallback { fun onProgress(copiedBytes: Long, totalBytes: Long) }
|
||||
|
||||
fun formatBytes(bytes: Long): String {
|
||||
if (bytes <= 0) return "0 bytes"
|
||||
val units = arrayOf("bytes", "KB", "MB", "GB", "TB")
|
||||
fun formatBytes(context: Context, bytes: Long): String {
|
||||
if (bytes <= 0) return context.getString(R.string.fm_size_zero)
|
||||
val units = arrayOf(context.getString(R.string.fm_unit_bytes), "KB", "MB", "GB", "TB")
|
||||
val group = (Math.log10(bytes.toDouble()) / Math.log10(1024.0)).toInt().coerceIn(0, units.lastIndex)
|
||||
return String.format(Locale.ENGLISH, "%.2f", bytes / Math.pow(1024.0, group.toDouble())) + " " + units[group]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.ui.DroidDeckTheme
|
||||
import java.io.File
|
||||
|
||||
@@ -26,6 +27,11 @@ import java.io.File
|
||||
* - [EXTRA_PICKER_TITLE] the header
|
||||
*/
|
||||
class FilePickerActivity : ComponentActivity() {
|
||||
override fun attachBaseContext(newBase: android.content.Context) {
|
||||
super.attachBaseContext(newBase)
|
||||
com.droiddeck.launcher.core.AppLanguage.applyTo(this, newBase)
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -43,14 +49,14 @@ class FilePickerActivity : ComponentActivity() {
|
||||
?.let { File(it) }?.takeIf { it.isDirectory }
|
||||
|
||||
setContent {
|
||||
DroidDeckTheme {
|
||||
DroidDeckTheme(appScale = com.droiddeck.launcher.core.AppUiPrefs.scale(this)) {
|
||||
Surface(modifier = Modifier.fillMaxSize().systemBarsPadding(), color = MaterialTheme.colorScheme.background) {
|
||||
FileManagerScreen(
|
||||
pickMode = true,
|
||||
pickDirMode = pickDir,
|
||||
pickExtensions = extensions,
|
||||
initialDir = initialDir,
|
||||
pickerTitle = title ?: if (pickDir) "Choose a folder" else "Choose a file",
|
||||
pickerTitle = title ?: getString(if (pickDir) R.string.fm_choose_folder else R.string.fm_choose_file),
|
||||
onPick = { file ->
|
||||
setResult(Activity.RESULT_OK, Intent().putExtra(EXTRA_SELECTED_FILE, file.absolutePath))
|
||||
finish()
|
||||
|
||||
@@ -25,6 +25,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.droiddeck.launcher.R
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -66,7 +68,7 @@ internal fun FilePropertiesDialog(
|
||||
|
||||
OutlinedAlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Properties") },
|
||||
title = { Text(stringResource(R.string.fm_properties)) },
|
||||
text = {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
// ── Basic info ──
|
||||
@@ -79,14 +81,14 @@ internal fun FilePropertiesDialog(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
PropertyLine("Location", file.parent ?: "-")
|
||||
PropertyLine(stringResource(R.string.fm_prop_location), file.parent ?: "-")
|
||||
PropertyLine(
|
||||
"Type",
|
||||
if (file.isDirectory) "Folder"
|
||||
else file.extension.uppercase().let { if (it.isBlank()) "File" else "$it file" },
|
||||
stringResource(R.string.fm_prop_type),
|
||||
if (file.isDirectory) stringResource(R.string.fm_type_folder)
|
||||
else file.extension.uppercase().let { if (it.isBlank()) stringResource(R.string.fm_type_file) else stringResource(R.string.fm_type_ext_file, it) },
|
||||
)
|
||||
if (!file.isDirectory) PropertyLine("Size", FileOps.formatBytes(file.length()))
|
||||
PropertyLine("Modified", dateFormat.format(Date(file.lastModified())))
|
||||
if (!file.isDirectory) PropertyLine(stringResource(R.string.fm_prop_size), FileOps.formatBytes(context, file.length()))
|
||||
PropertyLine(stringResource(R.string.fm_prop_modified), dateFormat.format(Date(file.lastModified())))
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
|
||||
@@ -95,8 +97,8 @@ internal fun FilePropertiesDialog(
|
||||
val state = attrs
|
||||
// ── Read-only ── checked when the owner can't write (Wine's FILE_ATTRIBUTE_READONLY).
|
||||
AttributeToggleRow(
|
||||
label = "Read-only",
|
||||
description = "Stops games (and Wine) from overwriting or deleting this file.",
|
||||
label = stringResource(R.string.fm_read_only),
|
||||
description = stringResource(R.string.fm_read_only_hint),
|
||||
checked = state?.readOnly == true,
|
||||
enabled = state != null && !busy,
|
||||
onToggle = { want ->
|
||||
@@ -108,7 +110,7 @@ internal fun FilePropertiesDialog(
|
||||
attrs = attrs?.copy(readOnly = want)
|
||||
onChanged()
|
||||
} else {
|
||||
Toast.makeText(context, "Couldn't change Read-only", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, R.string.fm_read_only_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -117,10 +119,10 @@ internal fun FilePropertiesDialog(
|
||||
// ── Hidden ── Wine's DOS hidden bit in the user.DOSATTRIB xattr.
|
||||
val hiddenSupported = state?.hiddenSupported == true
|
||||
AttributeToggleRow(
|
||||
label = "Hidden",
|
||||
label = stringResource(R.string.fm_hidden),
|
||||
description = if (state != null && !hiddenSupported)
|
||||
"This storage can't store the hidden flag."
|
||||
else "Marks the file hidden in Windows (Wine's hidden attribute).",
|
||||
stringResource(R.string.fm_hidden_unsupported)
|
||||
else stringResource(R.string.fm_hidden_hint),
|
||||
checked = state?.hidden == true,
|
||||
enabled = state != null && hiddenSupported && !busy,
|
||||
onToggle = { want ->
|
||||
@@ -134,14 +136,14 @@ internal fun FilePropertiesDialog(
|
||||
} else {
|
||||
// The write failed after all - disable the toggle rather than lie.
|
||||
attrs = attrs?.copy(hiddenSupported = false)
|
||||
Toast.makeText(context, "Couldn't change Hidden on this storage", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, R.string.fm_hidden_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text("Done") } },
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.fm_done)) } },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.os.storage.StorageVolume
|
||||
import com.droiddeck.launcher.R
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -63,7 +64,7 @@ object StorageRoots {
|
||||
|
||||
// Internal storage is never removable and never absent; seed it first so it heads the menu.
|
||||
volumeOf(PRIMARY_KEY).apply {
|
||||
label = "Internal"
|
||||
label = context.getString(R.string.fm_drive_internal)
|
||||
removable = false
|
||||
candidates += File(INTERNAL_PATH)
|
||||
}
|
||||
@@ -109,13 +110,13 @@ object StorageRoots {
|
||||
entry.seeds += dir
|
||||
}
|
||||
|
||||
val roots = volumes.values.map { it.toRoot() }
|
||||
val roots = volumes.values.map { it.toRoot(context) }
|
||||
val internal = roots.filter { !it.removable }
|
||||
val removable = roots.filter { it.removable }.sortedBy { it.label }
|
||||
return disambiguate(internal + removable)
|
||||
}
|
||||
|
||||
private fun Volume.toRoot(): StorageRoot {
|
||||
private fun Volume.toRoot(context: Context): StorageRoot {
|
||||
// Prefer a path we can actually list; a readable child beats an unreadable root.
|
||||
val dir = candidates.firstOrNull(::canBrowse)
|
||||
?: candidates.firstNotNullOfOrNull { root -> seeds.firstNotNullOfOrNull { deepestReadable(root, it) } }
|
||||
@@ -125,14 +126,14 @@ object StorageRoots {
|
||||
?: File("/storage/$key")
|
||||
|
||||
return StorageRoot(
|
||||
label = label ?: defaultLabel(),
|
||||
label = label ?: defaultLabel(context),
|
||||
dir = dir,
|
||||
removable = removable,
|
||||
readable = canBrowse(dir),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Volume.defaultLabel(): String = if (removable) "SD card" else key
|
||||
private fun Volume.defaultLabel(context: Context): String = if (removable) context.getString(R.string.fm_drive_sd) else key
|
||||
|
||||
/** Appends the volume id to any label used by more than one volume, so entries stay tellable apart. */
|
||||
private fun disambiguate(roots: List<StorageRoot>): List<StorageRoot> {
|
||||
@@ -183,12 +184,12 @@ object StorageRoots {
|
||||
* name like "android", which tells the user nothing about which card they are looking at.
|
||||
*/
|
||||
private fun labelFor(context: Context, volume: StorageVolume, uuid: String?): String {
|
||||
if (volume.isPrimary) return "Internal"
|
||||
if (volume.isRemovable) return "SD card"
|
||||
if (volume.isPrimary) return context.getString(R.string.fm_drive_internal)
|
||||
if (volume.isRemovable) return context.getString(R.string.fm_drive_sd)
|
||||
val description = volume.getDescription(context)?.trim()
|
||||
return description?.takeIf { it.isNotBlank() && !it.equals("android", ignoreCase = true) }
|
||||
?: uuid
|
||||
?: "Storage"
|
||||
?: context.getString(R.string.fm_drive_storage)
|
||||
}
|
||||
|
||||
private fun isEmulated(dir: File): Boolean = dir.absolutePath.startsWith(EMULATED_PREFIX)
|
||||
|
||||
@@ -96,40 +96,89 @@ 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 || folder.name.equals("steamapps", ignoreCase = true)) 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)
|
||||
val chosen = SessionPrefs.addedGameExe(context, folder.path).takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.isFile }
|
||||
val exe = chosen ?: candidates.firstOrNull() ?: return
|
||||
val guestExe = guestPath(context, exe)
|
||||
if (guestExe == null) { Log.w(TAG, "${folder.name}: the session cannot see ${exe.path}"); return }
|
||||
val guestDir = guestPath(context, exe.parentFile ?: folder) ?: return
|
||||
val picked = SessionPrefs.addedGameExe(context, folder.path)
|
||||
val chosen = picked.takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.isFile }
|
||||
val found = chosen ?: candidates.firstOrNull() ?: return
|
||||
val foundGuest = guestPath(context, found)
|
||||
if (foundGuest == null) { Log.w(TAG, "${folder.name}: the session cannot see ${found.path}"); return }
|
||||
val name = folder.name
|
||||
// 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)
|
||||
val crc = CRC32().apply { update(("\"${foundGuest.replaceFirst(Regex("^$LIBRARY/"), "$LEGACY_LIBRARY/")}\"" + name).toByteArray()) }.value
|
||||
val appId = SessionPrefs.addedGameAppId(context, folder.path, crc or 0x80000000L)
|
||||
val config = accountConfig(File(LinuxRuntime.rootDir(context), "root/.local/share/Steam"))
|
||||
val record = config?.let { shortcutRecord(it, appId) }
|
||||
val rev = record?.optInt("rev", 0) ?: 0
|
||||
val adopted = guestPath(context, folder)
|
||||
?.let { steamTarget(folder, it, record, foundGuest, SessionPrefs.addedGameExeSeen(context, folder.path)) }
|
||||
?.takeIf { SessionPrefs.adoptAddedGameExe(context, folder.path, picked, it.path, rev) }
|
||||
val exe = adopted ?: found
|
||||
val guestExe = if (adopted != null) guestPath(context, adopted) ?: return else foundGuest
|
||||
val guestDir = guestPath(context, exe.parentFile ?: folder) ?: return
|
||||
val steamId = config?.let { steamRoute(it, appId) }
|
||||
out.add(Game(folder, name, exe, guestExe, guestDir, appId,
|
||||
steamId?.toLong() ?: ((appId shl 32) or 0x02000000L), candidates, steamId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun steamRoute(root: File, appId: Long): Int? = runCatching {
|
||||
val users = File(root, "config/loginusers.vdf").readText()
|
||||
val blocks = Regex(""""(\d{5,})"\s*\{([^}]*)\}""")
|
||||
val recent = blocks.findAll(users).firstOrNull { Regex(""""MostRecent"\s*"1"""").containsMatchIn(it.groupValues[2]) }
|
||||
?: return@runCatching null
|
||||
val account = recent.groupValues[1].toLong() - 76561197960265728L
|
||||
JSONObject(File(root, "userdata/$account/config/.droiddeck-routes.json").readText())
|
||||
.optInt(appId.toString()).takeIf { it > 0 }
|
||||
private fun accountConfig(root: File): File? {
|
||||
val users = runCatching { File(root, "config/loginusers.vdf").readText() }.getOrDefault("")
|
||||
val recent = Regex(""""(\d{5,})"\s*\{([^}]*)\}""").findAll(users)
|
||||
.lastOrNull { Regex(""""MostRecent"\s*"1"""").containsMatchIn(it.groupValues[2]) }
|
||||
val account = recent?.groupValues?.get(1)?.toLongOrNull()?.let { (it - 76561197960265728L).toString() }
|
||||
?: File(root, "userdata").list()?.filter { it.isNotEmpty() && it.all(Char::isDigit) && it != "0" }?.singleOrNull()
|
||||
return account?.let { File(root, "userdata/$it/config") }
|
||||
}
|
||||
|
||||
private fun steamRoute(config: File, appId: Long): Int? = runCatching {
|
||||
JSONObject(File(config, ".droiddeck-routes.json").readText()).optInt(appId.toString()).takeIf { it > 0 }
|
||||
}.getOrNull()
|
||||
|
||||
private fun shortcutRecord(config: File, appId: Long): JSONObject? = runCatching {
|
||||
JSONObject(File(config, ".droiddeck-shortcuts.json").readText()).optJSONObject("games")?.optJSONObject(appId.toString())
|
||||
}.getOrNull()
|
||||
|
||||
internal fun steamTarget(folder: File, folderGuest: String, record: JSONObject?, guestExe: String, seen: Int): File? {
|
||||
if (record == null || record.optInt("rev", 0) == seen) return null
|
||||
val asked = unquote(record.optJSONObject("app")?.optString("Exe"))
|
||||
val steam = unquote(record.optJSONObject("steam")?.optString("Exe"))
|
||||
if (asked != guestExe || steam.isEmpty() || steam == asked || !steam.startsWith("$folderGuest/")) return null
|
||||
val file = File(folder, steam.removePrefix("$folderGuest/"))
|
||||
val inside = runCatching { file.canonicalPath.startsWith(folder.canonicalPath + File.separator) }.getOrDefault(false)
|
||||
return file.takeIf { inside && it.isFile }
|
||||
}
|
||||
|
||||
private fun unquote(value: String?): String = value.orEmpty().trim().trim('"')
|
||||
|
||||
/** The list the session hands the runtime's shortcuts writer; one file per session start. */
|
||||
fun writeListing(context: Context, games: List<Game>): File {
|
||||
val file = File(context.filesDir, "session/added-games.json").apply { parentFile?.mkdirs() }
|
||||
@@ -139,6 +188,7 @@ object AddedGames {
|
||||
json.append("{\"name\":").append(quote(g.name)).append(",\"exe\":").append(quote(g.guestExe))
|
||||
.append(",\"folder\":").append(quote(guestPath(context, g.folder) ?: g.guestDir))
|
||||
.append(",\"dir\":").append(quote(g.guestDir)).append(",\"appid\":").append(g.appId)
|
||||
.append(",\"seen\":").append(SessionPrefs.addedGameExeSeen(context, g.folder.path))
|
||||
// The art, as the session sees it: the app's cache is bound at its own path, a file in
|
||||
// the game's folder at the folder's guest path.
|
||||
val art = AddedGameArt.resolve(context, g)
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Best-effort detection of the Windows redistributables a game bundles, by scanning its install
|
||||
* folder for the redist installers Steam/Epic/GOG ship (`_CommonRedist/`, `_Redist/`,
|
||||
* `Redistributables/`, loose `vcredist_x64.exe`, …) and mapping them to entries in our component
|
||||
* catalog (WinComponents, Bannerlator's components.json `name` keys). The result drives the
|
||||
* Recommended part of a game's Windows components; the user still switches each one on.
|
||||
* Bannerlator's detector, unchanged but for this note and its package.
|
||||
*
|
||||
* Wine-aware: keeps the redists that actually matter under Wine/DXVK (VC++ runtimes, .NET, PhysX,
|
||||
* OpenAL, XACT, XNA, GFWL) and SKIPS the ones DXVK/VKD3D already provide (DX12 Agility SDK) or that
|
||||
* aren't our components (anti-cheat, Rockstar/Social-Club/launcher setups). Never throws; returns an
|
||||
* empty list on anything unexpected. Pure filesystem — no network, no Android deps (unit-testable).
|
||||
*/
|
||||
object DependencyDetector {
|
||||
|
||||
/** How a recommendation was found — drives the UI's "recommended vs optional" split. */
|
||||
enum class Kind {
|
||||
/** The game bundles the redist installer (in _CommonRedist etc.) — meant to be installed. */
|
||||
BUNDLED,
|
||||
/** A runtime DLL the game ships loose — usually already works from the game folder; install
|
||||
* only if the game misbehaves. */
|
||||
SHIPPED,
|
||||
/** Steam installs it with the game from Steamworks Shared ([SteamRedists]). */
|
||||
STEAM,
|
||||
}
|
||||
|
||||
/** A recommended component: the catalog [componentName] to install, a display [label], the on-disk
|
||||
* redist that triggered it ([reason]), and [kind] (bundled installer vs shipped DLL). */
|
||||
data class Recommendation(val componentName: String, val label: String, val reason: String, val kind: Kind = Kind.BUNDLED)
|
||||
|
||||
private const val MAX_DEPTH = 5
|
||||
private const val MAX_ENTRIES = 8000 // cost guard against giant game trees
|
||||
|
||||
/** A loose VC++ installer with no year in its path — resolved to "latest" only if no specific
|
||||
* vcredistYYYY was found anywhere (else it's just a duplicate of the versioned one). */
|
||||
private const val VCREDIST_LOOSE = "__vcredist_loose"
|
||||
|
||||
/** Directory names that are redist containers worth descending into. */
|
||||
private val REDIST_DIR_RE = Regex(
|
||||
"""(?i)^(_?commonredist|_?redist(ributables)?|__?installer|vcredist|vc_redist|directx|direct_x|physx|openal|dotnet(fx)?|\.net|xna|xact|redist|support)$"""
|
||||
)
|
||||
|
||||
/** Skip whole subtrees that are never our components. */
|
||||
private val SKIP_DIR_RE = Regex("""(?i)^(d3d12.*|.*agility.*|dxvk|vkd3d|battleye|easyanticheat|eac|anticheat)$""")
|
||||
|
||||
private val YEAR_TO_VCREDIST = mapOf(
|
||||
"2005" to "vcredist2005", "2008" to "vcredist2008", "2010" to "vcredist2010",
|
||||
"2012" to "vcredist2012", "2013" to "vcredist2013", "2015" to "vcredist2015",
|
||||
"2017" to "vcredist2015", "2019" to "vcredist2019", "2022" to "vcredist2022",
|
||||
)
|
||||
|
||||
private val LABELS = mapOf(
|
||||
"vcredist2005" to "Visual C++ 2005", "vcredist2008" to "Visual C++ 2008",
|
||||
"vcredist2010" to "Visual C++ 2010", "vcredist2012" to "Visual C++ 2012",
|
||||
"vcredist2013" to "Visual C++ 2013", "vcredist2015" to "Visual C++ 2015-2017",
|
||||
"vcredist2019" to "Visual C++ 2019", "vcredist2022" to "Visual C++ 2015-2022",
|
||||
"physx" to "NVIDIA PhysX", "oalinst" to "OpenAL", "xact" to "DirectX XACT audio",
|
||||
"xact_x64" to "DirectX XACT audio (x64)", "xna40" to "XNA Framework 4.0",
|
||||
"xna31" to "XNA Framework 3.1", "d3dx9" to "DirectX 9 (d3dx9)", "d3dx11" to "DirectX 11 (d3dx11)",
|
||||
"XLiveRedist" to "Games for Windows Live", "dotnet40" to ".NET Framework 4.0",
|
||||
"dotnet45" to ".NET Framework 4.5", "dotnet452" to ".NET Framework 4.5.2",
|
||||
"dotnet46" to ".NET Framework 4.6", "dotnet472" to ".NET Framework 4.7.2",
|
||||
"dotnet48" to ".NET Framework 4.8", "dotnet35" to ".NET Framework 3.5",
|
||||
"dotnetcoredesktop6" to ".NET Desktop 6", "dotnetcoredesktop7" to ".NET Desktop 7",
|
||||
"dotnetcoredesktop8" to ".NET Desktop 8",
|
||||
)
|
||||
|
||||
/** Preferred install order (runtimes others may depend on first). */
|
||||
private val INSTALL_ORDER = listOf(
|
||||
"vcredist2005", "vcredist2008", "vcredist2010", "vcredist2012", "vcredist2013",
|
||||
"vcredist2015", "vcredist2019", "vcredist2022",
|
||||
"dotnet35", "dotnet40", "dotnet45", "dotnet452", "dotnet46", "dotnet472", "dotnet48",
|
||||
"dotnetcoredesktop6", "dotnetcoredesktop7", "dotnetcoredesktop8",
|
||||
"d3dx9", "d3dx11", "xact", "xact_x64", "xna31", "xna40", "physx", "oalinst", "XLiveRedist",
|
||||
)
|
||||
|
||||
/**
|
||||
* Detect from the .exe the user picked, resolving the game root first: walk up from the exe's
|
||||
* folder (bounded) to the nearest ancestor that holds a redist container (`_CommonRedist`, …) at
|
||||
* its top level, and scan from there — so a nested exe (`…/Game/bin/x.exe`) still finds a root-level
|
||||
* `_CommonRedist`, without over-reaching into a shared `steamapps/common` (which has no redist
|
||||
* container of its own). Falls back to scanning the exe's own folder.
|
||||
*/
|
||||
fun detectForExe(exeFile: File): List<Recommendation> {
|
||||
val start = exeFile.absoluteFile.parentFile ?: return emptyList()
|
||||
var d: File? = start
|
||||
var depth = 0
|
||||
while (d != null && depth < 4) {
|
||||
val hasRedistContainer = try {
|
||||
d.listFiles()?.any { it.isDirectory && REDIST_DIR_RE.matches(it.name.lowercase(Locale.US)) } == true
|
||||
} catch (_: Throwable) { false }
|
||||
if (hasRedistContainer) return detect(d)
|
||||
d = d.parentFile
|
||||
depth++
|
||||
}
|
||||
return detect(start)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan [gameDir] and return the recommended components, de-duplicated (first reason wins), in a
|
||||
* stable install-friendly order. Empty when nothing is detected or [gameDir] isn't a directory.
|
||||
*/
|
||||
fun detect(gameDir: File): List<Recommendation> {
|
||||
if (!gameDir.isDirectory) return emptyList()
|
||||
val hits = LinkedHashMap<String, Recommendation>() // componentName -> first recommendation
|
||||
var visited = 0
|
||||
|
||||
fun consider(f: File, isDir: Boolean, insideRedist: Boolean) {
|
||||
val name = f.name.lowercase(Locale.US)
|
||||
val path = f.path.replace('\\', '/').lowercase(Locale.US)
|
||||
// Bundled = in a redist container or an installer (exe/msi/cab); shipped = a loose runtime DLL.
|
||||
val kind = if (insideRedist || isDir || name.endsWith(".exe") || name.endsWith(".msi") || name.endsWith(".cab"))
|
||||
Kind.BUNDLED else Kind.SHIPPED
|
||||
for (comp in classify(path, name, isDir)) {
|
||||
val existing = hits[comp]
|
||||
if (existing == null) {
|
||||
hits[comp] = Recommendation(comp, LABELS[comp] ?: comp, relativeReason(gameDir, f), kind)
|
||||
} else if (existing.kind == Kind.SHIPPED && kind == Kind.BUNDLED) {
|
||||
hits[comp] = existing.copy(kind = Kind.BUNDLED) // a bundled installer outranks a shipped DLL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun walk(dir: File, depth: Int, insideRedist: Boolean) {
|
||||
if (depth > MAX_DEPTH || visited > MAX_ENTRIES) return
|
||||
val entries = dir.listFiles() ?: return
|
||||
for (f in entries) {
|
||||
if (visited++ > MAX_ENTRIES) return
|
||||
val lower = f.name.lowercase(Locale.US)
|
||||
if (f.isDirectory) {
|
||||
if (SKIP_DIR_RE.matches(lower)) continue
|
||||
val isRedistDir = insideRedist || REDIST_DIR_RE.matches(lower)
|
||||
if (isRedistDir) consider(f, isDir = true, insideRedist = true)
|
||||
// Aggressive coverage: descend redist containers fully, and otherwise sweep the game
|
||||
// tree a few levels deep so runtime libraries the game ships (in bin/, etc.) are seen.
|
||||
if (isRedistDir || depth < 3) walk(f, depth + 1, isRedistDir)
|
||||
} else {
|
||||
// Classify installers + runtime libraries (.dll/.exe/.msi) anywhere in the scanned
|
||||
// tree; other files are ignored. Recommendations are advisory, so cast a wide net.
|
||||
val ext = lower.substringAfterLast('.', "")
|
||||
if (insideRedist || ext == "dll" || ext == "exe" || ext == "msi") consider(f, isDir = false, insideRedist = insideRedist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
walk(gameDir, 0, false)
|
||||
resolveLooseVcredist(hits)
|
||||
hits.values.sortedBy { INSTALL_ORDER.indexOf(it.componentName).let { i -> if (i < 0) 999 else i } }
|
||||
} catch (_: Throwable) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/** A loose VC++ installer resolves to the latest 14.x only if no specific vcredistYYYY was found. */
|
||||
private fun resolveLooseVcredist(hits: LinkedHashMap<String, Recommendation>) {
|
||||
val loose = hits.remove(VCREDIST_LOOSE) ?: return
|
||||
val hasSpecific = hits.keys.any { it in YEAR_TO_VCREDIST.values }
|
||||
if (!hasSpecific) hits["vcredist2022"] = loose.copy(componentName = "vcredist2022", label = LABELS["vcredist2022"]!!)
|
||||
}
|
||||
|
||||
private fun relativeReason(gameDir: File, f: File): String {
|
||||
val g = gameDir.path.replace('\\', '/')
|
||||
val p = f.path.replace('\\', '/')
|
||||
return if (p.startsWith("$g/")) p.substring(g.length + 1) else f.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a redist file/dir (its full lowercased [path], leaf [name], and whether it [isDir]) to
|
||||
* catalog component name(s). Empty for things we deliberately skip.
|
||||
*/
|
||||
private fun classify(path: String, name: String, isDir: Boolean): List<String> {
|
||||
// Hard skips first — DX12 is provided by DXVK/VKD3D; these aren't our components.
|
||||
if (path.contains("d3d12") || path.contains("agility") ||
|
||||
path.contains("battleye") || path.contains("anticheat") || path.contains("easyanticheat") ||
|
||||
name.contains("socialclub") || name.contains("social-club") ||
|
||||
name.contains("rockstar") || name == "setup.exe"
|
||||
) return emptyList()
|
||||
|
||||
// Visual C++ runtimes — most important. Year comes from the path context.
|
||||
if (path.contains("vcredist") || path.contains("vc_redist") || name.startsWith("vcredist") || name.startsWith("vc_redist")) {
|
||||
val year = YEAR_TO_VCREDIST.keys.firstOrNull { path.contains(it) }
|
||||
return when {
|
||||
year != null -> listOf(YEAR_TO_VCREDIST.getValue(year))
|
||||
isDir -> emptyList() // bare "vcredist" container — wait for the year subfolder / installer
|
||||
else -> listOf(VCREDIST_LOOSE) // loose installer, no year → resolved post-walk
|
||||
}
|
||||
}
|
||||
// VC++ runtime DLL the game ships (msvcp140.dll → 2015-2022, msvcp120.dll → 2013, …).
|
||||
VCRUNTIME_DLL.entries.firstOrNull { name.startsWith(it.key) }?.let { return listOf(it.value) }
|
||||
if (path.contains("openal") || name.startsWith("oalinst") || name == "openal32.dll" || name == "wrap_oal.dll") return listOf("oalinst")
|
||||
if (path.contains("physx") || name.startsWith("physx")) return listOf("physx")
|
||||
if (path.contains("xact") || name.startsWith("xactengine") || name.startsWith("x3daudio")) return listOf("xact", "xact_x64")
|
||||
// Games for Windows Live — the redist folder, OR a GFWL library the game ships (xlive.dll,
|
||||
// Codemasters' dbxLive32/64, gfwl*): GFWL titles need XLiveRedist to run even when they don't
|
||||
// bundle the redist itself.
|
||||
if (path.contains("xliveredist") || name.contains("xlive") || name.startsWith("gfwl") || name.startsWith("dbxlive")) {
|
||||
return listOf("XLiveRedist")
|
||||
}
|
||||
if (path.contains("xna")) return listOf(if (path.contains("3.1") || path.contains("3_1") || path.contains("v31")) "xna31" else "xna40")
|
||||
// .NET — Framework and Core/Desktop runtimes.
|
||||
if (path.contains("dotnet") || path.contains(".net") || name.startsWith("ndp") || name.startsWith("dotnetfx") || name.contains("windowsdesktop-runtime")) {
|
||||
val comp = dotnetComponent("$path $name")
|
||||
return when {
|
||||
comp != null -> listOf(comp)
|
||||
isDir -> emptyList() // bare ".NET" container — wait for the versioned installer
|
||||
else -> listOf("dotnet48") // unlabeled .NET Framework installer file → sensible default
|
||||
}
|
||||
}
|
||||
// DirectX 11 helper DLLs the game ships (d3dx11_43.dll).
|
||||
if (D3DX11_RE.matches(name)) return listOf("d3dx11")
|
||||
// DirectX 9 helper DLLs / d3dcompiler / June-2010 DX redist (DXVK provides core d3d9 but NOT
|
||||
// these helper DLLs).
|
||||
if (name.startsWith("dxsetup") || name.startsWith("dxwebsetup") ||
|
||||
D3DX9_RE.matches(name) || D3DCOMPILER_RE.matches(name) ||
|
||||
(path.contains("directx") && name.endsWith(".exe"))
|
||||
) return listOf("d3dx9")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
/** Runtime DLL prefix → the VC++ redistributable that provides it (checked with startsWith). */
|
||||
private val VCRUNTIME_DLL = linkedMapOf(
|
||||
"vcruntime140" to "vcredist2022", "msvcp140" to "vcredist2022", "msvcr140" to "vcredist2022", "concrt140" to "vcredist2022",
|
||||
"msvcp120" to "vcredist2013", "msvcr120" to "vcredist2013",
|
||||
"msvcp110" to "vcredist2012", "msvcr110" to "vcredist2012",
|
||||
"msvcp100" to "vcredist2010", "msvcr100" to "vcredist2010",
|
||||
"msvcp90" to "vcredist2008", "msvcr90" to "vcredist2008",
|
||||
"msvcp80" to "vcredist2005", "msvcr80" to "vcredist2005",
|
||||
)
|
||||
private val D3DX9_RE = Regex("""d3dx9_\d+\.dll""")
|
||||
private val D3DX11_RE = Regex("""d3dx11_\d+\.dll""")
|
||||
private val D3DCOMPILER_RE = Regex("""d3dcompiler_\d+\.dll""")
|
||||
|
||||
/** Resolve a .NET component from installer/path text, or null when no version is discernible. */
|
||||
private fun dotnetComponent(s: String): String? = when {
|
||||
s.contains("desktop-runtime-8") || Regex("""\b8\.\d""").containsMatchIn(s) -> "dotnetcoredesktop8"
|
||||
s.contains("desktop-runtime-7") || Regex("""\b7\.\d""").containsMatchIn(s) -> "dotnetcoredesktop7"
|
||||
s.contains("desktop-runtime-6") || Regex("""\b6\.\d""").containsMatchIn(s) -> "dotnetcoredesktop6"
|
||||
s.contains("ndp48") || s.contains("fx48") || s.contains("4.8") -> "dotnet48"
|
||||
s.contains("ndp472") || s.contains("4.7.2") -> "dotnet472"
|
||||
s.contains("ndp46") || s.contains("4.6") -> "dotnet46"
|
||||
s.contains("ndp452") || s.contains("4.5.2") -> "dotnet452"
|
||||
s.contains("dotnetfx45") || s.contains("ndp45") || s.contains("4.5") -> "dotnet45"
|
||||
s.contains("dotnetfx40") || s.contains("ndp40") || s.contains("4.0") -> "dotnet40"
|
||||
s.contains("dotnetfx35") || s.contains("3.5") -> "dotnet35"
|
||||
else -> null // no version signal (e.g. a bare "DotNet" container dir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
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.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)) { context.getString(R.string.game_sync_err_runtime) }
|
||||
// A disconnected library must not be interpreted as a mass uninstall.
|
||||
check(File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps").isDirectory) { context.getString(R.string.game_sync_err_steam_library) }
|
||||
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) { context.getString(R.string.game_sync_err_previous_sd) }
|
||||
}
|
||||
storage?.let { check(File(it.path).listFiles() != null) { context.getString(R.string.game_sync_err_game_storage) } }
|
||||
SessionPrefs.addedGamesDirs(context).forEach { check(File(it).listFiles() != null) { context.getString(R.string.game_sync_err_added_games) } }
|
||||
GameFiles.sync(context, folder, Library.launchableGames(context, strictRead = true))
|
||||
prefs(context).edit().putString("storageSetting", storageSetting).putString("storagePath", storage?.path).apply()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AtomicFile
|
||||
import com.droiddeck.launcher.R
|
||||
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 = "steam"
|
||||
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 text.takeIf(GameLaunchLink::validId)
|
||||
}
|
||||
|
||||
fun filename(game: Library.SteamGame, disambiguate: Boolean = false): 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)
|
||||
val suffix = if (disambiguate) " (${game.gameIdString})" else ""
|
||||
return "$title$suffix.$EXTENSION"
|
||||
}
|
||||
|
||||
private fun contents(id: String): String {
|
||||
require(GameLaunchLink.validId(id))
|
||||
return id
|
||||
}
|
||||
|
||||
@Synchronized fun export(context: Context, folder: File, game: Library.SteamGame): File =
|
||||
exportNamed(context, folder, game, filename(game))
|
||||
|
||||
private fun exportNamed(context: Context, folder: File, game: Library.SteamGame, name: String): File {
|
||||
require(folder.isDirectory) { context.getString(R.string.gfiles_folder_unavailable) }
|
||||
val text = contents(game.gameIdString)
|
||||
val target = exportTarget(context, folder, game, name)
|
||||
require(!target.exists() || matches(target, text)) { context.getString(R.string.gfiles_name_taken, target.name) }
|
||||
if (!target.exists()) write(target, text)
|
||||
exportArt(context, folder, game)
|
||||
return target
|
||||
}
|
||||
|
||||
private fun exportTarget(context: Context, folder: File, game: Library.SteamGame, name: String): File {
|
||||
val target = child(context, folder, name)
|
||||
return if (target.exists() && !matches(target, contents(game.gameIdString))) {
|
||||
child(context, folder, filename(game, disambiguate = true))
|
||||
} else target
|
||||
}
|
||||
|
||||
/** Cocoon matches icons in its data folder's images directory. Never replace custom art. */
|
||||
private fun exportArt(context: Context, 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(".")
|
||||
.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(context, folder, "images")
|
||||
check(images.isDirectory || images.mkdir()) { context.getString(R.string.gfiles_art_unavailable) }
|
||||
val target = child(context, 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(context: Context, folder: File, games: List<Library.SteamGame>) {
|
||||
require(folder.isDirectory) { context.getString(R.string.gfiles_folder_unavailable) }
|
||||
val inventory = child(context, folder, INVENTORY)
|
||||
val previous = if (inventory.exists()) {
|
||||
val record = JSONObject(AtomicFile(inventory).readText())
|
||||
require(record.optString("format") == FORMAT) { context.getString(R.string.gfiles_unrelated_inventory) }
|
||||
record.getJSONObject("games")
|
||||
} else JSONObject()
|
||||
val next = JSONObject()
|
||||
val unique = games.distinctBy { it.gameId }
|
||||
val names = unique.groupingBy { filename(it).lowercase(java.util.Locale.ROOT) }.eachCount()
|
||||
val exports = unique.map { game ->
|
||||
val name = filename(game, disambiguate = names.getValue(filename(game).lowercase(java.util.Locale.ROOT)) > 1)
|
||||
val preferred = child(context, folder, name)
|
||||
val target = if (inventoryOwns(context, folder, previous, preferred, game.gameIdString)) preferred
|
||||
else exportTarget(context, folder, game, name)
|
||||
val text = contents(game.gameIdString)
|
||||
val existed = target.exists()
|
||||
// Keep an edited export without letting it block the rest of the library sync.
|
||||
val owned = inventoryOwns(context, folder, previous, target, game.gameIdString)
|
||||
val file = if (owned && target.exists() &&
|
||||
!matches(target, text)) target else exportNamed(context, folder, game, target.name)
|
||||
if (!existed || owned) 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(context, 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()) { context.getString(R.string.gfiles_remove_failed, name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (previous.toString() != next.toString()) write(inventory, JSONObject().put("format", FORMAT).put("games", next).toString())
|
||||
}
|
||||
|
||||
private fun inventoryOwns(context: Context, folder: File, record: JSONObject, file: File, id: String): Boolean {
|
||||
if (record.optString(file.name) == id) return true
|
||||
if (!file.exists()) return false
|
||||
return record.keys().asSequence().any { name ->
|
||||
record.optString(name) == id && child(context, folder, name).let { old ->
|
||||
old.exists() && java.nio.file.Files.isSameFile(old.toPath(), file.toPath())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun child(context: Context, 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) { context.getString(R.string.gfiles_outside_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,32 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
object GameLaunchIntent {
|
||||
const val ACTION = "com.droiddeck.launcher.LAUNCH_GAME"
|
||||
const val EXTRA_APP_ID = "app_id"
|
||||
const val EXTRA_GAME_SOURCE = "game_source"
|
||||
private const val GAMENATIVE_ACTION = "app.gamenative.LAUNCH_GAME"
|
||||
|
||||
fun accepts(action: String?): Boolean = action == Intent.ACTION_VIEW || action == ACTION || action == GAMENATIVE_ACTION
|
||||
|
||||
fun read(context: Context, intent: Intent): String? = when (intent.action) {
|
||||
Intent.ACTION_VIEW -> GameFiles.readIntent(context, intent)
|
||||
ACTION, GAMENATIVE_ACTION -> readId(intent)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
internal fun readId(intent: Intent): String? = runCatching {
|
||||
if (intent.action != ACTION && intent.action != GAMENATIVE_ACTION) return null
|
||||
val source = intent.extras?.get(EXTRA_GAME_SOURCE)
|
||||
if (source != null && (source !is String || !source.equals("STEAM", ignoreCase = true))) return null
|
||||
val id = when (val value = intent.extras?.get(EXTRA_APP_ID)) {
|
||||
is Int -> value.takeIf { it > 0 }?.toString()
|
||||
is String -> value.takeIf(GameLaunchLink::validId)
|
||||
else -> null
|
||||
}
|
||||
id
|
||||
}.getOrNull()
|
||||
}
|
||||
@@ -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,12 +23,18 @@ 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 shown name of [system] when it is words rather than a platform name, else 0. */
|
||||
val systemRes: Int get() = if (id == "retroarch") R.string.content_system_many else 0
|
||||
/** The emulator's own icon, bundled (the runtime keeps them as theme SVGs the app cannot draw). */
|
||||
val iconRes: Int get() = when (id) {
|
||||
"rpcs3" -> R.drawable.emu_rpcs3; "armsx2" -> R.drawable.emu_pcsx2; "dolphin" -> R.drawable.emu_dolphin
|
||||
@@ -67,6 +74,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)
|
||||
@@ -78,31 +86,40 @@ object Library {
|
||||
return listOfNotNull(
|
||||
root to "internal",
|
||||
GameStorage.effective(context)?.let { File(it.path) to it.label },
|
||||
)
|
||||
) + GameStorage.gamesFolderLibraries(context).map { it.host to it.label }
|
||||
}
|
||||
|
||||
/** Proton keeps each game's prefix below compatdata/<appid>/pfx in a Steam library. */
|
||||
fun protonPrefix(context: Context, appId: Long, preferredLibrary: File? = null): File? {
|
||||
fun protonPrefix(
|
||||
context: Context, appId: Long, preferredLibrary: File? = null,
|
||||
libraries: List<File> = steamLibraries(context).map { it.first },
|
||||
): File? {
|
||||
val ids = listOf(appId.toString(), java.lang.Integer.toString(appId.toInt())).distinct()
|
||||
val roots = (listOfNotNull(preferredLibrary) + steamLibraries(context).map { it.first })
|
||||
val secondary = libraries.drop(1).mapNotNull { runCatching { it.canonicalPath }.getOrNull() }.toSet()
|
||||
fun prefixRoot(root: File): File = if (runCatching { root.canonicalPath }.getOrNull() in secondary)
|
||||
SecondaryLibrary.privateRoot(context.filesDir, root).takeIf { File(it, "steamapps/compatdata").isDirectory } ?: root
|
||||
else root
|
||||
val roots = (listOfNotNull(preferredLibrary) + libraries).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 roots = libraries.map { it.first }
|
||||
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) { context.getString(R.string.game_sync_err_manifests) }
|
||||
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.
|
||||
@@ -119,13 +136,30 @@ object Library {
|
||||
out[appId] = SteamGame(
|
||||
appId, name, art, label, hero = hero, lastPlayed = lastPlayed,
|
||||
gameFiles = gameFiles,
|
||||
protonPrefix = protonPrefix(context, appId.toLong(), library),
|
||||
protonPrefix = protonPrefix(context, appId.toLong(), library, roots),
|
||||
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> {
|
||||
val roots = steamLibraries(context).map { it.first }
|
||||
return (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, libraries = roots),
|
||||
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())
|
||||
@@ -167,18 +201,19 @@ object Library {
|
||||
|
||||
/** The emulator's name for a program path from the rail ("ARMSX2"), or null. */
|
||||
fun nameForProgram(program: String?): String? =
|
||||
if (program == com.droiddeck.launcher.runtime.FlatpakManager.LAUNCHER || program == com.droiddeck.launcher.runtime.AppImageManager.LAUNCHER) {
|
||||
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
|
||||
|
||||
/** Flatpak apps' and AppImages' names by id or directory, as launched: the session only knows that. */
|
||||
/** 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 }
|
||||
|
||||
@@ -315,7 +350,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
|
||||
@@ -327,10 +362,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,66 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Which catalog components a game's prefix already has from elsewhere - a real installer, the game,
|
||||
* the user - so the list can say so instead of recommending them again. Bannerlator's detector,
|
||||
* on DroidDeck's compatdata/<id>: only markers absent from a fresh Proton prefix count (OpenAL32,
|
||||
* Mono, PhysX, Games for Windows Live); msvcp140, d3dx9 and the like ship with Proton and cannot
|
||||
* tell. Files DroidDeck put there itself (droiddeck-wincomponents' state) are not counted.
|
||||
* Pure filesystem reads; never throws.
|
||||
*/
|
||||
object PrefixInstalledDetector {
|
||||
private const val STATE = ".droiddeck-wincomponents.json"
|
||||
|
||||
/** Detector names (oalinst, mono, physx, XLiveRedist) present in [compat]/pfx. */
|
||||
fun detect(compat: File?): Set<String> = try {
|
||||
val pfx = compat?.let { File(it, "pfx") }
|
||||
if (pfx == null || !pfx.isDirectory) emptySet() else {
|
||||
val ours = ours(compat)
|
||||
val out = HashSet<String>()
|
||||
val openal = listOf("system32", "syswow64").map { "drive_c/windows/$it/openal32.dll" }
|
||||
if (openal.any { it !in ours && fileIgnoringCase(pfx, it) }) out += "oalinst"
|
||||
if (File(pfx, "drive_c/windows/mono").isDirectory) out += "mono"
|
||||
if (hasPhysX(pfx)) out += "physx"
|
||||
if (registryContains(pfx, "Games for Windows")) out += "XLiveRedist"
|
||||
out
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
private fun ours(compat: File): Set<String> = try {
|
||||
val files = JSONObject(File(compat, STATE).readText()).optJSONObject("files")
|
||||
files?.keys()?.asSequence()?.map { it.lowercase() }?.toSet().orEmpty()
|
||||
} catch (_: Exception) {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
/** Windows names ignore case; the prefix's folder may spell them either way. */
|
||||
private fun fileIgnoringCase(pfx: File, rel: String): Boolean {
|
||||
val target = File(pfx, rel)
|
||||
return target.parentFile?.listFiles()?.any { it.isFile && it.name.equals(target.name, ignoreCase = true) } == true
|
||||
}
|
||||
|
||||
private fun hasPhysX(pfx: File): Boolean {
|
||||
for (pf in arrayOf("drive_c/Program Files", "drive_c/Program Files (x86)")) {
|
||||
val base = File(pfx, pf)
|
||||
if (File(base, "NVIDIA Corporation/PhysX").isDirectory) return true
|
||||
if (base.listFiles()?.any { it.isDirectory && it.name.contains("PhysX", ignoreCase = true) } == true) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun registryContains(pfx: File, needle: String): Boolean =
|
||||
arrayOf("system.reg", "user.reg").any { name ->
|
||||
val f = File(pfx, name)
|
||||
try {
|
||||
f.isFile && f.length() < 32L * 1024 * 1024 &&
|
||||
f.bufferedReader().use { r -> r.lineSequence().any { it.contains(needle, ignoreCase = true) } }
|
||||
} catch (_: Throwable) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* What Steam itself installs alongside a game: the Steamworks Shared redistributables (app 228980)
|
||||
* listed under "SharedDepots" in the game's appmanifest. Those live in Steamworks Shared rather than
|
||||
* the game's folder, so the folder scan ([DependencyDetector]) cannot see them; this reads Steam's
|
||||
* list instead. Depots are matched to Bannerlator's catalog names; unknown ones are left out.
|
||||
*/
|
||||
object SteamRedists {
|
||||
/** Steamworks Shared depot -> (catalog component, what Steam calls it). 2015-2019 runtimes are one family. */
|
||||
val DEPOTS = linkedMapOf(
|
||||
"228981" to ("vcredist2005_dll" to "VC++ 2005"),
|
||||
"228982" to ("vcredist2008_dll" to "VC++ 2008"),
|
||||
"228983" to ("vcredist2010_dll" to "VC++ 2010"),
|
||||
"228984" to ("vcredist2012_dll" to "VC++ 2012"),
|
||||
"228985" to ("vcredist2013_dll" to "VC++ 2013"),
|
||||
"228986" to ("vcredist2015_dll" to "VC++ 2015"),
|
||||
"228987" to ("vcredist2019_dll" to "VC++ 2017"),
|
||||
"228988" to ("vcredist2019_dll" to "VC++ 2019"),
|
||||
"228989" to ("vcredist2022_dll" to "VC++ 2022"),
|
||||
"228990" to ("d3dx9" to "DirectX (June 2010)"),
|
||||
)
|
||||
private val BLOCK = Regex("\"SharedDepots\"\\s*\\{([^}]*)\\}", RegexOption.IGNORE_CASE)
|
||||
private val ENTRY = Regex("\"(\\d+)\"\\s+\"\\d+\"")
|
||||
|
||||
/** The game's appmanifest beside its install folder (steamapps/common/<dir> -> steamapps/). */
|
||||
fun manifest(gameDir: File?, appId: Int): File? {
|
||||
val steamapps = gameDir?.parentFile?.parentFile ?: return null
|
||||
return File(steamapps, "appmanifest_$appId.acf").takeIf { it.isFile }
|
||||
}
|
||||
|
||||
fun detect(gameDir: File?, appId: Int): List<DependencyDetector.Recommendation> {
|
||||
val text = try {
|
||||
manifest(gameDir, appId)?.takeIf { it.length() < 1L shl 20 }?.readText() ?: return emptyList()
|
||||
} catch (_: Exception) {
|
||||
return emptyList()
|
||||
}
|
||||
val block = BLOCK.find(text)?.groupValues?.get(1) ?: return emptyList()
|
||||
return ENTRY.findAll(block).mapNotNull { DEPOTS[it.groupValues[1]] }
|
||||
.distinctBy { it.first }
|
||||
.map { (component, label) -> DependencyDetector.Recommendation(component, label, label, DependencyDetector.Kind.STEAM) }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Everything the device info window shows about the phone's chip: SoC, GPU, CPU, memory, system,
|
||||
* the phone's own Vulkan driver and the drivers DroidDeck uses. Reads files and asks Vulkan once
|
||||
* ([VulkanInfo]), so [collect] belongs off the main thread. Values the phone does not give are
|
||||
* left out rather than guessed.
|
||||
*/
|
||||
object DeviceInfo {
|
||||
class Section(val title: String, val rows: List<Pair<String, String>>)
|
||||
|
||||
fun collect(context: Context): List<Section> {
|
||||
val vk = VulkanInfo.query()
|
||||
// After Vulkan has been asked, detection can use its name too.
|
||||
val gpu = GpuInfo.detect()
|
||||
return listOf(
|
||||
chip(), gpu(gpu), cpu(), memory(context), system(), vulkan(context, vk), drivers(context, gpu),
|
||||
).filter { it.rows.isNotEmpty() }
|
||||
}
|
||||
|
||||
/** The whole window as plain text, for a bug report. */
|
||||
fun text(sections: List<Section>): String = buildString {
|
||||
append("DroidDeck device info\n")
|
||||
for (s in sections) {
|
||||
append('\n').append(s.title).append('\n')
|
||||
for ((k, v) in s.rows) append(" ").append(k).append(": ").append(v).append('\n')
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<Pair<String, String>>.add(key: String, value: String?) {
|
||||
if (!value.isNullOrBlank() && value != Build.UNKNOWN) add(key to value.trim())
|
||||
}
|
||||
|
||||
private fun read(path: String): String? = FileUtils.readString(File(path))?.trim()?.ifEmpty { null }
|
||||
|
||||
private fun prop(name: String): String = GpuInfo.systemProperty(name)
|
||||
|
||||
private fun chip() = Section("Chip (SoC)", buildList<Pair<String, String>> {
|
||||
val model = SocNames.model()
|
||||
val platform = prop("ro.board.platform")
|
||||
add("Name", SocNames.name(model, platform))
|
||||
add("Model", model)
|
||||
if (Build.VERSION.SDK_INT >= 31) add("Maker", Build.SOC_MANUFACTURER)
|
||||
add("Platform", platform)
|
||||
add("Hardware", Build.HARDWARE)
|
||||
add("Board", Build.BOARD)
|
||||
val id = read("/sys/devices/soc0/soc_id")
|
||||
val machine = read("/sys/devices/soc0/machine")
|
||||
add("SoC id", listOfNotNull(id, machine).joinToString(" · "))
|
||||
add("Family", read("/sys/devices/soc0/family"))
|
||||
})
|
||||
|
||||
private fun gpu(gpu: GpuInfo) = Section("GPU", buildList<Pair<String, String>> {
|
||||
add("GPU", gpu.name)
|
||||
add("Family", gpu.family.label)
|
||||
add("Model found from", when (gpu.modelSource) {
|
||||
"kernel" -> "the kernel's GPU name"
|
||||
"vulkan" -> "the Vulkan driver (the kernel's name has no model)"
|
||||
"platform" -> "the platform (the kernel's name has no model)"
|
||||
else -> "unknown"
|
||||
})
|
||||
add("Kernel name", gpu.kgslName)
|
||||
val kgsl = "/sys/class/kgsl/kgsl-3d0"
|
||||
add("Clock range", gpuRange())
|
||||
add("Load", read("$kgsl/gpu_busy_percentage")?.let { if (it.endsWith("%")) it else "$it %" })
|
||||
add("Temperature", read("$kgsl/temp")?.toLongOrNull()?.let { if (it > 1000) "${it / 1000} °C" else "$it °C" })
|
||||
add("Support", gpu.supportText)
|
||||
})
|
||||
|
||||
private fun cpu() = Section("CPU", buildList<Pair<String, String>> {
|
||||
add("Cores", Runtime.getRuntime().availableProcessors().toString())
|
||||
// Each cpufreq policy is one cluster: the cores it covers and their speed range.
|
||||
val policies = File("/sys/devices/system/cpu/cpufreq").listFiles { f -> f.name.matches(Regex("policy\\d+")) }
|
||||
?.sortedBy { it.name.removePrefix("policy").toInt() }.orEmpty()
|
||||
for (p in policies) {
|
||||
val cpus = (read("${p.path}/related_cpus") ?: read("${p.path}/affected_cpus"))?.split(Regex("\\s+"))?.filter { it.isNotEmpty() }.orEmpty()
|
||||
val min = read("${p.path}/cpuinfo_min_freq")?.toLongOrNull()
|
||||
val max = read("${p.path}/cpuinfo_max_freq")?.toLongOrNull() ?: continue
|
||||
val cores = if (cpus.size > 1) "cores ${cpus.first()}-${cpus.last()}" else "core ${cpus.firstOrNull() ?: p.name.removePrefix("policy")}"
|
||||
add("Cluster ${p.name.removePrefix("policy")}", "${cpus.size.coerceAtLeast(1)} × $cores · " +
|
||||
(if (min != null) ghz(min) + "–" else "") + ghz(max))
|
||||
}
|
||||
add("ABI", Build.SUPPORTED_ABIS.joinToString(", "))
|
||||
})
|
||||
|
||||
private fun ghz(khz: Long) = String.format(Locale.US, "%.2f GHz", khz / 1e6)
|
||||
|
||||
/** The GPU's slowest and fastest step: KGSL's list, else a devfreq GPU node (Mali and others). */
|
||||
private fun gpuRange(): String? {
|
||||
fun mhz(hz: Long) = "${hz / 1_000_000} MHz"
|
||||
val kgsl = read("/sys/class/kgsl/kgsl-3d0/gpu_available_frequencies")
|
||||
?.split(Regex("\\s+"))?.mapNotNull { it.toLongOrNull() }?.filter { it > 0 }.orEmpty()
|
||||
if (kgsl.isNotEmpty()) return "${mhz(kgsl.min())}–${mhz(kgsl.max())} (${kgsl.size} steps)"
|
||||
read("/sys/class/kgsl/kgsl-3d0/max_gpuclk")?.toLongOrNull()?.let { return "up to ${mhz(it)}" }
|
||||
val node = File("/sys/class/devfreq").listFiles()?.firstOrNull { f ->
|
||||
val n = f.name.lowercase()
|
||||
(n.contains("gpu") || n.contains("mali") || n.contains("kgsl-3d")) && listOf("bus", "bw", "memlat").none { n.contains(it) }
|
||||
} ?: return null
|
||||
val steps = read("${node.path}/available_frequencies")?.split(Regex("\\s+"))?.mapNotNull { it.toLongOrNull() }?.filter { it > 0 }.orEmpty()
|
||||
if (steps.isNotEmpty()) return "${mhz(steps.min())}–${mhz(steps.max())} (${steps.size} steps)"
|
||||
val min = read("${node.path}/min_freq")?.toLongOrNull()
|
||||
val max = read("${node.path}/max_freq")?.toLongOrNull() ?: return null
|
||||
return (if (min != null) "${mhz(min)}–" else "up to ") + mhz(max)
|
||||
}
|
||||
|
||||
private fun memory(context: Context) = Section("Memory", buildList<Pair<String, String>> {
|
||||
val am = context.getSystemService(ActivityManager::class.java) ?: return@buildList
|
||||
val mi = ActivityManager.MemoryInfo().also(am::getMemoryInfo)
|
||||
add("Total", FileUtils.sizeToString(mi.totalMem))
|
||||
add("Available now", FileUtils.sizeToString(mi.availMem))
|
||||
})
|
||||
|
||||
private fun system() = Section("System", buildList<Pair<String, String>> {
|
||||
add("Device", "${Build.MANUFACTURER} ${Build.MODEL}")
|
||||
add("Android", "${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})")
|
||||
add("Security patch", Build.VERSION.SECURITY_PATCH)
|
||||
add("Kernel", System.getProperty("os.version"))
|
||||
add("Build", Build.DISPLAY)
|
||||
})
|
||||
|
||||
private fun vulkan(context: Context, vk: Map<String, String>) = Section("Vulkan (the phone's own driver)", buildList<Pair<String, String>> {
|
||||
vk["error"]?.let { add("Error", it) }
|
||||
add("Device", vk["device"])
|
||||
add("Type", vk["type"])
|
||||
add("API version", vk["api"])
|
||||
add("Loader version", vk["loader"])
|
||||
add("Driver", listOfNotNull(vk["driver_name"], vk["driver_version"]).joinToString(" "))
|
||||
// Qualcomm's build string starts with its own label ("Driver Build: 43c70540de, ...").
|
||||
add("Driver build", vk["driver_info"]?.replace(Regex("(?i)^driver build:\\s*"), ""))
|
||||
add("Conformance", vk["conformance"])
|
||||
add("Vendor / device id", listOfNotNull(vk["vendor_id"], vk["device_id"]).joinToString(" / "))
|
||||
add("Max texture size", vk["max_image_2d"])
|
||||
add("GPU memory heap", vk["device_heap_mb"]?.let { "$it MB" })
|
||||
add("Extensions", vk["extension_count"])
|
||||
val features = listOf("geometry_shader" to "Geometry shaders", "tessellation_shader" to "Tessellation", "texture_bc" to "BC textures")
|
||||
for ((key, label) in features) add(label, vk[key])
|
||||
for ((key, value) in vk) if (key.startsWith("ext.")) add(key.removePrefix("ext."), value)
|
||||
// What Android certifies the phone for.
|
||||
val pm = context.packageManager
|
||||
val hw = pm.systemAvailableFeatures.associate { it.name to it.version }
|
||||
hw[PackageManager.FEATURE_VULKAN_HARDWARE_VERSION]?.let {
|
||||
add("Android-certified version", "${it shr 22}.${(it shr 12) and 0x3ff}.${it and 0xfff}")
|
||||
}
|
||||
hw[PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL]?.let { add("Android hardware level", it.toString()) }
|
||||
})
|
||||
|
||||
private fun drivers(context: Context, gpu: GpuInfo) = Section("Drivers DroidDeck uses", buildList<Pair<String, String>> {
|
||||
add("Driver mode", SessionPrefs.gpuDriverMode(context))
|
||||
val turnip = TurnipDriver(context)
|
||||
val choice = SessionPrefs.androidDriver(context).ifEmpty { turnip.autoId() }
|
||||
add("Display driver", "${turnip.displayName(choice)} ${turnip.driverVersion(choice)}".trim())
|
||||
val linux = LinuxVulkanDriverManager(context)
|
||||
val id = SessionPrefs.linuxDriver(context)
|
||||
add("Game driver", if (id.isEmpty()) "the runtime's own Turnip" else "${linux.getDriverName(id)} ${linux.getDriverVersion(id)}".trim())
|
||||
if (gpu.oneUi8Gen2) add("Note", "One UI on an 8 Gen 2: needs the OneUI Turnip build")
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.droiddeck.launcher.gpu
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import org.json.JSONObject
|
||||
@@ -84,7 +85,7 @@ object DriverBundle {
|
||||
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")
|
||||
?: throw IllegalArgumentException(context.getString(R.string.bundle_no_manifest))
|
||||
val androidDir = File(work, manifest.androidPath)
|
||||
val linuxDir = File(work, manifest.linuxPath)
|
||||
|
||||
@@ -135,7 +136,7 @@ object DriverBundle {
|
||||
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")
|
||||
if (!out.path.startsWith(root.path + File.separator)) throw IllegalArgumentException(context.getString(R.string.bundle_entry_outside, entry.name))
|
||||
out.parentFile?.mkdirs()
|
||||
out.outputStream().use { sink ->
|
||||
val buf = ByteArray(1 shl 16)
|
||||
@@ -143,7 +144,7 @@ object DriverBundle {
|
||||
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")
|
||||
if (total > MAX_BYTES) throw IllegalArgumentException(context.getString(R.string.bundle_too_large, MAX_BYTES shr 20))
|
||||
sink.write(buf, 0, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.droiddeck.launcher.R
|
||||
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.
|
||||
* support level is what the app has actually been tested on (Adreno 650, 725 and newer) - 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. */
|
||||
@@ -16,10 +18,14 @@ data class GpuInfo(
|
||||
/** 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. */
|
||||
/** The SoC as a person would say it ("Snapdragon 8 Gen 2 (QCS8550)"), or "" when the phone does not say. */
|
||||
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,
|
||||
/** KGSL's own name for the GPU, as the kernel spells it ("Adreno740v2", "Adreno33v2"). */
|
||||
val kgslName: String = "",
|
||||
/** Where [model] came from: "kernel", "vulkan" or "platform", "" when unknown. */
|
||||
val modelSource: String = "",
|
||||
) {
|
||||
enum class Family(val label: String) {
|
||||
A8XX("Adreno 8xx"),
|
||||
@@ -29,7 +35,10 @@ data class GpuInfo(
|
||||
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"),
|
||||
NOT_ADRENO("Not an Adreno GPU");
|
||||
|
||||
/** [label] in the app's language: the Adreno families are names, the rest is words. */
|
||||
fun label(context: Context): String = if (this == NOT_ADRENO) context.getString(R.string.gpuinfo_not_adreno) else label
|
||||
}
|
||||
|
||||
enum class Support { TESTED, UNTESTED, UNSUPPORTED }
|
||||
@@ -37,36 +46,82 @@ data class GpuInfo(
|
||||
val support: Support
|
||||
get() = when {
|
||||
family == Family.NOT_ADRENO -> Support.UNSUPPORTED
|
||||
family == Family.A6XX && model == 650 -> Support.TESTED
|
||||
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. */
|
||||
/** One line for the device card and the system check, in English for the device report. */
|
||||
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"
|
||||
else "Outside tested hardware (Adreno 650, 725 and newer): it may not run"
|
||||
Support.UNSUPPORTED -> "Not supported: DroidDeck needs an Adreno (Snapdragon) GPU"
|
||||
}
|
||||
|
||||
/** [supportText] in the app's language. */
|
||||
fun supportText(context: Context): String = context.getString(when (support) {
|
||||
Support.TESTED -> R.string.gpuinfo_supported
|
||||
Support.UNTESTED -> if (family == Family.A7XX_LOW) R.string.gpuinfo_experimental else R.string.gpuinfo_below_tested
|
||||
Support.UNSUPPORTED -> R.string.gpuinfo_unsupported
|
||||
})
|
||||
|
||||
/** [name] for the screen: the stand-in for a GPU the device does not name is in the app's language. */
|
||||
fun displayName(context: Context): String = if (name == UNNAMED) context.getString(R.string.gpu_this_gpu) else name
|
||||
|
||||
companion object {
|
||||
/** [name] when the device names no GPU; English, as the device report shows it. */
|
||||
private const val UNNAMED = "this GPU"
|
||||
|
||||
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
|
||||
// Where vendors put the chip's model, named when it is known: "Snapdragon 8 Gen 2 (QCS8550)".
|
||||
val soc = if (adreno) SocNames.label() else ""
|
||||
val fromKernel = raw?.let { threeDigits(it) } ?: 0
|
||||
// Some kernels name the GPU without its model (AYANEO's Pocket FIT: "Adreno33v2"). The
|
||||
// Vulkan driver's own name, when this process has asked it, then the platform's code
|
||||
// name stand in - the GPU is the same on every phone of a platform.
|
||||
val fromVulkan = if (fromKernel > 0 || !adreno) 0 else VulkanInfo.cachedOrNull()?.get("device")?.let { threeDigits(it) } ?: 0
|
||||
val fromPlatform = if (fromKernel > 0 || fromVulkan > 0 || !adreno) 0 else platformModel()
|
||||
val model = maxOf(fromKernel, fromVulkan, fromPlatform)
|
||||
val source = when {
|
||||
fromKernel > 0 -> "kernel"
|
||||
fromVulkan > 0 -> "vulkan"
|
||||
fromPlatform > 0 -> "platform"
|
||||
else -> ""
|
||||
}
|
||||
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",
|
||||
name = if (!adreno) Build.HARDWARE.ifBlank { UNNAMED } else if (model > 0) "Adreno $model" else "Adreno",
|
||||
model = model, family = family, soc = soc,
|
||||
oneUi8Gen2 = samsung && model == 740,
|
||||
kgslName = raw.orEmpty(), modelSource = source,
|
||||
)
|
||||
}
|
||||
|
||||
private fun threeDigits(text: String): Int = Regex("""(\d{3})""").find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||
|
||||
/** Qualcomm platform code names whose GPU is known for certain. */
|
||||
private val PLATFORMS = mapOf(
|
||||
"msmnile" to 640, "kona" to 650, "lahaina" to 660, "taro" to 730, "cape" to 730,
|
||||
"kalama" to 740, "pineapple" to 750, "sun" to 830,
|
||||
)
|
||||
|
||||
internal fun platformModel(platform: String = systemProperty("ro.board.platform")): Int =
|
||||
PLATFORMS[platform.lowercase()] ?: PLATFORMS[systemProperty("ro.vendor.qti.soc_name").lowercase()] ?: 0
|
||||
|
||||
/** android.os.SystemProperties.get, which apps may read but not call directly. */
|
||||
fun systemProperty(name: String): String = try {
|
||||
Class.forName("android.os.SystemProperties").getMethod("get", String::class.java).invoke(null, name) as? String ?: ""
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
|
||||
internal fun familyOf(adreno: Boolean, model: Int): Family = when {
|
||||
!adreno -> Family.NOT_ADRENO
|
||||
model == 0 -> Family.ADRENO_UNKNOWN
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.droiddeck.launcher.R;
|
||||
import com.droiddeck.launcher.core.FileUtils;
|
||||
|
||||
import org.json.JSONObject;
|
||||
@@ -181,20 +182,17 @@ public class LinuxVulkanDriverManager {
|
||||
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.");
|
||||
throw new IllegalArgumentException(context.getString(R.string.lvd_no_library));
|
||||
}
|
||||
File so = new File(tmpDir, LIB_NAME);
|
||||
if (!isAarch64Elf(so)) {
|
||||
throw new IllegalArgumentException(soName + " is not a 64-bit AArch64 ELF shared library.");
|
||||
throw new IllegalArgumentException(context.getString(R.string.lvd_not_aarch64, soName));
|
||||
}
|
||||
// The one check that actually separates this from the bionic builds. Both sonames
|
||||
// are in the driver's .dynstr, so a byte scan is enough and needs no ELF parsing:
|
||||
// glibc's is versioned ("libc.so.6"), bionic's is not ("libc.so").
|
||||
if (!containsAscii(so, "libc.so.6")) {
|
||||
throw new IllegalArgumentException(soName + " is not a glibc driver - it links Android's libc. "
|
||||
+ "The Linux runtime needs a \"-Linux\" zip; a plain or \"-Wayland\" Turnip cannot be "
|
||||
+ "loaded by the Steam client at all.");
|
||||
throw new IllegalArgumentException(context.getString(R.string.lvd_not_glibc, soName));
|
||||
}
|
||||
String kind = zipMeta != null ? zipMeta.optString("kind", "") : "";
|
||||
if (!kind.isEmpty() && !"linux-vulkan-icd".equals(kind)) {
|
||||
|
||||
@@ -6,7 +6,7 @@ package com.droiddeck.launcher.gpu;
|
||||
* DXBC chain translated to SPIR-V. {@link Lossless} decides when; these calls block.
|
||||
*/
|
||||
final class LsfgNative {
|
||||
static { System.loadLibrary("bannerwayland"); }
|
||||
static { System.loadLibrary("droiddeckwayland"); }
|
||||
|
||||
private LsfgNative() {}
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
import org.json.JSONObject
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* The compositor's post chain (waylandcomp/src/effects_chain.c) as the session menu drives it:
|
||||
* the colour grade, the sharpening, the shader toggles and terminal debanding. Units are the
|
||||
* menu's own, so a value goes straight from a row into the setter it drives:
|
||||
*
|
||||
* - [brightness] / [contrast] -100..100, 0 = neutral
|
||||
* - [gamma] 0.5..3.0, 1.0 = neutral
|
||||
* - [saturation] 0..200 percent, 100 = neutral
|
||||
* - [casLevel] 0..100; the pass runs only while [cas] is on
|
||||
* - [debandStrength] 0..200, 100 = one LSB
|
||||
*
|
||||
* Applies live: the compositor picks the change up on its next frame. While any pass is on, a
|
||||
* zero-copy game goes through the compositor pass instead, which costs a little GPU time.
|
||||
*/
|
||||
data class ScreenEffects(
|
||||
val brightness: Int = 0,
|
||||
val contrast: Int = 0,
|
||||
val gamma: Float = 1f,
|
||||
val saturation: Int = 100,
|
||||
val cas: Boolean = false,
|
||||
val casLevel: Int = 50,
|
||||
val hdr: Boolean = false,
|
||||
val deband: Boolean = false,
|
||||
val debandStrength: Int = 100,
|
||||
val fxaa: Boolean = false,
|
||||
val toon: Boolean = false,
|
||||
val crt: Boolean = false,
|
||||
val ntsc: Boolean = false,
|
||||
) {
|
||||
/** The sharpening level the picture gets: 0 while the pass is off, whatever its level is parked at. */
|
||||
val effectiveCas: Int get() = if (cas) casLevel else 0
|
||||
|
||||
/** Whether any pass runs, so the menu can say when the picture is untouched. */
|
||||
val any: Boolean get() = brightness != 0 || contrast != 0 || abs(gamma - 1f) > 0.005f || saturation != 100 ||
|
||||
effectiveCas > 0 || hdr || deband || fxaa || toon || crt || ntsc
|
||||
|
||||
/** Hands every pass to the compositor, with the Look's name for its log line. */
|
||||
fun push(upscaler: Int) {
|
||||
WaylandCompositor.nativeSetCas(cas, casLevel)
|
||||
WaylandCompositor.nativeSetHdr(hdr)
|
||||
WaylandCompositor.nativeSetDeband(deband, debandStrength)
|
||||
WaylandCompositor.nativeSetScreenEffects(brightness.toFloat(), contrast.toFloat(), gamma, saturation.toFloat(), fxaa, toon, crt, ntsc)
|
||||
WaylandCompositor.nativeSetLookName(ScreenEffectLooks.match(this, upscaler)?.name)
|
||||
}
|
||||
|
||||
fun encode(): JSONObject = JSONObject()
|
||||
.put("brightness", brightness).put("contrast", contrast).put("gamma", gamma.toDouble()).put("saturation", saturation)
|
||||
.put("cas", cas).put("casLevel", casLevel).put("hdr", hdr).put("deband", deband).put("debandStrength", debandStrength)
|
||||
.put("fxaa", fxaa).put("toon", toon).put("crt", crt).put("ntsc", ntsc)
|
||||
|
||||
companion object {
|
||||
val OFF = ScreenEffects()
|
||||
|
||||
fun decode(json: JSONObject): ScreenEffects = ScreenEffects(
|
||||
brightness = json.optInt("brightness", 0).coerceIn(-100, 100),
|
||||
contrast = json.optInt("contrast", 0).coerceIn(-100, 100),
|
||||
gamma = json.optDouble("gamma", 1.0).toFloat().coerceIn(0.5f, 3f),
|
||||
saturation = json.optInt("saturation", 100).coerceIn(0, 200),
|
||||
cas = json.optBoolean("cas", false),
|
||||
casLevel = json.optInt("casLevel", 50).coerceIn(0, 100),
|
||||
hdr = json.optBoolean("hdr", false),
|
||||
deband = json.optBoolean("deband", false),
|
||||
debandStrength = json.optInt("debandStrength", 100).coerceIn(0, 200),
|
||||
fxaa = json.optBoolean("fxaa", false),
|
||||
toon = json.optBoolean("toon", false),
|
||||
crt = json.optBoolean("crt", false),
|
||||
ntsc = json.optBoolean("ntsc", false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One Look: a named set of values for the rows that already exist on the Display page. Picking
|
||||
* a Look moves those rows; each stays adjustable afterwards, and the moment one moves the Look
|
||||
* row reads Custom. [scalingMode] null leaves the scaling mode alone; otherwise it is a
|
||||
* SessionPrefs.upscalerChoices mode the Look switches to.
|
||||
*/
|
||||
data class Look(
|
||||
/** The Look's id: what the row picks by and the compositor logs. */
|
||||
val name: String,
|
||||
val effects: ScreenEffects,
|
||||
val scalingMode: Int? = null,
|
||||
/** One line under the row while this Look is picked. */
|
||||
val desc: String,
|
||||
/** [name] and [desc] in the app's language, for the menu. */
|
||||
@StringRes val label: Int,
|
||||
@StringRes val descRes: Int,
|
||||
)
|
||||
|
||||
object ScreenEffectLooks {
|
||||
// Scaling modes as the compositor numbers them (WaylandCompositor.nativeSetUpscaler).
|
||||
private const val SCALING_NEAREST = 2
|
||||
private const val SCALING_FSR = 4
|
||||
|
||||
/** Index 0 is the neutral state a fresh session matches, so the row always names something. */
|
||||
val LOOKS: List<Look> = listOf(
|
||||
Look("Off", ScreenEffects.OFF, desc = "Nothing applied.", label = R.string.frame_gen_off, descRes = R.string.look_off_desc),
|
||||
Look("Game Clarity", ScreenEffects(brightness = 2, contrast = 12, saturation = 108, cas = true, casLevel = 55),
|
||||
desc = "Sharpening with a touch of contrast - the everyday one.", label = R.string.look_game_clarity, descRes = R.string.look_game_clarity_desc),
|
||||
Look("Vivid", ScreenEffects(brightness = 3, contrast = 10, gamma = 0.98f, saturation = 145, cas = true, casLevel = 30, deband = true),
|
||||
desc = "Colour pushed hard. Deband on so skies don't stripe.", label = R.string.look_vivid, descRes = R.string.look_vivid_desc),
|
||||
Look("Cinematic", ScreenEffects(brightness = -4, contrast = 22, gamma = 1.10f, saturation = 88, cas = true, casLevel = 20, fxaa = true, deband = true),
|
||||
desc = "Deeper contrast, cooler colour, edges smoothed.", label = R.string.look_cinematic, descRes = R.string.look_cinematic_desc),
|
||||
Look("Competitive", ScreenEffects(brightness = 16, contrast = 18, gamma = 0.92f, saturation = 82, cas = true, casLevel = 75),
|
||||
desc = "Shadows lifted and heavy sharpening - spot people first.", label = R.string.look_competitive, descRes = R.string.look_competitive_desc),
|
||||
Look("Adaptive Sharpen", ScreenEffects(cas = true, casLevel = 85),
|
||||
desc = "Sharpening only, colour untouched.", label = R.string.look_adaptive_sharpen, descRes = R.string.look_adaptive_sharpen_desc),
|
||||
Look("Filmic", ScreenEffects(brightness = -2, contrast = 16, gamma = 1.14f, saturation = 92, cas = true, casLevel = 15, fxaa = true, deband = true),
|
||||
desc = "Film-like curve, slightly muted.", label = R.string.look_filmic, descRes = R.string.look_filmic_desc),
|
||||
Look("Arcade", ScreenEffects(brightness = 6, contrast = 20, gamma = 0.95f, saturation = 170, cas = true, casLevel = 45, deband = true),
|
||||
desc = "Loud and punchy.", label = R.string.look_arcade, descRes = R.string.look_arcade_desc),
|
||||
Look("Retro CRT", ScreenEffects(brightness = 6, contrast = 14, gamma = 1.05f, saturation = 115, crt = true, ntsc = true),
|
||||
desc = "The CRT tube and analogue signal passes together.", label = R.string.look_retro_crt, descRes = R.string.look_retro_crt_desc),
|
||||
Look("Upscale Sharp", ScreenEffects(contrast = 6, saturation = 104, cas = true, casLevel = 70), scalingMode = SCALING_FSR,
|
||||
desc = "For running below panel resolution.", label = R.string.look_upscale_sharp, descRes = R.string.look_upscale_sharp_desc),
|
||||
Look("Pixel Clean", ScreenEffects(contrast = 8, deband = true), scalingMode = SCALING_NEAREST,
|
||||
desc = "Crisp pixels for 2D and old titles.", label = R.string.look_pixel_clean, descRes = R.string.look_pixel_clean_desc),
|
||||
Look("Anime Edge", ScreenEffects(brightness = 2, contrast = 10, saturation = 125, cas = true, casLevel = 50, toon = true, deband = true),
|
||||
desc = "Toon outlines plus sharpening.", label = R.string.look_anime_edge, descRes = R.string.look_anime_edge_desc),
|
||||
)
|
||||
|
||||
/**
|
||||
* The Look the live values are, or null for Custom. A Look's sharpening is compared as the
|
||||
* effective level, so a pass switched off matches a Look without one whatever its level is
|
||||
* parked at; a Look with no scaling mode of its own matches under any.
|
||||
*/
|
||||
fun match(e: ScreenEffects, upscaler: Int): Look? = LOOKS.firstOrNull { l ->
|
||||
val t = l.effects
|
||||
t.brightness == e.brightness && t.contrast == e.contrast && abs(t.gamma - e.gamma) < 0.005f &&
|
||||
t.saturation == e.saturation && t.effectiveCas == e.effectiveCas &&
|
||||
t.fxaa == e.fxaa && t.crt == e.crt && t.toon == e.toon && t.ntsc == e.ntsc && t.deband == e.deband &&
|
||||
(l.scalingMode == null || l.scalingMode == upscaler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.os.Build
|
||||
|
||||
/**
|
||||
* Which chip this is, as a person would say it. Handhelds often leave Android's SoC model blank
|
||||
* (the AYANEO Pocket FIT does), so the model is looked for where vendors put it, the same order
|
||||
* ClusterTune uses; then it is named from a table of models we are sure of, or from the platform
|
||||
* code name as a family ("Snapdragon 8 Gen 3 family") when the model itself is a vendor variant.
|
||||
*/
|
||||
object SocNames {
|
||||
/** Model codes whose product name is certain. */
|
||||
private val MODELS = mapOf(
|
||||
"SM8150" to "Snapdragon 855", "SM8250" to "Snapdragon 865", "SM8250-AC" to "Snapdragon 870",
|
||||
"SM8350" to "Snapdragon 888", "SM8450" to "Snapdragon 8 Gen 1", "SM8475" to "Snapdragon 8+ Gen 1",
|
||||
"SM8550" to "Snapdragon 8 Gen 2", "QCS8550" to "Snapdragon 8 Gen 2", "SM8650" to "Snapdragon 8 Gen 3",
|
||||
"SM8750" to "Snapdragon 8 Elite", "SM8850" to "Snapdragon 8 Elite Gen 5",
|
||||
"SM7325" to "Snapdragon 778G", "SM7450" to "Snapdragon 7 Gen 1", "SM7550" to "Snapdragon 7 Gen 3",
|
||||
"SM6375" to "Snapdragon 695",
|
||||
)
|
||||
|
||||
/** Platform code names: every chip of one platform shares its CPU and GPU design. */
|
||||
private val PLATFORMS = mapOf(
|
||||
"msmnile" to "Snapdragon 855/860", "kona" to "Snapdragon 865/870", "lahaina" to "Snapdragon 888",
|
||||
"taro" to "Snapdragon 8 Gen 1", "cape" to "Snapdragon 8+ Gen 1", "kalama" to "Snapdragon 8 Gen 2",
|
||||
"pineapple" to "Snapdragon 8 Gen 3", "sun" to "Snapdragon 8 Elite",
|
||||
)
|
||||
|
||||
/** The chip's model code ("QCS8550", "SG8350P"), or "" when the phone does not say. */
|
||||
fun model(prop: (String) -> String = GpuInfo::systemProperty): String {
|
||||
val sdk = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it != Build.UNKNOWN }.orEmpty() else ""
|
||||
return listOf(prop("ro.soc.model"), prop("ro.vendor.qti.soc_model"), sdk, prop("ro.fota.platform"))
|
||||
.map { it.trim() }
|
||||
// ro.fota.platform is often a branch name ("MSM_13.0"), not a chip.
|
||||
.firstOrNull { it.isNotEmpty() && !Regex("(?i)^msm_\\d").containsMatchIn(it) }.orEmpty()
|
||||
}
|
||||
|
||||
/** "Snapdragon 8 Gen 2", "Snapdragon 8 Gen 3 family", or null when neither the model nor the platform is known. */
|
||||
fun name(model: String, platform: String = GpuInfo.systemProperty("ro.board.platform")): String? =
|
||||
MODELS[model.uppercase()] ?: PLATFORMS[platform.lowercase()]?.let { "$it family" }
|
||||
|
||||
/** For the GPU card: "Snapdragon 8 Gen 2 (QCS8550)", the bare model, or "". */
|
||||
fun label(model: String = model(), platform: String = GpuInfo.systemProperty("ro.board.platform")): String {
|
||||
val name = name(model, platform)
|
||||
return when {
|
||||
name != null && model.isNotEmpty() -> "$name ($model)"
|
||||
name != null -> name
|
||||
else -> model
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import com.droiddeck.launcher.R;
|
||||
import com.droiddeck.launcher.core.FileUtils;
|
||||
import com.droiddeck.launcher.core.TarZst;
|
||||
import com.droiddeck.launcher.session.SessionPrefs;
|
||||
@@ -218,7 +219,7 @@ public final class TurnipDriver {
|
||||
*/
|
||||
String adopt(File tmpDir, String displayName) throws IOException {
|
||||
String rejected = rejectionReason(tmpDir);
|
||||
if (rejected != null) throw new IllegalArgumentException("Not an AdrenoTools driver: " + rejected);
|
||||
if (rejected != null) throw new IllegalArgumentException(context.getString(R.string.turnip_not_adrenotools, rejected));
|
||||
|
||||
JSONObject meta = readMetaFile(new File(tmpDir, "meta.json"));
|
||||
String name = meta != null ? meta.optString("name", "") : "";
|
||||
@@ -245,31 +246,31 @@ public final class TurnipDriver {
|
||||
* Why a just-extracted zip is not something adrenotools can be pointed at, or null when it is.
|
||||
* The kinds refused by name are the other driver lists' zips, which carry a meta.json too.
|
||||
*/
|
||||
private static String rejectionReason(File dir) {
|
||||
private String rejectionReason(File dir) {
|
||||
File metaFile = new File(dir, "meta.json");
|
||||
if (!metaFile.isFile()) return "no meta.json";
|
||||
if (!metaFile.isFile()) return context.getString(R.string.turnip_reject_no_meta);
|
||||
String libraryName, kind;
|
||||
try {
|
||||
JSONObject meta = new JSONObject(FileUtils.readString(metaFile));
|
||||
libraryName = meta.optString("libraryName", "");
|
||||
kind = meta.optString("kind", "");
|
||||
} catch (Exception e) {
|
||||
return "meta.json is unreadable (" + e.getMessage() + ")";
|
||||
return context.getString(R.string.turnip_reject_meta_unreadable, e.getMessage());
|
||||
}
|
||||
if ("linux-vulkan-icd".equals(kind))
|
||||
return "this is a Linux runtime driver (import it under \"Linux runtime driver\")";
|
||||
return context.getString(R.string.turnip_reject_linux_driver);
|
||||
if ("wayland-game-driver".equals(kind))
|
||||
return "this is a Wayland game driver, which this app has no use for";
|
||||
return context.getString(R.string.turnip_reject_wayland);
|
||||
if (libraryName.isEmpty())
|
||||
return "meta.json names no libraryName, so nothing could be handed to AdrenoTools";
|
||||
return context.getString(R.string.turnip_reject_no_library_name);
|
||||
File library = new File(dir, libraryName);
|
||||
if (!library.isFile())
|
||||
return "meta.json names " + libraryName + ", which is not in the zip";
|
||||
return context.getString(R.string.turnip_reject_library_missing, libraryName);
|
||||
if (!LinuxVulkanDriverManager.isAarch64Elf(library))
|
||||
return libraryName + " is not a 64-bit AArch64 ELF shared library";
|
||||
return context.getString(R.string.turnip_reject_not_aarch64, libraryName);
|
||||
// A glibc build that happens to carry a libraryName would load nothing in this process.
|
||||
if (LinuxVulkanDriverManager.containsAscii(library, "libc.so.6"))
|
||||
return libraryName + " links glibc - a \"-Linux\" Turnip, which belongs under \"Linux runtime driver\"";
|
||||
return context.getString(R.string.turnip_reject_glibc, libraryName);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import com.droiddeck.launcher.core.Hashes
|
||||
import com.droiddeck.launcher.R
|
||||
import android.content.Context
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import org.json.JSONArray
|
||||
@@ -95,7 +96,7 @@ object TurnipReleases {
|
||||
var lastError: Exception? = null
|
||||
for (src in SOURCES) {
|
||||
try {
|
||||
val releases = JSONArray(get("https://api.github.com/repos/${src.repo}/releases?per_page=15"))
|
||||
val releases = JSONArray(get(context, "https://api.github.com/repos/${src.repo}/releases?per_page=15"))
|
||||
val seen = HashSet<String>()
|
||||
var newest: String? = null
|
||||
for (i in 0 until releases.length()) {
|
||||
@@ -135,7 +136,7 @@ object TurnipReleases {
|
||||
return parse(stored)
|
||||
}
|
||||
|
||||
private fun get(url: String): String {
|
||||
private fun get(context: Context, url: String): String {
|
||||
val c = URL(url).openConnection() as HttpURLConnection
|
||||
c.connectTimeout = 15_000
|
||||
c.readTimeout = 20_000
|
||||
@@ -143,8 +144,8 @@ object TurnipReleases {
|
||||
c.setRequestProperty("User-Agent", "DroidDeck-app")
|
||||
try {
|
||||
val code = c.responseCode
|
||||
if (code == 403 || code == 429) throw IOException("GitHub's rate limit was hit - try again later")
|
||||
if (code != 200) throw IOException("GitHub answered HTTP $code")
|
||||
if (code == 403 || code == 429) throw IOException(context.getString(R.string.turnip_rate_limited))
|
||||
if (code != 200) throw IOException(context.getString(R.string.turnip_github_http, code))
|
||||
return c.inputStream.bufferedReader().use { it.readText() }
|
||||
} finally {
|
||||
c.disconnect()
|
||||
@@ -194,7 +195,7 @@ object TurnipReleases {
|
||||
|
||||
/** Download an asset into the cache; the caller imports it and deletes the file. */
|
||||
fun download(context: Context, asset: Asset, progress: (Int) -> Unit): File {
|
||||
if (asset.sha256.isEmpty()) throw IOException("This driver list predates checksums - refresh it and try again")
|
||||
if (asset.sha256.isEmpty()) throw IOException(context.getString(R.string.turnip_list_predates_checksums))
|
||||
val target = File(context.cacheDir, asset.name)
|
||||
val c = URL(asset.url).openConnection() as HttpURLConnection
|
||||
c.connectTimeout = 15_000
|
||||
@@ -202,7 +203,7 @@ object TurnipReleases {
|
||||
c.setRequestProperty("User-Agent", "DroidDeck-app")
|
||||
c.instanceFollowRedirects = true
|
||||
try {
|
||||
if (c.responseCode != 200) throw IOException("download answered HTTP ${c.responseCode}")
|
||||
if (c.responseCode != 200) throw IOException(context.getString(R.string.turnip_download_http, c.responseCode))
|
||||
val total = c.contentLengthLong.takeIf { it > 0 } ?: asset.size
|
||||
c.inputStream.use { input ->
|
||||
FileOutputStream(target).use { out ->
|
||||
@@ -220,7 +221,7 @@ object TurnipReleases {
|
||||
}
|
||||
}
|
||||
if (!asset.sha256.equals(Hashes.sha256(target), ignoreCase = true)) {
|
||||
throw IOException("Checksum mismatch - the download was discarded")
|
||||
throw IOException(context.getString(R.string.turnip_checksum_mismatch))
|
||||
}
|
||||
return target
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* The phone's own Vulkan driver, as it describes itself (native/deviceinfo): device name, API and
|
||||
* driver versions, a few limits and the extensions DXVK/VKD3D lean on. Asked once per process and
|
||||
* kept; the first [query] creates a Vulkan instance, so it belongs off the main thread.
|
||||
*/
|
||||
object VulkanInfo {
|
||||
@Volatile private var cached: Map<String, String>? = null
|
||||
|
||||
@JvmStatic private external fun nativeQuery(): String
|
||||
|
||||
/** Blocking on first use. Empty-ish map with an "error" key when Vulkan cannot be asked. */
|
||||
@Synchronized
|
||||
fun query(): Map<String, String> {
|
||||
cached?.let { return it }
|
||||
val text = try {
|
||||
System.loadLibrary("deviceinfo")
|
||||
nativeQuery()
|
||||
} catch (t: Throwable) {
|
||||
Log.w("VulkanInfo", "query failed", t)
|
||||
"error=" + (t.message ?: t.javaClass.simpleName)
|
||||
}
|
||||
val map = text.lineSequence().mapNotNull { line ->
|
||||
val eq = line.indexOf('=')
|
||||
if (eq <= 0) null else line.substring(0, eq) to line.substring(eq + 1)
|
||||
}.toMap(LinkedHashMap())
|
||||
cached = map
|
||||
return map
|
||||
}
|
||||
|
||||
/** What [query] found, if it has run in this process; never blocks. */
|
||||
fun cachedOrNull(): Map<String, String>? = cached
|
||||
}
|
||||
@@ -24,11 +24,18 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.ui.DroidDeckTheme
|
||||
import com.droiddeck.launcher.ui.onSignal
|
||||
import com.droiddeck.launcher.ui.LocalPalette
|
||||
|
||||
class ControllerEditorActivity : ComponentActivity() {
|
||||
override fun attachBaseContext(newBase: android.content.Context) {
|
||||
super.attachBaseContext(newBase)
|
||||
com.droiddeck.launcher.core.AppLanguage.applyTo(this, newBase)
|
||||
}
|
||||
|
||||
private lateinit var controls: OnScreenControls
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -48,10 +55,10 @@ class ControllerEditorActivity : ComponentActivity() {
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.9f), RoundedCornerShape(50))
|
||||
.padding(start = 14.dp, end = 5.dp, top = 5.dp, bottom = 5.dp),
|
||||
) {
|
||||
Text("Drag to move", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 4.dp))
|
||||
EditorButton("Reset", false) { controls.resetLayout() }
|
||||
EditorButton("Cancel", false) { finish() }
|
||||
EditorButton("Save", true) {
|
||||
Text(stringResource(R.string.ctrl_editor_drag), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 4.dp))
|
||||
EditorButton(stringResource(R.string.ctrl_reset), false) { controls.resetLayout() }
|
||||
EditorButton(stringResource(R.string.common_cancel), false) { finish() }
|
||||
EditorButton(stringResource(R.string.ctrl_editor_save), true) {
|
||||
controls.saveLayout()
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
import android.content.Context
|
||||
import com.droiddeck.launcher.R
|
||||
|
||||
object ControllerPrefs {
|
||||
const val STEAM_BLUE = 0xFF1A9FFF.toInt()
|
||||
const val OFF = "off"
|
||||
|
||||
val tints = listOf(
|
||||
STEAM_BLUE to "Steam blue",
|
||||
0xFF66C0F4.toInt() to "Sky",
|
||||
0xFFE8EEF4.toInt() to "White",
|
||||
0xFFC77DFF.toInt() to "Violet",
|
||||
0xFF3DDC84.toInt() to "Green",
|
||||
0xFFFFA726.toInt() to "Amber",
|
||||
0xFFFF5252.toInt() to "Red",
|
||||
0xFFFF6FB5.toInt() to "Pink",
|
||||
STEAM_BLUE to R.string.ctrl_tint_steam_blue,
|
||||
0xFF66C0F4.toInt() to R.string.ctrl_tint_sky,
|
||||
0xFFE8EEF4.toInt() to R.string.ctrl_tint_white,
|
||||
0xFFC77DFF.toInt() to R.string.ctrl_tint_violet,
|
||||
0xFF3DDC84.toInt() to R.string.ctrl_tint_green,
|
||||
0xFFFFA726.toInt() to R.string.ctrl_tint_amber,
|
||||
0xFFFF5252.toInt() to R.string.ctrl_tint_red,
|
||||
0xFFFF6FB5.toInt() to R.string.ctrl_tint_pink,
|
||||
)
|
||||
|
||||
val opacities = listOf(40, 60, 80, 100)
|
||||
@@ -22,17 +23,17 @@ object ControllerPrefs {
|
||||
val sizes = listOf(80, 90, 100, 110, 125)
|
||||
|
||||
val mappable = listOf(
|
||||
"a" to "A button", "b" to "B button", "x" to "X button", "y" to "Y button",
|
||||
"lb" to "Left bumper", "rb" to "Right bumper", "lt" to "Left trigger", "rt" to "Right trigger",
|
||||
"select" to "View button", "start" to "Menu button",
|
||||
"a" to R.string.ctrl_button_a, "b" to R.string.ctrl_button_b, "x" to R.string.ctrl_button_x, "y" to R.string.ctrl_button_y,
|
||||
"lb" to R.string.ctrl_button_lb, "rb" to R.string.ctrl_button_rb, "lt" to R.string.ctrl_button_lt, "rt" to R.string.ctrl_button_rt,
|
||||
"select" to R.string.ctrl_button_view, "start" to R.string.ctrl_button_menu,
|
||||
)
|
||||
|
||||
val targets = listOf(
|
||||
"a" to "A", "b" to "B", "x" to "X", "y" to "Y",
|
||||
"lb" to "LB", "rb" to "RB", "lt" to "LT", "rt" to "RT",
|
||||
"l3" to "L3", "r3" to "R3", "select" to "View", "start" to "Menu", "guide" to "Steam",
|
||||
"up" to "D-pad up", "down" to "D-pad down", "left" to "D-pad left", "right" to "D-pad right",
|
||||
OFF to "Hidden",
|
||||
"a" to R.string.ctrl_target_a, "b" to R.string.ctrl_target_b, "x" to R.string.ctrl_target_x, "y" to R.string.ctrl_target_y,
|
||||
"lb" to R.string.ctrl_target_lb, "rb" to R.string.ctrl_target_rb, "lt" to R.string.ctrl_target_lt, "rt" to R.string.ctrl_target_rt,
|
||||
"l3" to R.string.ctrl_target_l3, "r3" to R.string.ctrl_target_r3, "select" to R.string.ctrl_target_view, "start" to R.string.ctrl_target_menu, "guide" to R.string.ctrl_target_steam,
|
||||
"up" to R.string.ctrl_target_up, "down" to R.string.ctrl_target_down, "left" to R.string.ctrl_target_left, "right" to R.string.ctrl_target_right,
|
||||
OFF to R.string.ctrl_target_hidden,
|
||||
)
|
||||
|
||||
class Settings(
|
||||
@@ -41,11 +42,15 @@ object ControllerPrefs {
|
||||
val size: Int,
|
||||
val stickClick: Boolean,
|
||||
val adaptiveSticks: Boolean,
|
||||
val rumble: Boolean,
|
||||
val steamButton: Boolean,
|
||||
val qamButton: Boolean,
|
||||
val keyboardButton: Boolean,
|
||||
val customLayout: Boolean,
|
||||
val mapping: Map<String, String>,
|
||||
)
|
||||
|
||||
private fun prefs(context: Context) = context.getSharedPreferences("controller", Context.MODE_PRIVATE)
|
||||
internal fun prefs(context: Context) = context.getSharedPreferences("controller", Context.MODE_PRIVATE)
|
||||
|
||||
fun read(context: Context): Settings {
|
||||
val p = prefs(context)
|
||||
@@ -55,6 +60,10 @@ object ControllerPrefs {
|
||||
size = p.getInt("size", 100).takeIf { it in sizes } ?: 100,
|
||||
stickClick = p.getBoolean("stickClick", true),
|
||||
adaptiveSticks = p.getBoolean("adaptiveSticks", true),
|
||||
rumble = rumbleEnabled(context),
|
||||
steamButton = p.getBoolean("steamButton", true),
|
||||
qamButton = p.getBoolean("qamButton", true),
|
||||
keyboardButton = p.getBoolean("keyboardButton", true),
|
||||
customLayout = p.all.keys.any { it.startsWith("layout.") },
|
||||
mapping = mappable.associate { (id, _) -> id to target(context, id) },
|
||||
)
|
||||
@@ -92,6 +101,24 @@ object ControllerPrefs {
|
||||
prefs(context).edit().putBoolean("adaptiveSticks", on).apply()
|
||||
}
|
||||
|
||||
fun rumbleEnabled(context: Context): Boolean = prefs(context).getBoolean("rumble", true)
|
||||
|
||||
fun setRumble(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("rumble", on).apply()
|
||||
}
|
||||
|
||||
fun setSteamButton(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("steamButton", on).apply()
|
||||
}
|
||||
|
||||
fun setQamButton(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("qamButton", on).apply()
|
||||
}
|
||||
|
||||
fun setKeyboardButton(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("keyboardButton", on).apply()
|
||||
}
|
||||
|
||||
fun layout(context: Context, width: Int, height: Int): Map<String, Pair<Float, Float>> {
|
||||
val raw = prefs(context).getString("layout.${width}x$height", null) ?: return emptyMap()
|
||||
return raw.split(';').mapNotNull { entry ->
|
||||
@@ -111,7 +138,8 @@ object ControllerPrefs {
|
||||
}
|
||||
|
||||
fun resetAll(context: Context) {
|
||||
prefs(context).edit().clear().apply()
|
||||
// On older Android versions clear() does not notify listeners; restore rumble explicitly.
|
||||
prefs(context).edit().clear().putBoolean("rumble", true).apply()
|
||||
}
|
||||
|
||||
fun resetAllLayouts(context: Context) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.droiddeck.launcher.input;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Where the fake input rings live and how the guest is told about them. Kept apart from
|
||||
* {@link FakeInputWriter}, which loads native code, so it can be tested on the JVM.
|
||||
*/
|
||||
final class FakeInputRingPaths {
|
||||
static final String RING_DIR_NAME = "fakeinput-rings";
|
||||
|
||||
private FakeInputRingPaths() {}
|
||||
|
||||
/** The rings sit beside the fake evdev nodes: session/dev/input -> session/dev/fakeinput-rings. */
|
||||
static File ringDir(File fakeInputDir) {
|
||||
if (fakeInputDir == null) {
|
||||
return null;
|
||||
}
|
||||
File inputDir = fakeInputDir.getAbsoluteFile();
|
||||
File parent = inputDir.getParentFile();
|
||||
return new File(parent != null ? parent : inputDir, RING_DIR_NAME);
|
||||
}
|
||||
|
||||
static File ringFile(File fakeInputDir, int slot) {
|
||||
File ringDir = ringDir(fakeInputDir);
|
||||
return ringDir != null ? new File(ringDir, "ring" + slot) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ring's path as the guest opens it. The session tree is bound into the guest at its own
|
||||
* host path, spelled the way {@code Context.getFilesDir()} spells it (LinuxRuntime.binds), so
|
||||
* the export has to use that same spelling. A canonical path is not that: where
|
||||
* /data/user/0 is a link, or a separate mount, onto /data/data (ColorOS 16, #166) it resolves
|
||||
* to a prefix nothing is bound at, and every pad, physical and on-screen, goes dead.
|
||||
*/
|
||||
static String exportPath(File ringFile) {
|
||||
return ringFile.getAbsolutePath();
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,6 @@ public class FakeInputWriter {
|
||||
private static final int RING_SNAPSHOT_SEQ_OFFSET = 32;
|
||||
private static final int RING_SNAPSHOT_BUTTONS_OFFSET = 40;
|
||||
private static final int RING_SNAPSHOT_AXES_OFFSET = 44; // short[8]
|
||||
private static final String RING_DIR_NAME = "fakeinput-rings";
|
||||
// After the events - MUST match the block in fakeinput_steam.cpp (DeckImu): what a Deck has and
|
||||
// an Xbox pad does not - motion, back grips, trackpads - in the Steam Deck controller's axes and
|
||||
// units, under a seqlock like the snapshot's. Only libfakeinput's Deck controller reads it.
|
||||
@@ -198,26 +197,8 @@ public class FakeInputWriter {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static File getRingDir(File fakeInputDir) {
|
||||
if (fakeInputDir == null) {
|
||||
return null;
|
||||
}
|
||||
File inputDir = fakeInputDir.getAbsoluteFile();
|
||||
File parent = inputDir.getParentFile();
|
||||
return new File(parent != null ? parent : inputDir, RING_DIR_NAME);
|
||||
}
|
||||
|
||||
private static File getRingFile(File fakeInputDir, int slot) {
|
||||
File ringDir = getRingDir(fakeInputDir);
|
||||
return ringDir != null ? new File(ringDir, "ring" + slot) : null;
|
||||
}
|
||||
|
||||
private static String getCanonicalOrAbsolutePath(File file) {
|
||||
try {
|
||||
return file.getCanonicalPath();
|
||||
} catch (IOException e) {
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
return FakeInputRingPaths.ringFile(fakeInputDir, slot);
|
||||
}
|
||||
|
||||
private static void initializeRingHeader(ByteBuffer data) {
|
||||
@@ -325,7 +306,7 @@ public class FakeInputWriter {
|
||||
ringSlot.ringFile = ringFile.getAbsoluteFile();
|
||||
ringSlot.ringRaf = raf;
|
||||
ringSlot.ringChannel = channel;
|
||||
ringSlot.exportPath = getCanonicalOrAbsolutePath(ringFile);
|
||||
ringSlot.exportPath = FakeInputRingPaths.exportPath(ringFile);
|
||||
Log.i(TAG, "Created fake input file ring for slot " + slot + ": " + ringSlot.exportPath);
|
||||
return ringSlot;
|
||||
} catch (IOException e) {
|
||||
|
||||
Loaded 100 of 400 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user