mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-07 10:46:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11218f0b24 | ||
|
|
d9bf6d8a17 | ||
|
|
041c3b1b55 | ||
|
|
e5434117f2 | ||
|
|
b474cba79e | ||
|
|
ffb76240fd | ||
|
|
875f8e3ce2 | ||
|
|
5446497301 | ||
|
|
98a670fb81 | ||
|
|
a123af7c13 | ||
|
|
6026d72435 | ||
|
|
d5f3d1a0d5 | ||
|
|
4cb3a89483 | ||
|
|
22a675a664 | ||
|
|
ca6c409436 | ||
|
|
7c429f3a26 | ||
|
|
aba55031c3 | ||
|
|
18c7d56409 | ||
|
|
0b4bc827e7 | ||
|
|
a552f1e293 | ||
|
|
066834d17d | ||
|
|
aff9738748 | ||
|
|
f33d5b9e09 | ||
|
|
281c2aea1b | ||
|
|
c2d7765619 | ||
|
|
250f80fbb5 | ||
|
|
08526f8283 | ||
|
|
f44826ae3a | ||
|
|
31b8adbb55 | ||
|
|
542bbf89c3 | ||
|
|
e1987ca785 | ||
|
|
a81da1e476 | ||
|
|
116728917f | ||
|
|
5f180d909b | ||
|
|
88acdbc3ab | ||
|
|
42b2810822 | ||
|
|
1001bc00af | ||
|
|
69c6c14f3f | ||
|
|
6060f2288f | ||
|
|
915f373d82 | ||
|
|
7b7722bd25 | ||
|
|
63004567fd | ||
|
|
aa3757eff1 | ||
|
|
a1bc8467fd | ||
|
|
d6566f0dbe | ||
|
|
ba5876bf6d | ||
|
|
bf693d7f71 | ||
|
|
4cbd7722f8 | ||
|
+1 |
3733c50902 | ||
|
|
f4e5032b7d | ||
|
|
c8a76e7cce | ||
|
|
6a681bcbc2 | ||
|
|
cd452e2b60 | ||
|
|
5971b91ed4 | ||
|
|
906e60a042 | ||
|
|
9d2580d502 | ||
|
|
6a5722c6d1 | ||
|
|
b875b39905 | ||
|
|
ecedf131e0 | ||
|
|
8ac5652131 | ||
|
|
5624b483fb | ||
|
|
d64205ba5b | ||
|
|
df76f66e8f | ||
|
|
da851dae22 | ||
|
|
7a78d2b66b | ||
|
|
604a01d353 | ||
|
|
e8ffe1164f | ||
|
|
1ae37055c9 | ||
|
|
a431c40b1b | ||
|
|
285ff7956b | ||
|
|
64f114c96f | ||
|
|
9f22f63222 | ||
|
|
0bb8384c76 | ||
|
|
671c4a623f | ||
|
|
dfc0fd4340 | ||
|
|
11c0763fba | ||
|
|
ff686d7c3b | ||
|
|
4765644451 | ||
|
|
718712eb01 | ||
|
|
ca0689d254 | ||
|
|
7e0859872b | ||
|
|
28d4f58f33 | ||
|
|
ee0d8d9997 | ||
|
|
5805631be5 | ||
|
|
baf7d90df9 | ||
|
|
cb3c3cb2c9 | ||
|
|
df60fdb194 | ||
|
|
93923e125d | ||
|
|
fcbd45453f | ||
|
|
1ec1c20be1 | ||
|
|
ccab14384d | ||
|
|
8cc652e812 | ||
|
|
2fb066507a | ||
|
|
4b6cba0a6d | ||
|
|
4f831fbd34 | ||
|
|
a28ff05981 | ||
|
|
2ab8a1bac3 | ||
|
|
7053058db6 | ||
|
|
44f8d5de70 | ||
|
|
3c6447528a | ||
|
|
5614bd5d5a | ||
|
|
b563b8d1eb | ||
|
|
6b887c42af | ||
|
|
3d1f0f63b9 | ||
|
|
aad60fef5b | ||
|
|
db8c66b107 | ||
|
|
abb28792e8 | ||
|
|
57b3623a9a | ||
|
|
bae4ecd867 | ||
|
|
17fb80ad51 | ||
|
|
5e0f5cb9cd | ||
|
|
0544933abd | ||
|
|
fe1dd8ca72 | ||
|
|
969b7e625f | ||
|
|
1c24c600cd | ||
|
|
d8c1bc6059 | ||
|
|
e1fe6f99cf | ||
|
|
ef305c93cd | ||
|
|
f6f26d03f9 | ||
|
|
e72cddccd9 | ||
|
|
92a588ea16 | ||
|
|
e879375ca4 | ||
|
|
7c5bf77f17 | ||
|
|
a0a7a66470 | ||
|
|
26c562c174 | ||
|
|
e1c2b0e340 | ||
|
|
d4fe440b49 | ||
|
|
4c23092c55 | ||
|
|
a309428a2a | ||
|
|
b9dbd006be | ||
|
|
a9d2d09e64 | ||
|
|
516e3862fa | ||
|
|
1ee5e924b6 | ||
|
|
ab92033b90 | ||
|
|
d62f017bce | ||
|
|
68b0396dfe | ||
|
|
f005241549 | ||
|
|
bba40cd69c | ||
|
|
f3d6efbcfa | ||
|
|
f1c1ca59fe | ||
|
|
5cf69732ef | ||
|
|
eed9edaa1c | ||
|
|
940efdab3a | ||
|
|
b7878d2841 | ||
|
|
430af5e22d | ||
|
|
0110dc3c0f | ||
|
|
d849e6ae0a | ||
|
|
26274114a3 | ||
|
|
45fb017496 | ||
|
|
a2a05ebc5b | ||
|
|
ad0d39f457 | ||
|
|
1915e16fe1 | ||
|
|
c1b8f4c706 | ||
|
|
306cd789df | ||
|
|
8e480d0e84 | ||
|
|
3ad13b1b66 | ||
|
|
7901e76561 | ||
|
|
f3650d3d37 | ||
|
|
ad90d9e200 | ||
|
|
a9f365851e | ||
|
|
4cc117b45b | ||
|
|
0cc187025f | ||
|
|
be5f1560b2 | ||
|
|
9f6c73442b | ||
|
|
627aea0e8f | ||
|
|
edbe0c7179 | ||
|
|
ccd317384d | ||
|
|
9388ef07d8 | ||
|
|
b7e550ae89 | ||
|
|
4e571f1f99 | ||
|
|
5766696555 | ||
|
|
951270bc8f | ||
|
|
b7b08a40e8 | ||
|
|
8eeea5ef5a | ||
|
|
61cbf91150 | ||
|
|
3dff7d3962 | ||
|
|
2b790a8a71 | ||
|
|
4b8f63d84d | ||
|
|
3dcb3eac42 | ||
|
|
1f91283f40 | ||
|
|
c43a493ad8 | ||
|
|
80da86a5d3 | ||
|
|
ece3fec8ba | ||
|
|
3a4c6a4a7a | ||
|
|
0a0bbb68d1 | ||
|
|
34a1cbbead | ||
|
|
f05e0df39e | ||
|
|
cdde9d3390 | ||
|
|
cae095ce30 | ||
|
|
5fe3021e5e | ||
|
|
bc5edb81db | ||
|
|
8d3616eccd | ||
|
|
d2d5540ff1 | ||
|
|
3fbd86c89d | ||
|
|
ce3f31a48c | ||
|
|
89a2ae87d7 | ||
|
|
730acbeeb7 | ||
|
|
3583643a11 | ||
|
|
0050b5eb6b | ||
|
|
b16b67e6c4 | ||
|
|
55362d68ef | ||
|
|
2bc6985ae3 | ||
|
|
55da0afd06 | ||
|
|
2fdc8b0841 | ||
|
|
313b6a59d2 | ||
|
|
92ec0929ae | ||
|
|
57b7c99b9a | ||
|
|
8171bf20fd | ||
|
|
99dce74b09 | ||
|
|
40dfcd94f9 | ||
|
|
84a04d9ead | ||
|
|
e1d241d723 | ||
|
|
6a30a9bb4b | ||
|
|
da665bb14f | ||
|
|
e30e722f8b | ||
|
|
c5577478ca | ||
|
|
d730f0fa8b | ||
|
|
4798fadc12 | ||
|
|
bf9435cd36 | ||
|
|
fdea9f0435 | ||
|
|
8cb5c2168e | ||
|
|
6cd844ea51 | ||
|
|
7eb9175def | ||
|
|
af821184d5 | ||
|
|
8c09b2c6a8 | ||
|
|
5b7e401b11 | ||
|
|
d22ef554bb | ||
|
|
3566779895 | ||
|
|
9c5f1e4df9 | ||
|
|
a2a5f84711 | ||
|
|
55897a47a9 | ||
|
|
67197aafec | ||
|
|
ddfc94d7ce | ||
|
|
22a806d05a | ||
|
|
65e2c55e9b | ||
|
|
e97e6b29df | ||
|
|
6d4961f7b6 | ||
|
|
e31a6eef18 | ||
|
|
a3d76b9a94 | ||
|
|
234b137328 | ||
|
|
9419a19c66 | ||
|
|
3193a7a706 | ||
|
|
1259abed5f | ||
|
|
6d7732710c | ||
|
|
ec9573643a | ||
|
|
bf0ee99b6d | ||
|
|
9fce12730c | ||
|
|
09d170eb22 | ||
|
|
65b414df93 | ||
|
|
730b3affae | ||
|
|
dbf114a0f2 | ||
|
|
3fc80a03d9 | ||
|
|
901c453c16 | ||
|
|
9b024ffcdc | ||
|
|
6e57af02af | ||
|
|
86983092af | ||
|
|
85c41f9032 | ||
|
|
2ff3ab447d | ||
|
|
8a7022fc14 | ||
|
|
bfa61b5139 | ||
|
|
113ba7b77c | ||
|
|
69ffc0966b | ||
|
|
d4a6d95127 | ||
|
|
3df48d5625 | ||
|
|
0a5955d6fa | ||
|
|
d6b4b0589b | ||
|
|
ab43524bbc | ||
|
|
2ea78cb238 | ||
|
|
b245017593 | ||
|
|
ed121f247c | ||
|
|
92b4361100 | ||
|
|
e450765eb6 | ||
|
|
018d0e1db9 | ||
|
|
c300342a29 | ||
|
|
fdc4ff607d | ||
|
|
3639196c83 | ||
|
|
9c8d2acacc | ||
|
|
7aeb194d6e | ||
|
|
678e16af4d | ||
|
|
9040f828e9 | ||
|
|
e63fad1daa | ||
|
|
e527d4a9f3 | ||
|
|
30a70a0f7c | ||
|
|
9c28b41d49 | ||
|
|
5b35ace098 | ||
|
|
e04eb84090 | ||
|
|
b3c5570bdd | ||
|
|
dae36b0e67 | ||
|
|
3943794b0e | ||
|
|
c883ce4439 | ||
|
|
a69ce00b6a | ||
|
|
061ae17cc2 | ||
|
|
5ee3ed07be | ||
|
|
884671a065 | ||
|
|
1dae4bf8ee | ||
|
|
d202233beb | ||
|
|
6530faaf78 | ||
|
|
3dd6aeef46 | ||
|
|
45a496b802 | ||
|
|
ab8f41b80e |
No files matched your search
+1
-1
@@ -2,8 +2,8 @@ tools/linuxfs/** text=auto eol=lf
|
||||
tools/mangoapp/** text=auto eol=lf
|
||||
tools/comet/LICENSE whitespace=-blank-at-eof
|
||||
tools/comet/droiddeck.patch whitespace=-blank-at-eol,-blank-at-eof
|
||||
tools/gamescope/patches/0900-enhanced-frame-diagnostics.patch whitespace=-blank-at-eol,-blank-at-eof,-space-before-tab
|
||||
|
||||
# These helpers are packaged verbatim and executed inside the Linux runtime.
|
||||
tools/linuxfs/overlay/** text eol=lf
|
||||
tools/linuxfs/desktop/** text eol=lf
|
||||
tools/mangoapp/mangoapp text eol=lf
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# GitHub PR openers trusted for automatic release-signed test builds.
|
||||
ASEbrahim
|
||||
FX192
|
||||
K11MCH1
|
||||
kingokksa
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
name: Build FEX (x86-64 Steam client)
|
||||
|
||||
# Checks tools/fex: the same build DroidDeck-Components' build-fex.yml runs from this repository to publish FEX
|
||||
# (its single `fex` release and fex.json, which the app follows). Nothing is published from here.
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
@@ -31,7 +28,6 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build in Arch Linux ARM
|
||||
# Inputs reach the shell as environment variables, never pasted into the script.
|
||||
env:
|
||||
FEX_EXTRA_CXXFLAGS: ${{ inputs.cxxflags }}
|
||||
FEX_THUNKS: ${{ inputs.thunks == 'true' && '1' || '0' }}
|
||||
|
||||
@@ -2,8 +2,11 @@ name: Build gamescope (patched)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'tools/gamescope/**'
|
||||
- 'tools/gamescope/build-in-arch.sh'
|
||||
- 'tools/gamescope/patches/**'
|
||||
- 'tools/gamescope/runtime-sonames.txt'
|
||||
- '.github/workflows/build-gamescope.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -15,6 +18,10 @@ on:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ inputs.tag && format('gamescope-tag-{0}', inputs.tag) || format('gamescope-{0}-{1}', github.event_name, github.ref) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
@@ -32,11 +39,25 @@ jobs:
|
||||
path: |
|
||||
gamescope.tzst
|
||||
gamescope.tzst.sha256
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: components-token
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-Components
|
||||
- name: Publish
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_TOKEN: ${{ steps.components-token.outputs.token }}
|
||||
GH_REPO: Droid-Deck/DroidDeck-Components
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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
|
||||
gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
echo "::error::$TAG is already published in DroidDeck-Components, and published tags are never replaced: use a new tag"
|
||||
exit 1
|
||||
fi
|
||||
gh release create "$TAG" --latest=false --prerelease=false --title "$TAG" --notes "gamescope 3.16.29 with tools/gamescope/patches ($GITHUB_REPOSITORY $GITHUB_SHA, run $GITHUB_RUN_ID); staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256
|
||||
gh release edit "$TAG" --latest=false --prerelease=false
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Build Venus (Mali)
|
||||
# The Mali (Venus) route's binaries from tools/venus (David Cimo's recipes): Mesa's Venus ICD and the
|
||||
# Mali compatibility layer (glibc, for the session) and virglrenderer's vtest server (bionic, run by
|
||||
# the app over the phone's own Vulkan driver), packed as venus.tzst rooted at app/src/main. The apk
|
||||
# build fetches it by tools/venus/release.env (tag + sha256); nothing compiled is kept in this repo.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [test/non-adreno-all]
|
||||
paths: ['tools/venus/**', '.github/workflows/build-venus.yml']
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq ninja-build pkg-config zstd python3-pip
|
||||
pip3 install --break-system-packages 'meson>=1.5'
|
||||
- name: Build
|
||||
run: ANDROID_NDK_HOME="$ANDROID_NDK_LATEST_HOME" WORK=$PWD/venus-work tools/venus/ci-build.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: venus
|
||||
path: |
|
||||
venus-work/venus.tzst
|
||||
venus-work/venus.tzst.sha256
|
||||
@@ -63,6 +63,8 @@ jobs:
|
||||
echo "base-sha=$(git rev-parse origin/main)" >> "$GITHUB_OUTPUT"
|
||||
git config user.name "ci"
|
||||
git config user.email "[email protected]"
|
||||
# Partial clone: a merged blob that only exists in a fork's history can't be lazily fetched (GitHub: "not our ref"); merge-tree writes it locally first.
|
||||
git merge-tree --write-tree HEAD origin/main >/dev/null || true
|
||||
git merge --no-ff --no-edit origin/main
|
||||
- name: Resolve trusted manifest base
|
||||
id: trusted
|
||||
@@ -100,6 +102,7 @@ jobs:
|
||||
git fetch --no-tags origin "$BUILD_BASE_SHA"
|
||||
git config user.name "ci"
|
||||
git config user.email "[email protected]"
|
||||
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
|
||||
git merge --no-ff --no-edit FETCH_HEAD
|
||||
- name: Verify pinned source tree
|
||||
env:
|
||||
@@ -133,11 +136,18 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
components:
|
||||
name: Prepare verified components
|
||||
components-runner:
|
||||
needs: source
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
uses: ./.github/workflows/pick-runner.yml
|
||||
secrets:
|
||||
RUNNER_STATUS_TOKEN: ${{ secrets.RUNNER_STATUS_TOKEN }}
|
||||
|
||||
components:
|
||||
name: Prepare verified components
|
||||
needs: [source, components-runner]
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ${{ fromJSON(needs.components-runner.outputs.labels) }}
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -158,6 +168,7 @@ jobs:
|
||||
git fetch --no-tags origin "$BUILD_BASE_SHA"
|
||||
git config user.name "ci"
|
||||
git config user.email "[email protected]"
|
||||
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
|
||||
git merge --no-ff --no-edit FETCH_HEAD
|
||||
- name: Verify pinned source tree
|
||||
env:
|
||||
@@ -165,6 +176,8 @@ jobs:
|
||||
run: test "$(git rev-parse 'HEAD^{tree}')" = "$SOURCE_TREE"
|
||||
- name: Resolve the pinned Android NDK
|
||||
run: bash tools/ci/resolve-ndk.sh
|
||||
- name: Check out MaliBridge
|
||||
run: git submodule update --init external/MaliBridge
|
||||
|
||||
- name: Plan component identities
|
||||
id: plan
|
||||
@@ -228,6 +241,7 @@ jobs:
|
||||
${{ steps.preloads.outputs.cache-hit != 'true' && 'runtime' || '' }}
|
||||
${{ steps.component-preloads.outputs.arm64 != 'true' && 'arm64-preloads' || '' }}
|
||||
${{ steps.comet.outputs.cache-hit != 'true' && 'comet' || '' }}
|
||||
vkbridge
|
||||
run: python3 tools/components/prepare.py tools
|
||||
|
||||
- name: Build changed ARM64 preloads
|
||||
@@ -238,6 +252,9 @@ jobs:
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
run: python3 tools/components/prepare.py runtime
|
||||
|
||||
- name: Build the Mali bridge's Linux side
|
||||
run: python3 tools/components/prepare.py vkbridge
|
||||
|
||||
- name: Fetch the Debian image the preloads build in
|
||||
if: steps.component-preloads.outputs.x86 != 'true'
|
||||
env:
|
||||
@@ -265,15 +282,37 @@ jobs:
|
||||
if: steps.proot.outputs.cache-hit != 'true'
|
||||
run: python3 tools/components/prepare.py proot
|
||||
|
||||
- name: Build the non-Adreno test additions (Mali bridge client, Venus GL probe, Xclipse layer)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash tools/ci/install-ubuntu-packages.sh gcc-aarch64-linux-gnu libvulkan-dev
|
||||
D=app/src/main/assets/linuxfs
|
||||
mkdir -p "$D/usr/local/bin" "$D/usr/local/lib/droiddeck-xclipse"
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wextra -Wno-unused-parameter -fvisibility=hidden -pthread \
|
||||
-o "$D/usr/local/lib/droiddeck-xclipse/libVkLayer_droiddeck_xclipse.so" tools/linuxfs/xclipse-layer/xclipse_layer.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/lib/droiddeck-xclipse/libVkLayer_droiddeck_xclipse.so"
|
||||
install -m644 tools/linuxfs/xclipse-layer/VkLayer_droiddeck_xclipse.json "$D/usr/local/lib/droiddeck-xclipse/"
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -o "$D/usr/local/bin/droiddeck-glprobe" tools/venus/glprobe.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-glprobe"
|
||||
|
||||
- name: Check the preloads
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs
|
||||
D=app/src/main/assets/linuxfs
|
||||
VKB="$D/usr/local/lib/vkbridge/libvulkan_droidbridge.so"
|
||||
test -f "$VKB" && test -f "$D/usr/local/share/vkbridge/droidbridge_icd.json" && test -f "$D/usr/local/bin/droiddeck-vkbridge-smoke"
|
||||
VKB_NEED=$(readelf -d "$VKB" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p' | grep -vx 'libc.so.6' | grep -vx 'ld-linux-aarch64.so.1' || true)
|
||||
[ -z "$VKB_NEED" ] || { echo "::error::the bridge ICD needs $VKB_NEED"; exit 1; }
|
||||
[ "$(readelf -Ws "$VKB" | awk '$4 == "FUNC" && $5 == "GLOBAL" && $7 != "UND" {sub(/@.*/, "", $8); print $8}' | grep -c '^vk_icd')" = 3 ] || { echo "::error::the bridge ICD does not export the loader interface"; exit 1; }
|
||||
|
||||
- name: Build GOG Galaxy companion
|
||||
if: steps.comet.outputs.cache-hit != 'true'
|
||||
run: python3 tools/components/prepare.py comet
|
||||
|
||||
- name: Build the DiRT 3 / DiRT Showdown ws2_32 stand-in
|
||||
run: python3 tools/components/prepare.py dirt-ws2
|
||||
|
||||
- name: Prepare graphics driver variants
|
||||
run: python3 tools/quest/build-driver.py "$ANDROID_HOME/ndk/$NDK_VERSION"
|
||||
- name: Record and pack prepared components
|
||||
@@ -286,11 +325,18 @@ jobs:
|
||||
compression-level: 0
|
||||
retention-days: 14
|
||||
|
||||
build-runner:
|
||||
needs: [components]
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
uses: ./.github/workflows/pick-runner.yml
|
||||
secrets:
|
||||
RUNNER_STATUS_TOKEN: ${{ secrets.RUNNER_STATUS_TOKEN }}
|
||||
|
||||
build:
|
||||
needs: [source, components, xbox]
|
||||
needs: [source, components, xbox, build-runner]
|
||||
name: Compile APK and Android tests
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ${{ fromJSON(needs.build-runner.outputs.labels) }}
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
variants: ${{ steps.variants.outputs.list }}
|
||||
@@ -304,6 +350,7 @@ jobs:
|
||||
!/docs/releases/media/
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Check human authorship
|
||||
env:
|
||||
@@ -364,6 +411,7 @@ jobs:
|
||||
git fetch --no-tags origin "$BUILD_BASE_SHA"
|
||||
BASE=$(git rev-parse --short FETCH_HEAD)
|
||||
HEAD=$(git rev-parse --short HEAD)
|
||||
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
|
||||
if git merge --no-ff --no-edit FETCH_HEAD >/tmp/merge.out 2>&1; then
|
||||
{
|
||||
echo "### Mergeable"
|
||||
@@ -514,7 +562,33 @@ jobs:
|
||||
key: pack-inputs-v1-${{ hashFiles('tools/packs/build.sh', 'tools/packs/fetch-verified.sh', 'tools/packs/tracking/release.env') }}
|
||||
restore-keys: pack-inputs-v1-
|
||||
- name: Build the add-on packs
|
||||
run: bash tools/packs/build.sh app/build/packs
|
||||
run: |
|
||||
python3 tools/powervr/fetch.py
|
||||
bash tools/packs/build.sh app/build/packs
|
||||
|
||||
- name: Pin the Syncthing source
|
||||
id: syncthing-src
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git submodule update --init --recursive tools/syncthing/syncthing-android
|
||||
echo "commit=$(git -C tools/syncthing/syncthing-android/syncthing/src/github.com/syncthing/syncthing rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache Syncthing
|
||||
id: syncthing
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: app/src/main/jniLibs/arm64-v8a/libsyncthingnative.so
|
||||
key: syncthing-${{ env.NDK_VERSION }}-${{ hashFiles('tools/syncthing/build.sh', 'tools/syncthing/syncthing-android/gradle/libs.versions.toml') }}-${{ steps.syncthing-src.outputs.commit }}
|
||||
- uses: actions/setup-go@v5
|
||||
if: steps.syncthing.outputs.cache-hit != 'true'
|
||||
with:
|
||||
go-version: stable
|
||||
cache: false
|
||||
- name: Build Syncthing
|
||||
if: steps.syncthing.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export NDK="$ANDROID_HOME/ndk/$NDK_VERSION"
|
||||
tools/syncthing/build.sh app/src/main/jniLibs/arm64-v8a
|
||||
|
||||
- name: Note when the proot pack pin is behind this build
|
||||
run: |
|
||||
@@ -526,6 +600,21 @@ jobs:
|
||||
echo "::warning::built proot pack $sha does not match tools/packs/proot/release.env ($PROOT_PACK_SHA256). This build publishes the pack it just built. Source builds keep downloading the pinned pack until release.env is updated."
|
||||
fi
|
||||
|
||||
- name: Check out MaliBridge
|
||||
run: git submodule update --init external/MaliBridge
|
||||
- name: Fetch the Mali (Venus) component (tools/venus/release.env)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. tools/venus/release.env
|
||||
gh release download "${VENUS_TAG}" -R "${VENUS_REPO}" -p venus.tzst -O venus.tzst
|
||||
echo "${VENUS_SHA256} venus.tzst" | sha256sum -c -
|
||||
tar --use-compress-program=unzstd -xf venus.tzst -C app/src/main
|
||||
test -f app/src/main/jniLibs/arm64-v8a/libvirgl_test_server.so
|
||||
test -f app/src/main/assets/venus/libvulkan_virtio.so
|
||||
rm venus.tzst
|
||||
|
||||
- name: Build
|
||||
id: build
|
||||
env:
|
||||
@@ -533,6 +622,7 @@ jobs:
|
||||
SGDB_API_KEY: ${{ secrets.SGDB_API_KEY }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
python3 tools/powervr/fetch.py
|
||||
python3 tools/release/runtime_inputs.py record
|
||||
./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
|
||||
@@ -666,6 +756,7 @@ jobs:
|
||||
git fetch --no-tags origin "$BUILD_BASE_SHA"
|
||||
git config user.name "ci"
|
||||
git config user.email "[email protected]"
|
||||
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
|
||||
git merge --no-ff --no-edit FETCH_HEAD
|
||||
- name: Verify backend and test source match
|
||||
env:
|
||||
@@ -719,6 +810,7 @@ jobs:
|
||||
git fetch --no-tags origin "$BUILD_BASE_SHA"
|
||||
git config user.name "ci"
|
||||
git config user.email "[email protected]"
|
||||
git merge-tree --write-tree HEAD FETCH_HEAD >/dev/null || true
|
||||
git merge --no-ff --no-edit FETCH_HEAD
|
||||
- name: Verify backend and test source match
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Pick runner
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
labels:
|
||||
value: ${{ jobs.pick.outputs.labels }}
|
||||
secrets:
|
||||
RUNNER_STATUS_TOKEN:
|
||||
required: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
pick:
|
||||
name: Pick runner
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 2
|
||||
outputs:
|
||||
labels: ${{ steps.runner.outputs.labels || '"ubuntu-24.04"' }}
|
||||
steps:
|
||||
- name: Pick the compile runner
|
||||
id: runner
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RUNNER_STATUS_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
labels='"ubuntu-24.04"'
|
||||
if [ -n "$GH_TOKEN" ]; then
|
||||
idle=$(gh api "orgs/${GITHUB_REPOSITORY_OWNER}/actions/runners" --jq '[.runners[] | select(.status == "online" and .busy == false and any(.labels[]; .name == "droiddeck-ws"))] | length' 2>/dev/null || echo 0)
|
||||
if [ "${idle:-0}" -gt 0 ]; then
|
||||
labels='["self-hosted","droiddeck-ws"]'
|
||||
fi
|
||||
fi
|
||||
echo "labels=$labels" >> "$GITHUB_OUTPUT"
|
||||
echo "Compile runner: \`$labels\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -47,10 +47,18 @@ jobs:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
retry() {
|
||||
for attempt in 1 2 3 4; do
|
||||
"$@" && return 0
|
||||
[ "$attempt" = 4 ] || sleep $((attempt * 20))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
existing=$(retry gh api --paginate "repos/Droid-Deck/DroidDeck-CI/releases?per_page=100" --jq '.[].tag_name')
|
||||
found=0
|
||||
for tag in "pr-$PR_NUMBER" "pr-$PR_NUMBER.0"; do
|
||||
if gh release view "$tag" -R Droid-Deck/DroidDeck-CI >/dev/null 2>&1; then
|
||||
gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
|
||||
if grep -qxF "$tag" <<<"$existing"; then
|
||||
retry gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
|
||||
echo "Deleted $tag"
|
||||
found=1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: PR Release Sweep
|
||||
run-name: sweep test builds of closed PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 */6 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: pr-release-sweep
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sweep:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-CI
|
||||
- name: Delete test builds whose PR is no longer open
|
||||
env:
|
||||
CI_TOKEN: ${{ steps.ci-token.outputs.token }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
retry() {
|
||||
for attempt in 1 2 3 4; do
|
||||
"$@" && return 0
|
||||
[ "$attempt" = 4 ] || sleep $((attempt * 20))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
tags=$(GH_TOKEN="$CI_TOKEN" retry gh api --paginate "repos/Droid-Deck/DroidDeck-CI/releases?per_page=100" --jq '.[].tag_name' |
|
||||
grep -E '^pr-[0-9]+(\.0)?$' || true)
|
||||
failed=0
|
||||
for tag in $tags; do
|
||||
number=${tag#pr-}
|
||||
number=${number%.0}
|
||||
state=$(retry gh api "repos/$GITHUB_REPOSITORY/pulls/$number" --jq .state) || { failed=1; continue; }
|
||||
if [ "$state" != open ]; then
|
||||
if GH_TOKEN="$CI_TOKEN" retry gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag; then
|
||||
echo "Deleted $tag (PR #$number is $state)"
|
||||
else
|
||||
failed=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
exit "$failed"
|
||||
@@ -1,11 +1,5 @@
|
||||
name: Publish add-on packs
|
||||
|
||||
# The add-on packs (tools/packs/build.sh) a Build APK run made, published to DroidDeck-Components under
|
||||
# content-addressed tags (pack-<name>-<first 16 hex of sha256>). The APK from that run pins each pack's
|
||||
# SHA-256 and downloads it from that tag. A tag is created once and never replaced.
|
||||
# Only main and release builds publish here; a PR test build attaches the packs main has not published
|
||||
# to its pr-N release on DroidDeck-CI (pr-release-publish.yml), which is deleted when the PR closes.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -61,7 +55,7 @@ jobs:
|
||||
jq -r --arg n "$2" '.assets // [] | .[] | select(.name == $n) | .digest // empty'
|
||||
}
|
||||
commit=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN" --jq .head_sha 2>/dev/null || echo "$GITHUB_SHA")
|
||||
for name in proot directaudio tracking xbox comet butler; do
|
||||
for name in proot directaudio tracking xbox comet butler powervr; do
|
||||
file="packs/$name.tar.zst"
|
||||
[ -f "$file" ] || { echo "::error::$name.tar.zst is missing from Build APK run $BUILD_RUN"; exit 1; }
|
||||
sha=$(sha256sum "$file" | cut -d' ' -f1)
|
||||
|
||||
@@ -13,6 +13,8 @@ app/src/main/jniLibs/arm64-v8a/libproot.so
|
||||
app/src/main/jniLibs/arm64-v8a/.proot-inputs
|
||||
app/src/main/jniLibs/arm64-v8a/.proot-outputs
|
||||
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
|
||||
app/src/main/jniLibs/arm64-v8a/libsyncthingnative.so
|
||||
app/src/main/jniLibs/arm64-v8a/.syncthing-inputs
|
||||
app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so
|
||||
app/src/main/rust/stores/target/
|
||||
app/src/main/assets/droiddeck-esync/
|
||||
@@ -32,4 +34,5 @@ __pycache__/
|
||||
|
||||
.signing.env
|
||||
app/src/main/assets/directaudio/
|
||||
app/src/main/assets/hybris/
|
||||
/tools/vr/out/
|
||||
@@ -0,0 +1,6 @@
|
||||
[submodule "external/MaliBridge"]
|
||||
path = external/MaliBridge
|
||||
url = https://github.com/ASEbrahim/MaliBridge.git
|
||||
[submodule "tools/syncthing/syncthing-android"]
|
||||
path = tools/syncthing/syncthing-android
|
||||
url = https://github.com/researchxxl/syncthing-android
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copy to .signing.env, fill in, chmod 600 .signing.env
|
||||
RELEASE_KEYSTORE=/path/to/release.p12
|
||||
RELEASE_STORE_PASSWORD=change-me
|
||||
RELEASE_KEY_ALIAS=droiddeck
|
||||
@@ -5,12 +5,12 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big Picture on your Adreno handheld, with Windows games through Valve's ARM64 Proton.
|
||||
DroidDeck brings the SteamOS experience to Android: Steam Gaming Mode, full Linux desktop, integration with GOG, Epic Games, Xbox, and other storefronts, Steam VR, and more!
|
||||
|
||||
<p align="center"><b>100% open source. No ads. No telemetry. No source withheld.</b></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Droid-Deck/DroidDeck/releases/download/0.6.7/DroidDeck-0.6.7.apk"><img src="https://img.shields.io/badge/Download-DroidDeck%200.6.7%20APK-3DDC84?style=for-the-badge&logo=android&logoColor=white" alt="Download DroidDeck 0.6.7"></a>
|
||||
<a href="https://github.com/Droid-Deck/DroidDeck/releases/download/0.6.9/DroidDeck-0.6.9.apk"><img src="https://img.shields.io/badge/Download-DroidDeck%200.6.9%20APK-3DDC84?style=for-the-badge&logo=android&logoColor=white" alt="Download DroidDeck 0.6.9"></a>
|
||||
<a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20us-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a>
|
||||
</p>
|
||||
|
||||
@@ -38,7 +38,7 @@ DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big
|
||||
|
||||
1. Android 9 or newer on Adreno 730 or newer, or any 8xx. Adreno 6xx is experimental. Mali, Xclipse, PowerVR and Adreno 710 are unsupported. No root needed.
|
||||
2. About 3 GB free for the runtime, plus 1.1 GB for the desktop and emulators.
|
||||
3. Install the [APK](https://github.com/Droid-Deck/DroidDeck/releases/download/0.6.7/DroidDeck-0.6.7.apk), install the Linux runtime, press **Play** and sign in to Steam.
|
||||
3. Install the [APK](https://github.com/Droid-Deck/DroidDeck/releases/download/0.6.9/DroidDeck-0.6.9.apk), install the Linux runtime, press **Play** and sign in to Steam.
|
||||
4. On Android 14 and newer, turn on **Disable child process restrictions** in Developer options. On Android 12 and 13, Steam's first launch shows a **Fix it for me** button that walks you through it.
|
||||
|
||||
Smart glasses setup is in the [support matrix](docs/smart-glasses-support.md). Release notes are in [docs/releases](docs/releases).
|
||||
@@ -78,6 +78,7 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V
|
||||
- libblsession runs Wine's arm64 `SIGUSR2` context handler (`dlls/ntdll/unix/signal_arm64.c`, LGPL-2.1-or-later) in place on a frame laid out like [Linux](https://github.com/torvalds/linux)'s `arch/arm64/kernel/signal.c`, and serves `fill_vm_counters` in `dlls/ntdll/unix/process.c` the kernel's own `/proc/<pid>/status`.
|
||||
- droiddeck-ntsync follows the [Linux](https://github.com/torvalds/linux) ntsync semantics in `Documentation/userspace-api/ntsync.rst` (GPL-2.0).
|
||||
- The proot fast path orders bindings like [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later).
|
||||
- DiRT 3 and DiRT Showdown keep their telemetry socket the way RoccoC's DiRTTelemetryErrorFix (MIT) does, based on the fix by Carsten Gottschling.
|
||||
- Games use [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's per render pass memory choice. The Steam client keeps `TU_DEBUG=sysmem`.
|
||||
- Black Ops III and Dark Souls Remastered start without [umu-protonfixes](https://github.com/Open-Wine-Components/umu-protonfixes) (BSD-2-Clause), whose `311210.py` and `570940.py` fail under ARM64 Proton.
|
||||
- [ReSkate](https://github.com/Dingo-Shenanigans/ReSkate) (GPL-3.0) by Dingo Shenanigans uses the launch option from its README and D3D12 feature level 12_1.
|
||||
@@ -92,9 +93,13 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V
|
||||
- Epic's official Linux launcher preview runs through FEX. That setup is outside Epic's listed Linux support.
|
||||
- Battle.net is by [@kayakyakr](https://github.com/kayakyakr). It runs Blizzard's own client in Proton (downloaded from Blizzard, not included) and shows key art from the [Battle.net Shop](https://shop.battle.net). Without Steam, its games see the pad as an Xbox 360 controller via `PROTON_SPOOF_STEAMINPUT_VIDPID` in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/winebus.sys`.
|
||||
- Battle.net installs drive Blizzard's Agent the way [@barncastle](https://github.com/barncastle)'s [Battle.Net-Installer](https://github.com/barncastle/Battle.Net-Installer) does, with requests from [wowdev.wiki](https://wowdev.wiki/Agent). Read without taking code: [@mubaraknumann](https://github.com/mubaraknumann)'s [unifideck](https://github.com/mubaraknumann/unifideck), [@djmittens](https://github.com/djmittens)'s [neo-mittens](https://github.com/djmittens/neo-mittens) `bnetswitch`, [@BCD1210](https://github.com/BCD1210)'s [soju](https://github.com/BCD1210/soju) and [Lutris](https://github.com/lutris/lutris)'s `product.db` reader.
|
||||
- World of Warcraft addon imports replace an addon's folder from a temporary copy, as [WowUp](https://github.com/WowUp/WowUp) does, and read flavour folders the way [WoWDeck](https://github.com/sigboe/wowdeck) does. No code from either is used.
|
||||
- The World of Warcraft addon Download window searches [WoWInterface](https://www.wowinterface.com) through its MMOUI API. [CurseForge](https://www.curseforge.com/wow) opens in the user's browser, with no API and no key.
|
||||
- Xbox games load [@domsvoboda](https://github.com/domsvoboda)'s [xodus-nexus](https://github.com/domsvoboda/xodus-nexus) modules (LGPL-2.1-or-later), including `wintypes.dll`. Minecraft's GameInput setup was checked against [@Wyze3306](https://github.com/Wyze3306)'s [BedrockOnLinux](https://github.com/Wyze3306/BedrockOnLinux) (MIT).
|
||||
- The Xbox Cloud Gaming player uses the WebRTC client of [@unknownskl](https://github.com/unknownskl)'s [Greenlight](https://github.com/unknownskl/greenlight) (MIT), and its offer tweaks and sharpening follow [@redphx](https://github.com/redphx)'s [Better xCloud](https://github.com/redphx/better-xcloud) (MIT). See `tools/xcloud-player/NOTICE.md`.
|
||||
- The itch.io butler pack ships butler's `7z.so`, itch.io's build of [7-Zip](https://www.7-zip.org) by Igor Pavlov (LGPL-2.1, unRAR restriction on RAR code), unmodified. Other archives use Apache Commons Compress and the runtime's bsdtar (libarchive).
|
||||
- The Store uses [Flatpak](https://github.com/flatpak/flatpak) (LGPL-2.1) through DroidDeck's own bubblewrap shim. Chromium and Electron apps start directly, since [zypak](https://github.com/refi64/zypak) by [@refi64](https://github.com/refi64) (BSD-3-Clause) always uses Flatpak's spawn portal.
|
||||
- Folder sync runs [Syncthing](https://github.com/syncthing/syncthing) (MPL-2.0), built from [@researchxxl](https://github.com/researchxxl)'s [syncthing-android](https://github.com/researchxxl/syncthing-android) fork (MPL-2.0).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -103,6 +108,7 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V
|
||||
|
||||
- Steam audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink`.
|
||||
- The DroidDeck HUD is [@The412Banner](https://github.com/The412Banner)'s [FusionHUD](https://github.com/The412Banner/FusionHUD) (GPL-3.0 with an attribution term). Its readings match Valve's [MangoHud](https://github.com/flightlessmango/MangoHud) build (MIT), with frame times from gamescope patch 0125.
|
||||
- The HUD's CPU, GPU load, thermal and battery readers come from [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx)'s work.
|
||||
- LSFG frame generation comes from Camille LaVey and the [Eden](https://eden-emu.dev) project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported 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.
|
||||
- Remote Play decodes through Android's MediaCodec or the [FFmpeg](https://ffmpeg.org) (LGPL-2.1-or-later) in Steam's runtime. Qualcomm low latency settings follow [Moonlight](https://github.com/moonlight-stream/moonlight-android) (GPL-3.0).
|
||||
- [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) sockets use the settings in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions.
|
||||
@@ -125,6 +131,7 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V
|
||||
<summary><b>Smart glasses</b></summary>
|
||||
|
||||
- Builds on WinNative's VITURE work. Protocol checked against [@wheaney](https://github.com/wheaney)'s [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver), [@badicsalex](https://github.com/badicsalex)'s [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) and [go-macos/iokit](https://pkg.go.dev/github.com/go-macos/iokit/viture). The optional VITURE SDK is not included.
|
||||
- The VITURE protocol V2 frames adapt [@elasticjava](https://github.com/elasticjava)'s [viture-v2](https://github.com/elasticjava/viture-v2) (MIT) by Holger Bartnick. See `docs/third-party/viture-v2`.
|
||||
- RayNeo adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0). See [docs/third-party/rayneo](docs/third-party/rayneo/README.md).
|
||||
- XREAL 1S references XRLinuxDriver and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [@rohitsangwan01](https://github.com/rohitsangwan01)'s [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) (MIT) and [@0xcaff](https://github.com/0xcaff)'s [xr-tools](https://github.com/0xcaff/xr-tools).
|
||||
- Rokid follows ar-drivers-rs, @badicsalex's [Void Computing write-ups](https://voidcomputing.hu/blog/worse-better-prettier/) and Monado's Rokid driver.
|
||||
|
||||
+35
-10
@@ -4,7 +4,6 @@ plugins {
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
||||
|
||||
// Machine-specific tooling and optional CI-prepared inputs stay out of version control.
|
||||
def localBuildOptions = new Properties()
|
||||
def localBuildOptionsFile = rootProject.layout.projectDirectory.file('local.properties')
|
||||
if (localBuildOptionsFile.asFile.exists()) {
|
||||
@@ -94,12 +93,10 @@ def trackingPack = rootProject.file('tools/packs/tracking/release.env').readLine
|
||||
def prootPackPin = rootProject.file('tools/packs/proot/release.env').readLines()
|
||||
.findAll { it.contains('=') && !it.startsWith('#') }
|
||||
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
|
||||
def addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'butler'].collectEntries { name ->
|
||||
def addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'butler', 'powervr'].collectEntries { name ->
|
||||
def pack = file("build/packs/${name}.tar.zst")
|
||||
if (!pack.isFile()) {
|
||||
if (ciBuild) throw new GradleException("Add-on pack ${pack} is missing: run tools/packs/build.sh app/build/packs")
|
||||
// A source build that did not compile proot still pins a published pack, so the app
|
||||
// downloads the patched binary instead of falling back to the rootfs copy.
|
||||
if (name == 'proot' && prootPackPin.PROOT_PACK_SHA256) {
|
||||
return [(name): [sha256: prootPackPin.PROOT_PACK_SHA256, size: (prootPackPin.PROOT_PACK_SIZE ?: '0') as long]]
|
||||
}
|
||||
@@ -208,8 +205,7 @@ android {
|
||||
}
|
||||
|
||||
androidResources {
|
||||
// aapt's default ignore list, plus the trees shipped as add-on packs (tools/packs).
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~:<dir>droiddeck-xbox:<dir>droiddeck-comet:<dir>directaudio'
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~:<dir>droiddeck-xbox:<dir>droiddeck-comet:<dir>directaudio:<dir>hybris'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
@@ -219,7 +215,6 @@ android {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
pickFirsts += ['**/libtermux.so']
|
||||
// Shipped in the proot and tracking packs (tools/packs) and run or loaded from there.
|
||||
excludes += ['**/libproot.so', '**/libproot-loader.so', '**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so']
|
||||
}
|
||||
}
|
||||
@@ -238,7 +233,6 @@ android {
|
||||
outputs.upToDateWhen { false }
|
||||
outputs.doNotCacheIf('CI executes the test suite on every run') { true }
|
||||
}
|
||||
// Robolectric keeps a sandbox per SDK level the tests ask for; seven no longer fit in Gradle's default 512 MB.
|
||||
maxHeapSize = '2g'
|
||||
maxParallelForks = 2
|
||||
def logTarget = providers.systemProperty('robolectric.logging').orNull
|
||||
@@ -255,6 +249,11 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// No instrumented tests; the unused androidTest variant breaks the VS Code Kotlin import.
|
||||
androidComponents {
|
||||
beforeVariants(selector().all()) { it.androidTest.enable = false }
|
||||
}
|
||||
|
||||
// Vendor binaries remain in the user's SDK directory; never vendor them into Git.
|
||||
if (vitureSdkDir) {
|
||||
android.sourceSets.main.jniLibs.srcDir(new File(vitureSdkDir, 'android'))
|
||||
@@ -300,10 +299,21 @@ def stageGogNotices = tasks.register('stageGogNotices', Copy) {
|
||||
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('gogNotices'))
|
||||
tasks.named('preBuild').configure { dependsOn(stageGogNotices) }
|
||||
|
||||
if (!ciBuild) {
|
||||
def stageLocalPacks = tasks.register('stageLocalPacks', Sync) {
|
||||
from(layout.buildDirectory.dir('packs')) { include '*.tar.zst' }
|
||||
into(layout.buildDirectory.dir('localPackAssets/packs'))
|
||||
}
|
||||
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('localPackAssets'))
|
||||
tasks.named('preBuild').configure { dependsOn(stageLocalPacks) }
|
||||
}
|
||||
|
||||
def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) {
|
||||
mustRunAfter(stageSessionScripts, stageRuntimeAssets)
|
||||
workingDir rootProject.projectDir
|
||||
commandLine pythonExecutable, 'tools/release/runtime_inputs.py', 'verify'
|
||||
def allowStale = (project.findProperty('allowStaleRuntime')
|
||||
?: localBuildOptions.getProperty('droiddeck.allowStaleRuntime', 'false')).toString().toBoolean()
|
||||
commandLine([pythonExecutable, 'tools/release/runtime_inputs.py', 'verify'] + (allowStale ? ['--allow-stale-sources'] : []))
|
||||
}
|
||||
tasks.configureEach { task ->
|
||||
if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs)
|
||||
@@ -341,6 +351,21 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
|
||||
if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
|
||||
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
|
||||
|
||||
def maliBridgeDir = rootProject.file('external/MaliBridge')
|
||||
def vkbridgeJniLibsDir = layout.buildDirectory.dir('vkbridgeJniLibs')
|
||||
def buildVkBridgeServer = tasks.register('buildVkBridgeServer', Exec) {
|
||||
description = 'Builds the Vulkan bridge server (libvkbridge_server.so) with the NDK.'
|
||||
inputs.dir(new File(maliBridgeDir, 'common'))
|
||||
inputs.dir(new File(maliBridgeDir, 'server'))
|
||||
inputs.dir(new File(maliBridgeDir, 'third_party'))
|
||||
inputs.file(new File(maliBridgeDir, 'scripts/build-server-android.sh'))
|
||||
outputs.dir(vkbridgeJniLibsDir)
|
||||
commandLine 'bash', new File(maliBridgeDir, 'scripts/build-server-android.sh').absolutePath,
|
||||
android.ndkDirectory.absolutePath, vkbridgeJniLibsDir.get().asFile.absolutePath
|
||||
}
|
||||
android.sourceSets.main.jniLibs.srcDir(vkbridgeJniLibsDir)
|
||||
tasks.named('preBuild').configure { dependsOn(buildVkBridgeServer) }
|
||||
|
||||
dependencies {
|
||||
implementation "org.opencv:opencv:${trackingPack.OPENCV_VERSION}"
|
||||
implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49'
|
||||
@@ -362,11 +387,11 @@ dependencies {
|
||||
implementation 'androidx.camera:camera-camera2:1.4.2'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.4.2'
|
||||
implementation 'androidx.webkit:webkit:1.12.1'
|
||||
// Google's WebRTC (BSD) for the native Xbox Cloud Gaming player (XboxNativePlayerActivity).
|
||||
implementation 'io.github.webrtc-sdk:android:125.6422.07'
|
||||
implementation("com.google.mediapipe:tasks-vision:${trackingPack.MEDIAPIPE_VERSION}") {
|
||||
exclude group: 'com.google.auto.value', module: 'auto-value'
|
||||
}
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||
|
||||
@@ -3,12 +3,21 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<queries>
|
||||
<package android:name="cn.nubia.gamelauncher" />
|
||||
<intent>
|
||||
<action android:name="android.content.pm.action.CONFIRM_INSTALL" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.content.pm.action.CONFIRM_PERMISSIONS" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.SECONDARY_HOME" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
||||
@@ -60,6 +69,19 @@
|
||||
<meta-data android:name="android.game_mode_config" android:resource="@xml/game_mode_config" />
|
||||
<service android:name=".glasses.LumaRgbCaptureService" android:exported="false"
|
||||
android:process=":rgb_diagnostic" />
|
||||
<service android:name=".wallpaper.SteamWallService"
|
||||
android:label="@string/steam_wall_label"
|
||||
android:permission="android.permission.BIND_WALLPAPER"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.service.wallpaper" android:resource="@xml/steam_wall_wallpaper" />
|
||||
</service>
|
||||
<activity android:name=".wallpaper.WallpaperSettingsActivity"
|
||||
android:label="@string/steam_wall_label"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="true" />
|
||||
<service android:name=".input.HomeButtonService"
|
||||
android:label="@string/integration_shortcuts_service"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
@@ -69,6 +91,14 @@
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.accessibilityservice" android:resource="@xml/home_button_service" />
|
||||
</service>
|
||||
<service android:name=".session.BottomNotificationListener"
|
||||
android:label="@string/bottom_notifications_listener"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<receiver android:name=".core.ShortcutsListingReceiver" android:exported="false"
|
||||
android:process=":shortcuts_sync">
|
||||
<intent-filter>
|
||||
@@ -165,7 +195,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".PinRequestActivity"
|
||||
android:exported="true"
|
||||
@@ -178,13 +207,13 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".input.ControllerEditorActivity"
|
||||
android:name=".sync.SyncthingWebActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/SessionTheme"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".files.FileManagerActivity"
|
||||
@@ -285,6 +314,11 @@
|
||||
android:name=".session.SessionService"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false" />
|
||||
<!-- The compositor's native state is one per process, so the bottom-screen desktop's compositor runs in its own. -->
|
||||
<service
|
||||
android:name=".wayland.BottomDesktopCompositorService"
|
||||
android:process=":bottom_desktop"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".runtime.RuntimeInstallService"
|
||||
android:exported="false" />
|
||||
@@ -295,6 +329,10 @@
|
||||
android:name=".stores.download.StoreDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".sync.SyncthingService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.logs"
|
||||
|
||||
@@ -8,7 +8,6 @@ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,max-page-size=16384
|
||||
|
||||
add_subdirectory(adrenotools)
|
||||
|
||||
# DXVK's DXBC->SPIR-V translator is zlib licensed (vendored under thirdparty/dxbc).
|
||||
add_subdirectory(thirdparty/dxbc)
|
||||
|
||||
set(DDFG_UPSTREAM "v0.3.0")
|
||||
@@ -37,13 +36,14 @@ target_link_libraries(sdnative log)
|
||||
add_library(deviceinfo SHARED deviceinfo/vkinfo.c)
|
||||
target_link_libraries(deviceinfo dl)
|
||||
|
||||
add_library(gpuclock SHARED gpuclock/kgsl_clock.c gpuclock/kgsl_busy.c)
|
||||
|
||||
add_library(termux SHARED termux/termux.c)
|
||||
|
||||
add_library(ddvenc SHARED venc/venc_server.cpp)
|
||||
target_compile_features(ddvenc PRIVATE cxx_std_17)
|
||||
target_link_libraries(ddvenc mediandk log)
|
||||
|
||||
# The optional proprietary SDK is supplied locally; its headers/libraries are not vendored.
|
||||
if(DROIDDECK_VITURE_SDK_DIR)
|
||||
add_library(droiddeck_viture SHARED viture/sdk_bridge.cpp)
|
||||
target_compile_features(droiddeck_viture PRIVATE cxx_std_17)
|
||||
|
||||
@@ -62,6 +62,7 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz
|
||||
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);
|
||||
LINE("max_push_constants=%u\n", p.limits.maxPushConstantsSize);
|
||||
|
||||
uint32_t extCount = 0;
|
||||
if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL);
|
||||
@@ -91,6 +92,7 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz
|
||||
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");
|
||||
LINE("shader_int64=%s\n", f.shaderInt64 ? "yes" : "no");
|
||||
}
|
||||
if (vkGetPhysicalDeviceMemoryProperties) {
|
||||
VkPhysicalDeviceMemoryProperties m;
|
||||
|
||||
@@ -78,7 +78,6 @@ static constexpr uint8_t GAMEPAD_AXIS_COUNT = 8;
|
||||
static constexpr uint8_t GAMEPAD_BUTTON_COUNT = 11;
|
||||
static constexpr uint32_t FAKE_INPUT_RING_MAGIC = 0x46494252;
|
||||
static constexpr uint32_t FAKE_INPUT_RING_VERSION = 2;
|
||||
// Rings hold the 64-bit input_event layout that the arm64 writers use; 32-bit guests convert on read.
|
||||
struct RingEvent {
|
||||
int64_t sec;
|
||||
int64_t usec;
|
||||
@@ -391,7 +390,6 @@ send_vibration(int strong, int weak, uint16_t duration_ms, uint16_t slot) {
|
||||
syscall(SYS_close, sock);
|
||||
}
|
||||
|
||||
// Must match RumbleComponent.HOLD_MS: play until the slot's next effect or stop.
|
||||
static constexpr uint16_t RUMBLE_HOLD_MS = 0xffff;
|
||||
|
||||
__attribute__((visibility("hidden"))) static void send_ff_effect(const struct ff_effect &effect, uint16_t slot) {
|
||||
@@ -1243,7 +1241,6 @@ static constexpr int TRITON_REPORT_ID_BYTES = 1;
|
||||
static constexpr int TRITON_STATE_BYTES = 45;
|
||||
static constexpr int TRITON_REPORT_INTERVAL_US = 4000;
|
||||
static constexpr int TRITON_FEATURE_TIMEOUT_MS = 500;
|
||||
// The app clears output_length once it has taken a report; a writer waits this long for that before overwriting.
|
||||
static constexpr int TRITON_OUTPUT_TIMEOUT_MS = 50;
|
||||
static constexpr const char *TRITON_NAME = "Steam Controller";
|
||||
static constexpr const char *TRITON_SERIAL = "DROIDDECKSC01";
|
||||
@@ -2673,9 +2670,6 @@ __attribute__((visibility("hidden"))) static int triton_get_feature(DeckHidraw &
|
||||
static constexpr uint32_t TRITON_HWID = 72;
|
||||
static constexpr uint32_t TRITON_FW_TIMESTAMP = 0x6AC686B3;
|
||||
|
||||
// Steam compares the controller's firmware with TRITON_FW_TS in its bundled hardwareupdater.cfg (and blocks
|
||||
// with a required update below MUST_UPDATE_TRITON_FW_TS), so the emulated pad reports at least the build
|
||||
// Steam ships.
|
||||
static uint32_t triton_fw_timestamp() {
|
||||
static const uint32_t timestamp = [] {
|
||||
std::vector<std::string> roots;
|
||||
@@ -2959,15 +2953,19 @@ EXPORT DIR *opendir(const char *name) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
static bool open_has_mode(int flags) {
|
||||
return (flags & O_CREAT) != 0 || (flags & O_TMPFILE) == O_TMPFILE;
|
||||
}
|
||||
|
||||
EXPORT int open(const char *pathname, int flags, ...) {
|
||||
va_list va;
|
||||
mode_t mode;
|
||||
mode_t mode = 0;
|
||||
int fd;
|
||||
bool hasMode;
|
||||
|
||||
va_start(va, flags);
|
||||
|
||||
hasMode = flags & O_CREAT;
|
||||
hasMode = open_has_mode(flags);
|
||||
|
||||
if (hasMode) {
|
||||
mode = va_arg(va, mode_t);
|
||||
@@ -3039,13 +3037,13 @@ EXPORT int open(const char *pathname, int flags, ...) {
|
||||
|
||||
EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
|
||||
va_list va;
|
||||
mode_t mode;
|
||||
mode_t mode = 0;
|
||||
int fd;
|
||||
bool hasMode;
|
||||
|
||||
va_start(va, flags);
|
||||
|
||||
hasMode = flags & O_CREAT;
|
||||
hasMode = open_has_mode(flags);
|
||||
|
||||
if (hasMode) {
|
||||
mode = va_arg(va, mode_t);
|
||||
@@ -3679,7 +3677,6 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) {
|
||||
Logger::log("Hooking ioctl EVIOCGRAB for event %s\n", event);
|
||||
return 0;
|
||||
} else if (type == 0x6A && !strncmp(event, "event", 5)) {
|
||||
// Older SDL2 builds treat any JSIOCGNAME answer as a joydev node and skip EVIOCGID, then drop the pad as 0000:0000.
|
||||
Logger::log("Rejecting joydev ioctl %d for event %s\n", number, event);
|
||||
guard.unlock();
|
||||
errno = EINVAL;
|
||||
@@ -4258,7 +4255,7 @@ EXPORT int select(int nfds, fd_set *readfds, fd_set *writefds,
|
||||
|
||||
#ifdef __GLIBC__
|
||||
static mode_t open_mode(int flags, va_list va) {
|
||||
return (flags & (O_CREAT | O_TMPFILE)) ? va_arg(va, mode_t) : 0;
|
||||
return open_has_mode(flags) ? va_arg(va, mode_t) : 0;
|
||||
}
|
||||
|
||||
EXPORT int open64(const char *pathname, int flags, ...) {
|
||||
|
||||
@@ -267,8 +267,6 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
|
||||
pFlowReg_, flowRegA_, flowRegB_, gmSsbo_, kLevels, cfg_);
|
||||
}
|
||||
|
||||
// Every generation of one real frame is recorded into the same command buffer, so the flow
|
||||
// between prev and curr is computed once and each generation gets its own synth UBO and set.
|
||||
if (reuseFlow && !g_scratch.flowReady) return;
|
||||
if (!reuseFlow) {
|
||||
bool gmEngaged = updateGlobalMotion(gmSlot, extent_);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS).
|
||||
// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
|
||||
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
|
||||
|
||||
#include "lsfg_dll.h"
|
||||
#include "lsfg_dxbc.h"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS).
|
||||
// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
|
||||
// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#pragma once
|
||||
// DXVK's dxbc subset is zlib licensed; vendored under cpp/thirdparty/dxbc.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// Whole-GPU busy share without root on Adreno (KGSL) devices whose kgsl sysfs is blocked for apps: a thread
|
||||
// samples the memstore timestamps (kgsl_busy.h) once a millisecond while running. Ringbuffer entries are used where
|
||||
// the kernel writes them; on hardware-scheduled kernels, which leave them at 0, the contexts' own entries are used.
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../adrenotools/src/hook/kgsl.h"
|
||||
#include "kgsl_busy.h"
|
||||
|
||||
#define SAMPLE_NS 1000000L
|
||||
// Context mode: how many samples between scans of the context entries.
|
||||
#define SCAN_EVERY 200
|
||||
|
||||
enum { MODE_RINGBUFFER = 1, MODE_CONTEXTS = 2 };
|
||||
|
||||
struct reader {
|
||||
int fd;
|
||||
void *map;
|
||||
size_t bytes;
|
||||
int first;
|
||||
int mode;
|
||||
uint32_t last[KGSL_BUSY_SLOTS];
|
||||
uint32_t *seen, *ctx_last;
|
||||
int live[KGSL_BUSY_MAX_LIVE];
|
||||
int live_count;
|
||||
pthread_mutex_t lock;
|
||||
pthread_t thread;
|
||||
int thread_on;
|
||||
atomic_int run;
|
||||
atomic_uint_fast64_t busy, samples, moves;
|
||||
};
|
||||
|
||||
static void *sample_loop(void *arg) {
|
||||
struct reader *r = arg;
|
||||
const struct timespec gap = {0, SAMPLE_NS};
|
||||
int until_scan = 0;
|
||||
while (atomic_load(&r->run)) {
|
||||
int moved = 0, busy;
|
||||
if (r->mode == MODE_CONTEXTS) {
|
||||
if (until_scan-- <= 0) {
|
||||
r->live_count = kgsl_busy_scan_contexts(r->map, r->first, r->seen, r->live, KGSL_BUSY_MAX_LIVE);
|
||||
until_scan = SCAN_EVERY;
|
||||
}
|
||||
busy = kgsl_busy_sample_contexts(r->map, r->live, r->live_count, r->ctx_last, &moved);
|
||||
} else {
|
||||
busy = kgsl_busy_sample(r->map, r->first, r->last, &moved);
|
||||
}
|
||||
if (busy) atomic_fetch_add(&r->busy, 1);
|
||||
if (moved) atomic_fetch_add(&r->moves, 1);
|
||||
atomic_fetch_add(&r->samples, 1);
|
||||
nanosleep(&gap, NULL);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void reader_run(struct reader *r, int on) {
|
||||
pthread_mutex_lock(&r->lock);
|
||||
if (on && !r->thread_on) {
|
||||
atomic_store(&r->run, 1);
|
||||
if (pthread_create(&r->thread, NULL, sample_loop, r) == 0) r->thread_on = 1;
|
||||
else atomic_store(&r->run, 0);
|
||||
} else if (!on && r->thread_on) {
|
||||
atomic_store(&r->run, 0);
|
||||
pthread_join(r->thread, NULL);
|
||||
r->thread_on = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&r->lock);
|
||||
}
|
||||
|
||||
static void reader_close(struct reader *r) {
|
||||
if (!r) return;
|
||||
reader_run(r, 0);
|
||||
if (r->map && r->map != MAP_FAILED) munmap(r->map, r->bytes);
|
||||
if (r->fd >= 0) close(r->fd);
|
||||
free(r->seen);
|
||||
free(r->ctx_last);
|
||||
pthread_mutex_destroy(&r->lock);
|
||||
free(r);
|
||||
}
|
||||
|
||||
// Returns 0 or a negative errno; *out is set on success.
|
||||
static int reader_open(struct reader **out) {
|
||||
struct reader *r = calloc(1, sizeof(*r));
|
||||
if (!r) return -ENOMEM;
|
||||
pthread_mutex_init(&r->lock, NULL);
|
||||
r->fd = open("/dev/kgsl-3d0", O_RDWR | O_CLOEXEC);
|
||||
if (r->fd < 0) { int e = -errno; reader_close(r); return e; }
|
||||
struct kgsl_shadowprop shadow;
|
||||
memset(&shadow, 0, sizeof(shadow));
|
||||
struct kgsl_device_getproperty prop = {.type = KGSL_PROP_DEVICE_SHADOW, .value = &shadow, .sizebytes = sizeof(shadow)};
|
||||
if (ioctl(r->fd, IOCTL_KGSL_DEVICE_GETPROPERTY, &prop)) { int e = -errno; reader_close(r); return e; }
|
||||
r->bytes = shadow.size;
|
||||
r->first = kgsl_busy_first_slot(shadow.size);
|
||||
if (r->first < 0) { reader_close(r); return -ERANGE; }
|
||||
r->map = mmap(NULL, r->bytes, PROT_READ, MAP_SHARED, r->fd, (off_t)shadow.gpuaddr);
|
||||
if (r->map == MAP_FAILED) { int e = -errno; reader_close(r); return e; }
|
||||
int moved = 0, started = 0;
|
||||
kgsl_busy_sample(r->map, r->first, r->last, &moved);
|
||||
for (int i = 0; i < KGSL_BUSY_SLOTS; i++) started |= r->last[i] != 0;
|
||||
if (started) {
|
||||
r->mode = MODE_RINGBUFFER;
|
||||
} else {
|
||||
r->seen = calloc((size_t)r->first, sizeof(uint32_t));
|
||||
r->ctx_last = calloc((size_t)r->first, sizeof(uint32_t));
|
||||
if (!r->seen || !r->ctx_last) { reader_close(r); return -ENOMEM; }
|
||||
r->live_count = kgsl_busy_scan_contexts(r->map, r->first, r->seen, r->live, KGSL_BUSY_MAX_LIVE);
|
||||
kgsl_busy_sample_contexts(r->map, r->live, r->live_count, r->ctx_last, &moved);
|
||||
r->mode = MODE_CONTEXTS;
|
||||
}
|
||||
*out = r;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifndef KGSL_BUSY_NO_JNI
|
||||
#include <jni.h>
|
||||
|
||||
JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeOpen(JNIEnv *env, jclass cls, jintArray error) {
|
||||
struct reader *r = NULL;
|
||||
int e = reader_open(&r);
|
||||
if (e && error && (*env)->GetArrayLength(env, error) > 0) {
|
||||
jint v = e;
|
||||
(*env)->SetIntArrayRegion(env, error, 0, 1, &v);
|
||||
}
|
||||
return e ? 0 : (jlong)(intptr_t)r;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeMode(JNIEnv *env, jclass cls, jlong handle) {
|
||||
return handle ? ((struct reader *)(intptr_t)handle)->mode : 0;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeRun(JNIEnv *env, jclass cls, jlong handle, jboolean on) {
|
||||
if (handle) reader_run((struct reader *)(intptr_t)handle, on ? 1 : 0);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeRead(JNIEnv *env, jclass cls, jlong handle, jlongArray out) {
|
||||
if (!handle || !out || (*env)->GetArrayLength(env, out) < 3) return JNI_FALSE;
|
||||
struct reader *r = (struct reader *)(intptr_t)handle;
|
||||
jlong v[3] = {(jlong)atomic_load(&r->busy), (jlong)atomic_load(&r->samples), (jlong)atomic_load(&r->moves)};
|
||||
(*env)->SetLongArrayRegion(env, out, 0, 3, v);
|
||||
return JNI_TRUE;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeClose(JNIEnv *env, jclass cls, jlong handle) {
|
||||
reader_close((struct reader *)(intptr_t)handle);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,77 @@
|
||||
#ifndef DROIDDECK_KGSL_BUSY_H
|
||||
#define DROIDDECK_KGSL_BUSY_H
|
||||
// kgsl lets any client map its memstore read-only (KGSL_PROP_DEVICE_SHADOW). The CP writes a ringbuffer's
|
||||
// start-of-pipe timestamp there before each submission's command buffers and its end-of-pipe timestamp after them
|
||||
// (adreno_gen7_ringbuffer.c gen7_ringbuffer_addcmds), so start != end means that ringbuffer has work on the GPU.
|
||||
// Ringbuffer entries start at KGSL_MEMSTORE_MAX: entries - 2 - 4 levels, or entries - 1 - 4 on kernels without the
|
||||
// LPAC slot. Entries - 6 .. entries - 2 cover both; a context entry in that range follows the same start/end rule.
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define KGSL_BUSY_ENTRY_WORDS 10
|
||||
#define KGSL_BUSY_SOP 0
|
||||
#define KGSL_BUSY_EOP 2
|
||||
#define KGSL_BUSY_SLOTS 5
|
||||
|
||||
static inline int kgsl_busy_first_slot(size_t bytes) {
|
||||
size_t entries = bytes / (KGSL_BUSY_ENTRY_WORDS * sizeof(uint32_t));
|
||||
return entries < 8 ? -1 : (int)(entries - 6);
|
||||
}
|
||||
|
||||
// 1 when a watched slot has a submission between its start and end timestamps. *moved is set when a start
|
||||
// timestamp differs from last[], which then holds the new ones.
|
||||
static inline int kgsl_busy_sample(const volatile uint32_t *memstore, int first, uint32_t last[KGSL_BUSY_SLOTS], int *moved) {
|
||||
int busy = 0;
|
||||
for (int i = 0; i < KGSL_BUSY_SLOTS; i++) {
|
||||
const volatile uint32_t *entry = memstore + (size_t)(first + i) * KGSL_BUSY_ENTRY_WORDS;
|
||||
uint32_t sop = entry[KGSL_BUSY_SOP], eop = entry[KGSL_BUSY_EOP];
|
||||
if (sop != eop) busy = 1;
|
||||
if (sop != last[i]) {
|
||||
*moved = 1;
|
||||
last[i] = sop;
|
||||
}
|
||||
}
|
||||
return busy;
|
||||
}
|
||||
|
||||
// Hardware-scheduled kernels (every gen8 GPU and some gen7 ones: .gpudev = *_hwsched_gpudev in adreno-gpulist.h)
|
||||
// leave the ringbuffer entries at 0. The GMU firmware writes each context's own entry instead
|
||||
// (gen8_hwsched_hfi.c send_context_pointers). Context ids run from 1 below the ringbuffer entries, lowest free first,
|
||||
// and kgsl zeroes an entry when its context is created or destroyed.
|
||||
#define KGSL_BUSY_MAX_LIVE 64
|
||||
|
||||
// Collects the contexts to sample: every entry below first with a non-zero start stamp (kgsl zeroes an entry when its
|
||||
// context is created or destroyed). Entries whose start stamp changed since the previous scan (seen[]) or that are
|
||||
// busy now come first, so the max_live cap only drops idle ones. Returns how many ids went into live[].
|
||||
static inline int kgsl_busy_scan_contexts(const volatile uint32_t *memstore, int first, uint32_t *seen, int *live, int max_live) {
|
||||
int n = 0, idle = 0;
|
||||
int idle_ids[KGSL_BUSY_MAX_LIVE];
|
||||
for (int id = 1; id < first; id++) {
|
||||
const volatile uint32_t *entry = memstore + (size_t)id * KGSL_BUSY_ENTRY_WORDS;
|
||||
uint32_t sop = entry[KGSL_BUSY_SOP], eop = entry[KGSL_BUSY_EOP];
|
||||
int changed = sop != seen[id];
|
||||
seen[id] = sop;
|
||||
if ((changed || sop != eop) && n < max_live) live[n++] = id;
|
||||
else if (sop != 0 && idle < KGSL_BUSY_MAX_LIVE) idle_ids[idle++] = id;
|
||||
}
|
||||
for (int i = 0; i < idle && n < max_live; i++) live[n++] = idle_ids[i];
|
||||
return n;
|
||||
}
|
||||
|
||||
// 1 when a live context has a submission between its start and end timestamps. *moved is set when a start
|
||||
// timestamp differs from last[] (indexed by context id), which then holds the new ones.
|
||||
static inline int kgsl_busy_sample_contexts(const volatile uint32_t *memstore, const int *live, int count, uint32_t *last, int *moved) {
|
||||
int busy = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
const volatile uint32_t *entry = memstore + (size_t)live[i] * KGSL_BUSY_ENTRY_WORDS;
|
||||
uint32_t sop = entry[KGSL_BUSY_SOP], eop = entry[KGSL_BUSY_EOP];
|
||||
if (sop != eop) busy = 1;
|
||||
if (sop != last[live[i]]) {
|
||||
*moved = 1;
|
||||
last[live[i]] = sop;
|
||||
}
|
||||
}
|
||||
return busy;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,159 @@
|
||||
// GPU clock without root on Adreno (KGSL) devices whose kgsl sysfs is blocked for apps.
|
||||
//
|
||||
// kgsl lets any app reserve a performance counter (IOCTL_KGSL_PERFCOUNTER_GET) but refuses to read it back while
|
||||
// /sys/class/kgsl/kgsl-3d0/perfcounter is 0, and clears the counters whenever the GPU switches to another process.
|
||||
// So the counter is read by the GPU itself: a small command buffer copies CP_ALWAYS_ON_COUNTER (19.2 MHz) and the
|
||||
// RBBM ALWAYS_COUNT counter (GPU core cycles) to memory, runs a block of NOP packets, and copies both again.
|
||||
// clock = cycles / always-on ticks * 19.2 MHz. Both reads sit in one command buffer, so a counter clear between
|
||||
// them only happens if the buffer is preempted; such samples fail the range checks and are dropped.
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../adrenotools/src/hook/kgsl.h"
|
||||
#include "kgsl_layout.h"
|
||||
|
||||
#define CP_TYPE7_PKT 0x70000000u
|
||||
#define CP_NOP 0x10u
|
||||
#define CP_REG_TO_MEM 0x3eu
|
||||
#define XO_HZ 19200000.0
|
||||
#define NOP_DWORDS 0x3fffu
|
||||
#define NOP_BLOCKS 4
|
||||
|
||||
struct sampler {
|
||||
int fd;
|
||||
const struct kgsl_layout *layout;
|
||||
unsigned context;
|
||||
unsigned counter_reg;
|
||||
struct kgsl_gpumem_alloc_id ib, out;
|
||||
uint32_t *ib_map;
|
||||
volatile uint64_t *out_map;
|
||||
size_t ib_bytes;
|
||||
};
|
||||
|
||||
static unsigned odd_parity(unsigned v) {
|
||||
v ^= v >> 16;
|
||||
v ^= v >> 8;
|
||||
v ^= v >> 4;
|
||||
v &= 0xf;
|
||||
return (~0x6996u >> v) & 1u;
|
||||
}
|
||||
|
||||
static uint32_t pkt7(unsigned op, unsigned count) {
|
||||
return CP_TYPE7_PKT | count | odd_parity(count) << 15 | (op & 0x7f) << 16 | odd_parity(op) << 23;
|
||||
}
|
||||
|
||||
static void sampler_close(struct sampler *s) {
|
||||
if (!s) return;
|
||||
if (s->fd >= 0) {
|
||||
if (s->ib_map && s->ib_map != MAP_FAILED) munmap(s->ib_map, s->ib.mmapsize);
|
||||
if (s->out_map && s->out_map != MAP_FAILED) munmap((void *)s->out_map, s->out.mmapsize);
|
||||
if (s->ib.id) { struct kgsl_gpumem_free_id f = {.id = s->ib.id}; ioctl(s->fd, IOCTL_KGSL_GPUMEM_FREE_ID, &f); }
|
||||
if (s->out.id) { struct kgsl_gpumem_free_id f = {.id = s->out.id}; ioctl(s->fd, IOCTL_KGSL_GPUMEM_FREE_ID, &f); }
|
||||
if (s->context) { struct kgsl_drawctxt_destroy d = {.drawctxt_id = s->context}; ioctl(s->fd, IOCTL_KGSL_DRAWCTXT_DESTROY, &d); }
|
||||
if (s->counter_reg) {
|
||||
struct kgsl_perfcounter_put p = {.groupid = KGSL_PERFCOUNTER_GROUP_RBBM, .countable = s->layout->cycles};
|
||||
ioctl(s->fd, IOCTL_KGSL_PERFCOUNTER_PUT, &p);
|
||||
}
|
||||
close(s->fd);
|
||||
}
|
||||
free(s);
|
||||
}
|
||||
|
||||
static void emit_reads(struct sampler *s, size_t *n, unsigned pass) {
|
||||
const unsigned regs[2] = {s->layout->always_on, s->counter_reg};
|
||||
for (unsigned k = 0; k < 2; k++) {
|
||||
uint64_t dst = s->out.gpuaddr + (uint64_t)(pass * 2 + k) * sizeof(uint64_t);
|
||||
s->ib_map[(*n)++] = pkt7(CP_REG_TO_MEM, 3);
|
||||
s->ib_map[(*n)++] = regs[k] | (2u << 18) | (1u << 30); // two dwords, 64-bit read
|
||||
s->ib_map[(*n)++] = (uint32_t)dst;
|
||||
s->ib_map[(*n)++] = (uint32_t)(dst >> 32);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns 0 or a negative errno; *out is set on success.
|
||||
static int sampler_open(struct sampler **out) {
|
||||
struct sampler *s = calloc(1, sizeof(*s));
|
||||
if (!s) return -ENOMEM;
|
||||
s->fd = open("/dev/kgsl-3d0", O_RDWR | O_CLOEXEC);
|
||||
if (s->fd < 0) { int e = -errno; s->fd = -1; sampler_close(s); return e; }
|
||||
struct kgsl_devinfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
struct kgsl_device_getproperty prop = {.type = KGSL_PROP_DEVICE_INFO, .value = &info, .sizebytes = sizeof(info)};
|
||||
if (ioctl(s->fd, IOCTL_KGSL_DEVICE_GETPROPERTY, &prop)) { int e = -errno; sampler_close(s); return e; }
|
||||
s->layout = kgsl_layout_for(info.chip_id);
|
||||
if (!s->layout) { sampler_close(s); return -ENOTSUP; }
|
||||
struct kgsl_perfcounter_get get = {.groupid = KGSL_PERFCOUNTER_GROUP_RBBM, .countable = s->layout->cycles};
|
||||
if (ioctl(s->fd, IOCTL_KGSL_PERFCOUNTER_GET, &get)) { int e = -errno; sampler_close(s); return e; }
|
||||
s->counter_reg = get.offset;
|
||||
if (!get.offset || get.offset > s->layout->readable_last) { sampler_close(s); return -ERANGE; }
|
||||
struct kgsl_drawctxt_create ctx = {.flags = KGSL_CONTEXT_NO_GMEM_ALLOC | KGSL_CONTEXT_PREAMBLE};
|
||||
if (ioctl(s->fd, IOCTL_KGSL_DRAWCTXT_CREATE, &ctx)) { int e = -errno; sampler_close(s); return e; }
|
||||
s->context = ctx.drawctxt_id;
|
||||
s->ib_bytes = (16 + NOP_BLOCKS * (NOP_DWORDS + 1)) * sizeof(uint32_t);
|
||||
s->ib.size = (s->ib_bytes + 4095) & ~(size_t)4095;
|
||||
s->out.size = 4096;
|
||||
if (ioctl(s->fd, IOCTL_KGSL_GPUMEM_ALLOC_ID, &s->ib)) { int e = -errno; s->ib.id = 0; sampler_close(s); return e; }
|
||||
if (ioctl(s->fd, IOCTL_KGSL_GPUMEM_ALLOC_ID, &s->out)) { int e = -errno; s->out.id = 0; sampler_close(s); return e; }
|
||||
s->ib_map = mmap(NULL, s->ib.mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, s->fd, (off_t)s->ib.id << 12);
|
||||
s->out_map = mmap(NULL, s->out.mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, s->fd, (off_t)s->out.id << 12);
|
||||
if (s->ib_map == MAP_FAILED || s->out_map == MAP_FAILED) { int e = -errno; sampler_close(s); return e; }
|
||||
size_t n = 0;
|
||||
emit_reads(s, &n, 0);
|
||||
for (unsigned b = 0; b < NOP_BLOCKS; b++) {
|
||||
s->ib_map[n++] = pkt7(CP_NOP, NOP_DWORDS);
|
||||
memset(&s->ib_map[n], 0, NOP_DWORDS * sizeof(uint32_t));
|
||||
n += NOP_DWORDS;
|
||||
}
|
||||
emit_reads(s, &n, 1);
|
||||
s->ib_bytes = n * sizeof(uint32_t);
|
||||
*out = s;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// MHz, or a negative value: -errno from kgsl, or -1000 for a sample that failed the range checks.
|
||||
static int sampler_mhz(struct sampler *s) {
|
||||
for (int i = 0; i < 4; i++) s->out_map[i] = 0;
|
||||
struct kgsl_command_object cmd = {.gpuaddr = s->ib.gpuaddr, .size = s->ib_bytes, .flags = KGSL_CMDLIST_IB};
|
||||
struct kgsl_gpu_command submit = {
|
||||
.cmdlist = (uint64_t)(uintptr_t)&cmd, .cmdsize = sizeof(cmd), .numcmds = 1, .context_id = s->context,
|
||||
};
|
||||
if (ioctl(s->fd, IOCTL_KGSL_GPU_COMMAND, &submit)) return -errno;
|
||||
struct kgsl_device_waittimestamp_ctxtid wait = {.context_id = s->context, .timestamp = submit.timestamp, .timeout = 250};
|
||||
if (ioctl(s->fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait)) return -errno;
|
||||
uint64_t xo0 = s->out_map[0], cyc0 = s->out_map[1], xo1 = s->out_map[2], cyc1 = s->out_map[3];
|
||||
if (xo1 <= xo0 || cyc1 <= cyc0) return -1000;
|
||||
uint64_t xo = xo1 - xo0, cycles = cyc1 - cyc0;
|
||||
// 5 us .. 50 ms of always-on time, and a clock between 50 MHz and 2 GHz.
|
||||
if (xo < 96 || xo > 960000) return -1000;
|
||||
double mhz = (double)cycles / (double)xo * XO_HZ / 1e6;
|
||||
if (mhz < 50 || mhz > 2000) return -1000;
|
||||
return (int)(mhz + 0.5);
|
||||
}
|
||||
|
||||
#ifndef KGSL_CLOCK_NO_JNI
|
||||
#include <jni.h>
|
||||
|
||||
JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_session_KgslClock_nativeOpen(JNIEnv *env, jclass cls, jintArray error) {
|
||||
struct sampler *s = NULL;
|
||||
int r = sampler_open(&s);
|
||||
if (r && error && (*env)->GetArrayLength(env, error) > 0) {
|
||||
jint e = r;
|
||||
(*env)->SetIntArrayRegion(env, error, 0, 1, &e);
|
||||
}
|
||||
return r ? 0 : (jlong)(intptr_t)s;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_session_KgslClock_nativeSample(JNIEnv *env, jclass cls, jlong handle) {
|
||||
return handle ? sampler_mhz((struct sampler *)(intptr_t)handle) : -EBADF;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_session_KgslClock_nativeClose(JNIEnv *env, jclass cls, jlong handle) {
|
||||
sampler_close((struct sampler *)(intptr_t)handle);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef DROIDDECK_KGSL_LAYOUT_H
|
||||
#define DROIDDECK_KGSL_LAYOUT_H
|
||||
#include <stddef.h>
|
||||
|
||||
// Register layout per GPU generation. A counter kgsl places outside the range the kernel's CP protection lets
|
||||
// command buffers read (adreno-gpulist.h *_protected_regs) is not read.
|
||||
struct kgsl_layout {
|
||||
unsigned always_on; // CP_ALWAYS_ON_COUNTER
|
||||
unsigned cycles; // RBBM countable that counts GPU cycles
|
||||
unsigned readable_last; // last register of the readable range starting at 0
|
||||
};
|
||||
// A6xx/A7xx: CP_ALWAYS_ON_COUNTER 0x980, RBBM countable 0 = ALWAYS_COUNT, 0x000-0x4ff readable (gen7 list).
|
||||
static const struct kgsl_layout KGSL_LAYOUT_A7XX = {0x980u, 0u, 0x4ffu};
|
||||
// A8xx: CP_ALWAYS_ON_COUNTER 0x8e7 (gen8_reg.h), RBBM countable 1 = US_ALWAYS_COUNT (0 is NEVER_COUNT, Mesa
|
||||
// a8xx_perfcntrs.xml), 0x000-0x3a3 readable (gen8_0_0_protected_regs).
|
||||
static const struct kgsl_layout KGSL_LAYOUT_A8XX = {0x8e7u, 1u, 0x3a3u};
|
||||
|
||||
// The KGSL chip id's top byte: 0x44 on every A8xx (Adreno 810/829/830/840, Mesa freedreno_devices.py),
|
||||
// 0x43 or the generation number on A6xx/A7xx. Later generations are not known, so nothing is read there.
|
||||
static inline const struct kgsl_layout *kgsl_layout_for(unsigned chip_id) {
|
||||
unsigned core = chip_id >> 24;
|
||||
if (core == 0x44u) return &KGSL_LAYOUT_A8XX;
|
||||
if (core > 0x44u) return NULL;
|
||||
return &KGSL_LAYOUT_A7XX;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -76,7 +76,6 @@ public:
|
||||
}
|
||||
lastFrame_ = frame.header.frame_id;
|
||||
if (state == XrGpuFrameState::Stale) {
|
||||
// No GPU read was submitted, so the producer can safely reclaim a dropped frame.
|
||||
dropped_++;
|
||||
if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect();
|
||||
return false;
|
||||
@@ -88,7 +87,6 @@ public:
|
||||
glBindImageTexture(0, raw_, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
|
||||
glDispatchCompute((width_ + 7) / 8, (height_ + 7) / 8, 1);
|
||||
glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
||||
// The producer may overwrite this slot only after the native GPU has finished reading it.
|
||||
glFinish();
|
||||
if (glGetError() != GL_NO_ERROR) { rejected_++; Disconnect(); return false; }
|
||||
if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect();
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* SOCK_SEQPACKET between the guest's Remote Play hooks and the app: the emulated /dev/video-enc0 and
|
||||
* MediaCodec encoder, and remote touches. A config carries the buffer memfds (inputs first) as SCM_RIGHTS;
|
||||
* every frame gets one reply, touches get none. */
|
||||
|
||||
#define VENC_MAX_BUFFERS 16
|
||||
|
||||
enum venc_msg { VENC_CONFIG = 1, VENC_FRAME = 2, VENC_DONE = 3, VENC_BITRATE = 4, VENC_TOUCH = 5, VDEC_CONFIG = 6, VDEC_CAPTURE = 7, VDEC_PACKET = 8, VDEC_RESULT = 9 };
|
||||
@@ -45,7 +41,6 @@ struct venc_bitrate {
|
||||
uint32_t bitrate;
|
||||
};
|
||||
|
||||
/* Remote Play's touch-as-mouse turned back into a touch; x and y are fractions of the screen in 1/65535. */
|
||||
struct venc_touch {
|
||||
uint32_t type;
|
||||
uint32_t action;
|
||||
|
||||
@@ -33,8 +33,8 @@ struct Mapping {
|
||||
constexpr int32_t kColorFormatYuv420SemiPlanar = 21;
|
||||
constexpr uint32_t kBufferFlagKeyFrame = 1;
|
||||
constexpr uint32_t kBufferFlagCodecConfig = 2;
|
||||
constexpr int kTouchWidth = 1920, kTouchHeight = 1080; // InputSpace.kt
|
||||
constexpr int kRemoteTouchId = 31; // the compositor's last touch slot (MAX_TOUCH_POINTS 32)
|
||||
constexpr int kTouchWidth = 1920, kTouchHeight = 1080;
|
||||
constexpr int kRemoteTouchId = 31;
|
||||
|
||||
int32_t avc_profile(int32_t v4l2) {
|
||||
switch (v4l2) {
|
||||
@@ -549,7 +549,7 @@ void serve(int listener) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_com_droiddeck_launcher_session_VideoEncodeBridge_nativeStart(JNIEnv *env, jobject, jstring jpath) {
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
// Our controller uses the standard display-mode vocabulary, even for Beast.
|
||||
// Native mode reuses some numbers with different meanings; never pass them through.
|
||||
namespace droiddeck::viture {
|
||||
constexpr int to_native_mode(int standard) {
|
||||
switch (standard) {
|
||||
case 0x31: return 0x31; // 1080p 60
|
||||
case 0x33: return 0x32; // 1080p 90
|
||||
case 0x34: return 0x33; // 1080p 120
|
||||
case 0x32: return 0x37; // 1080p SBS 60
|
||||
case 0x35: return 0x38; // 1080p SBS 90
|
||||
case 0x41: return 0x34; // 1200p 60
|
||||
case 0x43: return 0x35; // 1200p 90
|
||||
case 0x44: return 0x36; // 1200p 120
|
||||
case 0x42: return 0x3a; // 1200p SBS 60
|
||||
case 0x45: return 0x3b; // 1200p SBS 90
|
||||
case 0x31: return 0x31;
|
||||
case 0x33: return 0x32;
|
||||
case 0x34: return 0x33;
|
||||
case 0x32: return 0x37;
|
||||
case 0x35: return 0x38;
|
||||
case 0x41: return 0x34;
|
||||
case 0x43: return 0x35;
|
||||
case 0x44: return 0x36;
|
||||
case 0x42: return 0x3a;
|
||||
case 0x45: return 0x3b;
|
||||
default: return -4;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +28,7 @@ constexpr int from_native_mode(int native) {
|
||||
case 0x36: return 0x44;
|
||||
case 0x3a: return 0x42;
|
||||
case 0x3b: return 0x45;
|
||||
default: return -4; // Ultrawide is not stereo.
|
||||
default: return -4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
inline std::size_t mjpeg_payload_size(const uint8_t* bytes, std::size_t size) {
|
||||
if (!bytes || size < 4 || size > 4*1024*1024 || bytes[0] != 0xff || bytes[1] != 0xd8) return 0;
|
||||
std::size_t end = size;
|
||||
// Luma Ultra UVC frames can pad the JPEG to an eight-byte boundary with zeros.
|
||||
while (end > 4 && size-end < 7 && bytes[end-1] == 0) --end;
|
||||
return bytes[end-2] == 0xff && bytes[end-1] == 0xd9 ? end : 0;
|
||||
}
|
||||
@@ -30,7 +30,6 @@
|
||||
#include "stereo_frame_gate.h"
|
||||
|
||||
namespace {
|
||||
// SDK callbacks have no user-data parameter. Only one provider may exist at a time.
|
||||
std::mutex api_mutex;
|
||||
std::mutex pose_mutex;
|
||||
std::array<float, 4> latest_pose{};
|
||||
@@ -64,7 +63,6 @@ void on_pose(float* data, uint64_t) {
|
||||
std::lock_guard<std::mutex> lock(pose_mutex);
|
||||
if (!accepting_pose) return;
|
||||
std::copy_n(data + 3, 4, latest_pose.begin());
|
||||
// SDK device ticks are not Android's elapsed-realtime clock.
|
||||
latest_time = now_ns();
|
||||
}
|
||||
|
||||
@@ -234,8 +232,6 @@ int set_display(Session& s, int standard) {
|
||||
if (mode == 0) {
|
||||
int rc = xr_device_provider_set_display_mode(s.device, standard);
|
||||
if (rc != VITURE_GLASSES_ERROR_NOT_SUPPORTED) return rc;
|
||||
// Older Beast firmware exposes fewer bypass modes. Fall back to its
|
||||
// separate native table only when the SDK explicitly rejects the mode.
|
||||
}
|
||||
const int native = droiddeck::viture::to_native_mode(standard);
|
||||
if (native < 0) return native;
|
||||
|
||||
@@ -11,6 +11,7 @@ add_library(droiddeckwayland SHARED
|
||||
src/gpu_clock_pin.c
|
||||
src/droiddeck_ext.c
|
||||
src/wl_dmabuf.c
|
||||
src/wl_android_buffer.c
|
||||
src/wl_clipboard.c
|
||||
src/wl_text_input.c
|
||||
src/wl_toplevel_icon.c
|
||||
@@ -18,9 +19,11 @@ add_library(droiddeckwayland SHARED
|
||||
src/wl_color_mgmt.c
|
||||
src/hdr_compose.c
|
||||
src/blend_pass.c
|
||||
src/gpu_busy.c
|
||||
generated/color-management-v1-protocol.c
|
||||
generated/xdg-shell-protocol.c
|
||||
generated/linux-dmabuf-v1-protocol.c
|
||||
generated/wayland-android-protocol.c
|
||||
generated/viewporter-protocol.c
|
||||
generated/banner-desktop-v1-protocol.c
|
||||
generated/presentation-time-protocol.c
|
||||
@@ -43,6 +46,7 @@ target_include_directories(droiddeckwayland PRIVATE
|
||||
prebuilt/include
|
||||
src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../shaders
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../../../../tools/gpuload/src
|
||||
)
|
||||
|
||||
set(DROIDDECK_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/* Generated by wayland-scanner 1.26.0 */
|
||||
|
||||
/*
|
||||
* Copyright © 2012 Collabora, Ltd.
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this
|
||||
* software and its documentation for any purpose is hereby granted
|
||||
* without fee, provided that the above copyright notice appear in
|
||||
* all copies and that both that copyright notice and this permission
|
||||
* notice appear in supporting documentation, and that the name of
|
||||
* the copyright holders not be used in advertising or publicity
|
||||
* pertaining to distribution of the software without specific,
|
||||
* written prior permission. The copyright holders make no
|
||||
* representations about the suitability of this software for any
|
||||
* purpose. It is provided "as is" without express or implied
|
||||
* warranty.
|
||||
*
|
||||
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
* THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include "wayland-util.h"
|
||||
|
||||
#ifndef __has_attribute
|
||||
# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
|
||||
#endif
|
||||
|
||||
#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
|
||||
#define WL_PRIVATE __attribute__ ((visibility("hidden")))
|
||||
#else
|
||||
#define WL_PRIVATE
|
||||
#endif
|
||||
|
||||
extern const struct wl_interface android_wlegl_handle_interface;
|
||||
extern const struct wl_interface android_wlegl_server_buffer_handle_interface;
|
||||
extern const struct wl_interface wl_buffer_interface;
|
||||
|
||||
static const struct wl_interface *android_types[] = {
|
||||
NULL,
|
||||
NULL,
|
||||
&android_wlegl_handle_interface,
|
||||
NULL,
|
||||
NULL,
|
||||
&wl_buffer_interface,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
&android_wlegl_handle_interface,
|
||||
&android_wlegl_server_buffer_handle_interface,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
&wl_buffer_interface,
|
||||
NULL,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const struct wl_message android_wlegl_requests[] = {
|
||||
{ "create_handle", "nia", android_types + 2 },
|
||||
{ "create_buffer", "niiiiio", android_types + 5 },
|
||||
{ "get_server_buffer_handle", "2niiii", android_types + 12 },
|
||||
};
|
||||
|
||||
WL_PRIVATE const struct wl_interface android_wlegl_interface = {
|
||||
"android_wlegl", 3,
|
||||
3, android_wlegl_requests,
|
||||
0, NULL,
|
||||
};
|
||||
|
||||
static const struct wl_message android_wlegl_handle_requests[] = {
|
||||
{ "add_fd", "h", android_types + 0 },
|
||||
{ "destroy", "", android_types + 0 },
|
||||
};
|
||||
|
||||
WL_PRIVATE const struct wl_interface android_wlegl_handle_interface = {
|
||||
"android_wlegl_handle", 1,
|
||||
2, android_wlegl_handle_requests,
|
||||
0, NULL,
|
||||
};
|
||||
|
||||
static const struct wl_message android_wlegl_server_buffer_handle_events[] = {
|
||||
{ "buffer_fd", "h", android_types + 0 },
|
||||
{ "buffer_ints", "a", android_types + 0 },
|
||||
{ "buffer", "nii", android_types + 17 },
|
||||
{ "linear_layout", "3uu", android_types + 0 },
|
||||
};
|
||||
|
||||
WL_PRIVATE const struct wl_interface android_wlegl_server_buffer_handle_interface = {
|
||||
"android_wlegl_server_buffer_handle", 3,
|
||||
0, NULL,
|
||||
4, android_wlegl_server_buffer_handle_events,
|
||||
};
|
||||
@@ -0,0 +1,341 @@
|
||||
/* Generated by wayland-scanner 1.26.0 */
|
||||
|
||||
#ifndef ANDROID_SERVER_PROTOCOL_H
|
||||
#define ANDROID_SERVER_PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "wayland-server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct wl_client;
|
||||
struct wl_resource;
|
||||
|
||||
/**
|
||||
* @page page_android The android protocol
|
||||
* @section page_ifaces_android Interfaces
|
||||
* - @subpage page_iface_android_wlegl - Android EGL graphics buffer support
|
||||
* - @subpage page_iface_android_wlegl_handle - An Android native_handle_t object
|
||||
* - @subpage page_iface_android_wlegl_server_buffer_handle - a server allocated buffer
|
||||
* @section page_copyright_android Copyright
|
||||
* <pre>
|
||||
*
|
||||
* Copyright © 2012 Collabora, Ltd.
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this
|
||||
* software and its documentation for any purpose is hereby granted
|
||||
* without fee, provided that the above copyright notice appear in
|
||||
* all copies and that both that copyright notice and this permission
|
||||
* notice appear in supporting documentation, and that the name of
|
||||
* the copyright holders not be used in advertising or publicity
|
||||
* pertaining to distribution of the software without specific,
|
||||
* written prior permission. The copyright holders make no
|
||||
* representations about the suitability of this software for any
|
||||
* purpose. It is provided "as is" without express or implied
|
||||
* warranty.
|
||||
*
|
||||
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
* THIS SOFTWARE.
|
||||
* </pre>
|
||||
*/
|
||||
struct android_wlegl;
|
||||
struct android_wlegl_handle;
|
||||
struct android_wlegl_server_buffer_handle;
|
||||
struct wl_buffer;
|
||||
|
||||
#ifndef ANDROID_WLEGL_INTERFACE
|
||||
#define ANDROID_WLEGL_INTERFACE
|
||||
/**
|
||||
* @page page_iface_android_wlegl android_wlegl
|
||||
* @section page_iface_android_wlegl_desc Description
|
||||
*
|
||||
* Interface used in the Android wrapper libEGL to share
|
||||
* graphics buffers between the server and the client.
|
||||
* @section page_iface_android_wlegl_api API
|
||||
* See @ref iface_android_wlegl.
|
||||
*/
|
||||
/**
|
||||
* @defgroup iface_android_wlegl The android_wlegl interface
|
||||
*
|
||||
* Interface used in the Android wrapper libEGL to share
|
||||
* graphics buffers between the server and the client.
|
||||
*/
|
||||
extern const struct wl_interface android_wlegl_interface;
|
||||
#endif
|
||||
#ifndef ANDROID_WLEGL_HANDLE_INTERFACE
|
||||
#define ANDROID_WLEGL_HANDLE_INTERFACE
|
||||
/**
|
||||
* @page page_iface_android_wlegl_handle android_wlegl_handle
|
||||
* @section page_iface_android_wlegl_handle_desc Description
|
||||
*
|
||||
* The Android native_handle_t is a semi-opaque object, that
|
||||
* contains an EGL implementation specific number of int32
|
||||
* values and file descriptors.
|
||||
*
|
||||
* We cannot send a variable size array of file descriptors
|
||||
* over the Wayland protocol, so we send them one by one.
|
||||
* @section page_iface_android_wlegl_handle_api API
|
||||
* See @ref iface_android_wlegl_handle.
|
||||
*/
|
||||
/**
|
||||
* @defgroup iface_android_wlegl_handle The android_wlegl_handle interface
|
||||
*
|
||||
* The Android native_handle_t is a semi-opaque object, that
|
||||
* contains an EGL implementation specific number of int32
|
||||
* values and file descriptors.
|
||||
*
|
||||
* We cannot send a variable size array of file descriptors
|
||||
* over the Wayland protocol, so we send them one by one.
|
||||
*/
|
||||
extern const struct wl_interface android_wlegl_handle_interface;
|
||||
#endif
|
||||
#ifndef ANDROID_WLEGL_SERVER_BUFFER_HANDLE_INTERFACE
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_INTERFACE
|
||||
/**
|
||||
* @page page_iface_android_wlegl_server_buffer_handle android_wlegl_server_buffer_handle
|
||||
* @section page_iface_android_wlegl_server_buffer_handle_desc Description
|
||||
*
|
||||
* On creation a server_side_buffer object will immediately send
|
||||
* the "buffer_fd" and "buffer_ints" events needed by the client to
|
||||
* reference the gralloc buffer, followed by the "buffer" event carrying
|
||||
* the wl_buffer object.
|
||||
* @section page_iface_android_wlegl_server_buffer_handle_api API
|
||||
* See @ref iface_android_wlegl_server_buffer_handle.
|
||||
*/
|
||||
/**
|
||||
* @defgroup iface_android_wlegl_server_buffer_handle The android_wlegl_server_buffer_handle interface
|
||||
*
|
||||
* On creation a server_side_buffer object will immediately send
|
||||
* the "buffer_fd" and "buffer_ints" events needed by the client to
|
||||
* reference the gralloc buffer, followed by the "buffer" event carrying
|
||||
* the wl_buffer object.
|
||||
*/
|
||||
extern const struct wl_interface android_wlegl_server_buffer_handle_interface;
|
||||
#endif
|
||||
|
||||
#ifndef ANDROID_WLEGL_ERROR_ENUM
|
||||
#define ANDROID_WLEGL_ERROR_ENUM
|
||||
enum android_wlegl_error {
|
||||
ANDROID_WLEGL_ERROR_BAD_HANDLE = 0,
|
||||
ANDROID_WLEGL_ERROR_BAD_VALUE = 1,
|
||||
};
|
||||
#endif /* ANDROID_WLEGL_ERROR_ENUM */
|
||||
|
||||
#ifndef ANDROID_WLEGL_ERROR_ENUM_IS_VALID
|
||||
#define ANDROID_WLEGL_ERROR_ENUM_IS_VALID
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
* Validate a android_wlegl error value.
|
||||
*
|
||||
* @return true on success, false on error.
|
||||
* @ref android_wlegl_error
|
||||
*/
|
||||
static inline bool
|
||||
android_wlegl_error_is_valid(uint32_t value, uint32_t version) {
|
||||
switch (value) {
|
||||
case ANDROID_WLEGL_ERROR_BAD_HANDLE:
|
||||
return version >= 1;
|
||||
case ANDROID_WLEGL_ERROR_BAD_VALUE:
|
||||
return version >= 1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif /* ANDROID_WLEGL_ERROR_ENUM_IS_VALID */
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
* @struct android_wlegl_interface
|
||||
*/
|
||||
struct android_wlegl_interface {
|
||||
/**
|
||||
* Create an Android native_handle_t object
|
||||
*
|
||||
* This creator method initialises the native_handle_t object
|
||||
* with everything except the file descriptors, which have to be
|
||||
* submitted separately.
|
||||
* @param ints an array of int32_t
|
||||
*/
|
||||
void (*create_handle)(struct wl_client *client,
|
||||
struct wl_resource *resource,
|
||||
uint32_t id,
|
||||
int32_t num_fds,
|
||||
struct wl_array *ints);
|
||||
/**
|
||||
* Create a wl_buffer from the native handle
|
||||
*
|
||||
* Pass the Android native_handle_t to the server and attach it
|
||||
* to the new wl_buffer object.
|
||||
*
|
||||
* The android_wlegl_handle object must be destroyed immediately
|
||||
* after this request.
|
||||
*/
|
||||
void (*create_buffer)(struct wl_client *client,
|
||||
struct wl_resource *resource,
|
||||
uint32_t id,
|
||||
int32_t width,
|
||||
int32_t height,
|
||||
int32_t stride,
|
||||
int32_t format,
|
||||
int32_t usage,
|
||||
struct wl_resource *native_handle);
|
||||
/**
|
||||
* @since 2
|
||||
*/
|
||||
void (*get_server_buffer_handle)(struct wl_client *client,
|
||||
struct wl_resource *resource,
|
||||
uint32_t id,
|
||||
int32_t width,
|
||||
int32_t height,
|
||||
int32_t format,
|
||||
int32_t usage);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
*/
|
||||
#define ANDROID_WLEGL_CREATE_HANDLE_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
*/
|
||||
#define ANDROID_WLEGL_CREATE_BUFFER_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
*/
|
||||
#define ANDROID_WLEGL_GET_SERVER_BUFFER_HANDLE_SINCE_VERSION 2
|
||||
|
||||
#ifndef ANDROID_WLEGL_HANDLE_ERROR_ENUM
|
||||
#define ANDROID_WLEGL_HANDLE_ERROR_ENUM
|
||||
enum android_wlegl_handle_error {
|
||||
ANDROID_WLEGL_HANDLE_ERROR_TOO_MANY_FDS = 0,
|
||||
};
|
||||
#endif /* ANDROID_WLEGL_HANDLE_ERROR_ENUM */
|
||||
|
||||
#ifndef ANDROID_WLEGL_HANDLE_ERROR_ENUM_IS_VALID
|
||||
#define ANDROID_WLEGL_HANDLE_ERROR_ENUM_IS_VALID
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_handle
|
||||
* Validate a android_wlegl_handle error value.
|
||||
*
|
||||
* @return true on success, false on error.
|
||||
* @ref android_wlegl_handle_error
|
||||
*/
|
||||
static inline bool
|
||||
android_wlegl_handle_error_is_valid(uint32_t value, uint32_t version) {
|
||||
switch (value) {
|
||||
case ANDROID_WLEGL_HANDLE_ERROR_TOO_MANY_FDS:
|
||||
return version >= 1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif /* ANDROID_WLEGL_HANDLE_ERROR_ENUM_IS_VALID */
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_handle
|
||||
* @struct android_wlegl_handle_interface
|
||||
*/
|
||||
struct android_wlegl_handle_interface {
|
||||
/**
|
||||
*/
|
||||
void (*add_fd)(struct wl_client *client,
|
||||
struct wl_resource *resource,
|
||||
int32_t fd);
|
||||
/**
|
||||
*/
|
||||
void (*destroy)(struct wl_client *client,
|
||||
struct wl_resource *resource);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_HANDLE_ADD_FD_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_HANDLE_DESTROY_SINCE_VERSION 1
|
||||
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_FD 0
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_INTS 1
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER 2
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_LINEAR_LAYOUT 3
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_FD_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_INTS_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_LINEAR_LAYOUT_SINCE_VERSION 3
|
||||
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
* Sends an buffer_fd event to the client owning the resource.
|
||||
* @param resource_ The client's resource
|
||||
*/
|
||||
static inline void
|
||||
android_wlegl_server_buffer_handle_send_buffer_fd(struct wl_resource *resource_, int32_t fd)
|
||||
{
|
||||
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_FD, fd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
* Sends an buffer_ints event to the client owning the resource.
|
||||
* @param resource_ The client's resource
|
||||
*/
|
||||
static inline void
|
||||
android_wlegl_server_buffer_handle_send_buffer_ints(struct wl_resource *resource_, struct wl_array *ints)
|
||||
{
|
||||
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_INTS, ints);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
* Sends an buffer event to the client owning the resource.
|
||||
* @param resource_ The client's resource
|
||||
*/
|
||||
static inline void
|
||||
android_wlegl_server_buffer_handle_send_buffer(struct wl_resource *resource_, struct wl_resource *buffer, int32_t format, int32_t stride)
|
||||
{
|
||||
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER, buffer, format, stride);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
* Sends an linear_layout event to the client owning the resource.
|
||||
* @param resource_ The client's resource
|
||||
*/
|
||||
static inline void
|
||||
android_wlegl_server_buffer_handle_send_linear_layout(struct wl_resource *resource_, uint32_t drm_format, uint32_t stride)
|
||||
{
|
||||
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_LINEAR_LAYOUT, drm_format, stride);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="android">
|
||||
|
||||
<copyright>
|
||||
Copyright © 2012 Collabora, Ltd.
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this
|
||||
software and its documentation for any purpose is hereby granted
|
||||
without fee, provided that the above copyright notice appear in
|
||||
all copies and that both that copyright notice and this permission
|
||||
notice appear in supporting documentation, and that the name of
|
||||
the copyright holders not be used in advertising or publicity
|
||||
pertaining to distribution of the software without specific,
|
||||
written prior permission. The copyright holders make no
|
||||
representations about the suitability of this software for any
|
||||
purpose. It is provided "as is" without express or implied
|
||||
warranty.
|
||||
|
||||
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<interface name="android_wlegl" version="3">
|
||||
<description summary="Android EGL graphics buffer support">
|
||||
Interface used in the Android wrapper libEGL to share
|
||||
graphics buffers between the server and the client.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="bad_handle" value="0" />
|
||||
<entry name="bad_value" value="1" />
|
||||
</enum>
|
||||
|
||||
<request name="create_handle">
|
||||
<description summary="Create an Android native_handle_t object">
|
||||
This creator method initialises the native_handle_t object
|
||||
with everything except the file descriptors, which have to be
|
||||
submitted separately.
|
||||
</description>
|
||||
|
||||
<arg name="id" type="new_id" interface="android_wlegl_handle" />
|
||||
<arg name="num_fds" type="int" />
|
||||
<arg name="ints" type="array" summary="an array of int32_t" />
|
||||
</request>
|
||||
|
||||
<request name="create_buffer">
|
||||
<description summary="Create a wl_buffer from the native handle">
|
||||
Pass the Android native_handle_t to the server and attach it
|
||||
to the new wl_buffer object.
|
||||
|
||||
The android_wlegl_handle object must be destroyed immediately
|
||||
after this request.
|
||||
</description>
|
||||
|
||||
<arg name="id" type="new_id" interface="wl_buffer" />
|
||||
<arg name="width" type="int" />
|
||||
<arg name="height" type="int" />
|
||||
<arg name="stride" type="int" />
|
||||
<arg name="format" type="int" />
|
||||
<arg name="usage" type="int" />
|
||||
<arg name="native_handle" type="object" interface="android_wlegl_handle" />
|
||||
</request>
|
||||
|
||||
|
||||
<request name="get_server_buffer_handle" since="2">
|
||||
<arg name="id" type="new_id" interface="android_wlegl_server_buffer_handle"/>
|
||||
<arg name="width" type="int"/>
|
||||
<arg name="height" type="int"/>
|
||||
<arg name="format" type="int"/>
|
||||
<arg name="usage" type="int"/>
|
||||
</request>
|
||||
|
||||
</interface>
|
||||
|
||||
<interface name="android_wlegl_handle" version="1">
|
||||
<description summary="An Android native_handle_t object">
|
||||
The Android native_handle_t is a semi-opaque object, that
|
||||
contains an EGL implementation specific number of int32
|
||||
values and file descriptors.
|
||||
|
||||
We cannot send a variable size array of file descriptors
|
||||
over the Wayland protocol, so we send them one by one.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="too_many_fds" value="0" />
|
||||
</enum>
|
||||
|
||||
<request name="add_fd">
|
||||
<arg name="fd" type="fd" />
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor" />
|
||||
|
||||
</interface>
|
||||
|
||||
|
||||
<interface name="android_wlegl_server_buffer_handle" version="3">
|
||||
<description summary="a server allocated buffer">
|
||||
On creation a server_side_buffer object will immediately send
|
||||
the "buffer_fd" and "buffer_ints" events needed by the client to
|
||||
reference the gralloc buffer, followed by the "buffer" event carrying
|
||||
the wl_buffer object.
|
||||
</description>
|
||||
<event name="buffer_fd">
|
||||
<arg name="fd" type="fd"/>
|
||||
</event>
|
||||
<event name="buffer_ints">
|
||||
<arg name="ints" type="array"/>
|
||||
</event>
|
||||
<event name="buffer">
|
||||
<description summary="the wl_buffer">
|
||||
This event will be sent after the ints and all the fds have been sent
|
||||
</description>
|
||||
<arg name="buffer" type="new_id" interface="wl_buffer"/>
|
||||
<arg name="format" type="int"/>
|
||||
<arg name="stride" type="int"/>
|
||||
</event>
|
||||
<event name="linear_layout" since="3">
|
||||
<description summary="verified single-plane linear allocation">
|
||||
Optional metadata sent before the buffer event. The first buffer_fd
|
||||
contains one uncompressed 32-bit RGB plane at byte offset zero.
|
||||
drm_format is its DRM FourCC and stride is its row pitch in bytes.
|
||||
The server has verified the layout and allocation bounds using
|
||||
Android mapper metadata, not inferred them from opaque handle ints.
|
||||
Absence of this event means the layout is unspecified.
|
||||
</description>
|
||||
<arg name="drm_format" type="uint"/>
|
||||
<arg name="stride" type="uint"/>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
</protocol>
|
||||
@@ -79,6 +79,9 @@ static struct ahb_buf *find_buf(uint64_t id) {
|
||||
}
|
||||
|
||||
static void buf_free(struct ahb_buf *ab) {
|
||||
/* A display release does not prove the compositor's Vulkan reads finished.
|
||||
* Keep the AHB reference and its DMA-BUF record even if the client is gone. */
|
||||
if (vkp_completion_unknown()) return;
|
||||
if (ab->resource) { wl_list_remove(&ab->resource_destroy.link); ab->resource = NULL; }
|
||||
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; }
|
||||
@@ -103,6 +106,7 @@ static void on_buffer_resource_destroyed(struct wl_listener *l, void *data) {
|
||||
|
||||
|
||||
static void send_deferred_release(struct ahb_buf *ab) {
|
||||
if (vkp_completion_unknown()) return;
|
||||
if (ab->release_pending && ab->resource)
|
||||
droiddeck_release_buffer(ab->surface, ab->resource, ab->surface != NULL, ab->deferred_ns);
|
||||
ab->release_pending = 0;
|
||||
|
||||
@@ -34,7 +34,6 @@ void cardboard_clamp(struct cardboard_params *p) {
|
||||
p->screen_width_m = clampf(p->screen_width_m, 0.2f, 60.0f);
|
||||
p->k1 = clampf(p->k1, -1.0f, 2.0f);
|
||||
p->k2 = clampf(p->k2, -1.0f, 2.0f);
|
||||
// Optical vertical FOV on supported glasses can be as low as ~22 degrees.
|
||||
p->fov_deg = clampf(p->fov_deg, 20.0f, 140.0f);
|
||||
p->lens_inset = clampf(p->lens_inset, -0.25f, 0.25f);
|
||||
p->chroma = clampf(p->chroma, 0.0f, 0.05f);
|
||||
@@ -186,6 +185,13 @@ int vkp_cardboard_active(void) {
|
||||
return on;
|
||||
}
|
||||
|
||||
int vkp_cardboard_source(void) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
const int source = g_set.enabled ? g_set.source : CARDBOARD_SOURCE_SCREEN;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return source;
|
||||
}
|
||||
|
||||
int vkp_cardboard_pose_changed(void) {
|
||||
if (!g_changed) return 0;
|
||||
g_changed = 0;
|
||||
|
||||
@@ -27,6 +27,7 @@ void vkp_cardboard_set_render_height(int height);
|
||||
|
||||
void vkp_cardboard_bind_device(VkDevice dev, const VkPhysicalDeviceMemoryProperties *memprops);
|
||||
int vkp_cardboard_active(void);
|
||||
int vkp_cardboard_source(void);
|
||||
int vkp_cardboard_pose_changed(void);
|
||||
void vkp_cardboard_begin_frame(void);
|
||||
VkImage vkp_cardboard_run(VkCommandBuffer cmd, VkImage src, int src_w, int src_h, int out_w, int out_h);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// 1115.1.0
|
||||
#pragma once
|
||||
const uint32_t cardboard_frag_code[] = {
|
||||
0x07230203,0x00010000,0x0008000b,0x000001ba,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
|
||||
@@ -154,6 +154,16 @@ static void on_client_destroyed(struct wl_listener *l, void *data) {
|
||||
free(ci);
|
||||
}
|
||||
|
||||
/* libwayland's own reasons for dropping a client (a failed read or flush, an invalid object, a
|
||||
* malformed request) go through wl_log, which on Android writes to a closed stderr. */
|
||||
static void wl_log_to_session(const char *fmt, va_list ap) {
|
||||
char line[512];
|
||||
vsnprintf(line, sizeof(line), fmt, ap);
|
||||
size_t n = strlen(line);
|
||||
while (n && (line[n - 1] == '\n' || line[n - 1] == '\r')) line[--n] = 0;
|
||||
droiddeck_log("program", "libwayland: %s", line);
|
||||
}
|
||||
|
||||
static void on_client_created(struct wl_listener *l, void *data) {
|
||||
struct wl_client *client = data;
|
||||
struct client_info *ci = calloc(1, sizeof(*ci));
|
||||
@@ -289,7 +299,6 @@ static struct wl_event_source *g_fallback_timer;
|
||||
static int g_fallback_armed;
|
||||
|
||||
volatile int g_fps_limit;
|
||||
/* fps limit -1 (phone output, not mirrored): replaced buffers go back to the client at once instead of once per refresh. */
|
||||
volatile int g_release_unpaced;
|
||||
|
||||
volatile int g_hide_shell;
|
||||
@@ -350,6 +359,62 @@ static void perf_note_release(int64_t since_ns) {
|
||||
}
|
||||
|
||||
|
||||
/* Completion-unknown is terminal for externally owned DMA-BUF contents too.
|
||||
* Keeping a VkImage alone cannot stop the client from overwriting its backing
|
||||
* memory after wl_buffer.release. All paths (including cursors and idle roles)
|
||||
* use this gate; copied SHM buffers retain their normal release behaviour. */
|
||||
static struct dmabuf_buffer *get_dmabuf(struct wl_resource *buffer);
|
||||
static int buffer_release_blocked(struct wl_resource *buffer) {
|
||||
return vkp_completion_unknown() && get_dmabuf(buffer) != NULL;
|
||||
}
|
||||
/* Buffer tracing on a GPU that is not an Adreno (no KGSL node): every attach and release of the
|
||||
* first 400 buffer events, by wl_buffer id and the line that sent it, and any release of a buffer
|
||||
* already released and not attached again since (gamescope: "Compositor released us but we were
|
||||
* not acquired"). Off on an Adreno. */
|
||||
static int g_buf_trace = -1;
|
||||
static int g_buf_trace_left = 400;
|
||||
static uint32_t g_released_ids[128];
|
||||
static int buf_trace_on(void) {
|
||||
if (g_buf_trace < 0) g_buf_trace = access("/dev/kgsl-3d0", F_OK) != 0;
|
||||
return g_buf_trace;
|
||||
}
|
||||
static void buf_trace_attach(struct wl_resource *buffer) {
|
||||
if (!buffer || !buf_trace_on()) return;
|
||||
uint32_t id = wl_resource_get_id(buffer);
|
||||
for (int i = 0; i < 128; i++) if (g_released_ids[i] == id) g_released_ids[i] = 0;
|
||||
if (g_buf_trace_left > 0) { g_buf_trace_left--; droiddeck_log("buffer", "attach id %u", id); }
|
||||
}
|
||||
static void buf_trace_release(struct wl_resource *buffer, int line) {
|
||||
if (!buffer || !buf_trace_on()) return;
|
||||
uint32_t id = wl_resource_get_id(buffer);
|
||||
int twice = 0, slot = -1;
|
||||
for (int i = 0; i < 128; i++) {
|
||||
if (g_released_ids[i] == id) twice = 1;
|
||||
if (slot < 0 && g_released_ids[i] == 0) slot = i;
|
||||
}
|
||||
if (twice) droiddeck_log("buffer", "DOUBLE release id %u (line %d): released again without an attach", id, line);
|
||||
else if (slot >= 0) g_released_ids[slot] = id;
|
||||
if (g_buf_trace_left > 0) { g_buf_trace_left--; droiddeck_log("buffer", "release id %u (line %d)", id, line); }
|
||||
}
|
||||
|
||||
#define send_buffer_release(b) send_buffer_release_at((b), __LINE__)
|
||||
static void send_buffer_release_at(struct wl_resource *buffer, int line) {
|
||||
if (!buffer_release_blocked(buffer)) { buf_trace_release(buffer, line); wl_buffer_send_release(buffer); }
|
||||
}
|
||||
|
||||
/* Whether a surface other than `except` is showing this buffer right now. gamescope's Wayland
|
||||
* backend moves one buffer between its planes (the game's frame goes from the window to a
|
||||
* subsurface above a black backing while a Steam notification is up): releasing it when the first
|
||||
* surface lets go handed gamescope a buffer still on screen ("Compositor released us but we were
|
||||
* not acquired"). Only the last surface to let go releases it. */
|
||||
static int buffer_shown_elsewhere(struct wl_resource *buffer, const struct surface *except) {
|
||||
struct surface *o;
|
||||
if (!buffer) return 0;
|
||||
wl_list_for_each(o, &g_surfaces, link)
|
||||
if (o != except && o->dmabuf == buffer) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void pending_release_free(struct pending_release *pr) {
|
||||
if (pr->surface && pr->surface->releases_pending > 0) pr->surface->releases_pending--;
|
||||
wl_list_remove(&pr->destroy.link);
|
||||
@@ -373,7 +438,8 @@ static void arm_release_timer(void) {
|
||||
int64_t earliest = 0;
|
||||
if (g_release_timer_fd < 0) return;
|
||||
wl_list_for_each(pr, &g_pending_releases, link)
|
||||
if (!earliest || pr->at_ns < earliest) earliest = pr->at_ns;
|
||||
if (!buffer_release_blocked(pr->buffer) && (!earliest || pr->at_ns < earliest))
|
||||
earliest = pr->at_ns;
|
||||
struct itimerspec its = {0};
|
||||
if (earliest) { its.it_value.tv_sec = earliest / 1000000000LL; its.it_value.tv_nsec = earliest % 1000000000LL; }
|
||||
timerfd_settime(g_release_timer_fd, TFD_TIMER_ABSTIME, &its, NULL);
|
||||
@@ -385,8 +451,13 @@ static int on_release_timer(int fd, uint32_t mask, void *data) {
|
||||
int64_t now = now_ns() + 300000;
|
||||
if (read(fd, &expirations, sizeof(expirations)) < 0 && errno != EAGAIN) return 0;
|
||||
wl_list_for_each_safe(pr, tmp, &g_pending_releases, link) {
|
||||
if (pr->at_ns <= now) {
|
||||
wl_buffer_send_release(pr->buffer);
|
||||
if (pr->at_ns <= now && !buffer_release_blocked(pr->buffer)) {
|
||||
/* Attached again since (to its surface or another): that surface releases it later. */
|
||||
if (buffer_shown_elsewhere(pr->buffer, NULL)) {
|
||||
pending_release_free(pr);
|
||||
continue;
|
||||
}
|
||||
send_buffer_release(pr->buffer);
|
||||
perf_note_release(pr->since_ns);
|
||||
pending_release_free(pr);
|
||||
}
|
||||
@@ -397,9 +468,12 @@ static int on_release_timer(int fd, uint32_t mask, void *data) {
|
||||
}
|
||||
|
||||
static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_t since_ns) {
|
||||
/* No allocation or immediate-release fallback is allowed for a quarantined
|
||||
* DMA-BUF. Existing queued releases stay held; their timer is disarmed. */
|
||||
if (buffer_release_blocked(buffer)) return;
|
||||
const int limit = g_fps_limit;
|
||||
const int64_t interval = limit > 0 ? 1000000000LL / limit : g_refresh_ns;
|
||||
if (g_release_unpaced || interval <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
|
||||
if (g_release_unpaced || interval <= 0 || g_release_timer_fd < 0) { send_buffer_release(buffer); perf_note_release(since_ns); return; }
|
||||
int64_t now = now_ns();
|
||||
int64_t at = s->next_release_ns + interval;
|
||||
int64_t latest = now + interval * (int64_t)(s->releases_pending + 1);
|
||||
@@ -407,7 +481,7 @@ static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_
|
||||
if (at > latest) at = latest;
|
||||
s->next_release_ns = at;
|
||||
struct pending_release *pr = calloc(1, sizeof(*pr));
|
||||
if (!pr) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
|
||||
if (!pr) { send_buffer_release(buffer); perf_note_release(since_ns); return; }
|
||||
pr->buffer = buffer;
|
||||
pr->surface = s;
|
||||
pr->at_ns = at;
|
||||
@@ -446,6 +520,12 @@ static int g_cursor_w, g_cursor_h;
|
||||
static int g_cursor_hidden = 1;
|
||||
static int g_cursor_serial;
|
||||
|
||||
#define CURSOR_MAX_DRAWS 2
|
||||
static struct vkp_image *g_cursor_img;
|
||||
static int g_cursor_img_w, g_cursor_img_h, g_cursor_img_serial = -1;
|
||||
static atomic_int g_cursor_composite;
|
||||
static atomic_int g_cursor_composite_want;
|
||||
|
||||
static void cursor_publish_shown(void) {
|
||||
pthread_mutex_lock(&g_cursor_lock);
|
||||
if (g_cursor_w > 0) { g_cursor_hidden = 0; g_cursor_serial++; }
|
||||
@@ -518,7 +598,10 @@ static void constraints_focus_entered(struct wl_resource *target, struct wl_clie
|
||||
|
||||
|
||||
void dmabuf_buffer_unref(struct dmabuf_buffer *b) {
|
||||
if (!b || --b->refs > 0) return;
|
||||
/* Surface/client destruction must not close the backing FDs or free the
|
||||
* imported-buffer record while a GPU read may still be pending. Retain the
|
||||
* reference without allocating a quarantine node; process exit reclaims it. */
|
||||
if (!b || vkp_completion_unknown() || --b->refs > 0) return;
|
||||
vkp_image_destroy(b->img);
|
||||
for (int i = 0; i < b->n_planes; i++)
|
||||
if (b->fd[i] >= 0) close(b->fd[i]);
|
||||
@@ -617,9 +700,10 @@ static void unmap_toplevel(struct surface *s) {
|
||||
static void drop_dmabuf(struct surface *s, int paced) {
|
||||
if (s->dmabuf) {
|
||||
wl_list_remove(&s->dmabuf_destroy.link);
|
||||
if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) {
|
||||
if (buffer_shown_elsewhere(s->dmabuf, s)) {
|
||||
} else if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) {
|
||||
if (paced) release_buffer(s, s->dmabuf, now_ns());
|
||||
else wl_buffer_send_release(s->dmabuf);
|
||||
else send_buffer_release(s->dmabuf);
|
||||
}
|
||||
s->dmabuf = NULL;
|
||||
}
|
||||
@@ -728,7 +812,7 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res
|
||||
vkp_image_upload_shm(s->shm_img, wl_shm_buffer_get_data(shm), stride);
|
||||
wl_shm_buffer_end_access(shm);
|
||||
}
|
||||
wl_buffer_send_release(buffer);
|
||||
send_buffer_release(buffer);
|
||||
s->buf_w = w;
|
||||
s->buf_h = h;
|
||||
s->buf_alpha = wl_shm_buffer_get_format(shm) == WL_SHM_FORMAT_ARGB8888;
|
||||
@@ -772,7 +856,7 @@ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resou
|
||||
}
|
||||
if (!s->shm_img) s->shm_img = vkp_image_create_shm(1, 1);
|
||||
if (s->shm_img) vkp_image_upload_shm(s->shm_img, bgra, 4);
|
||||
wl_buffer_send_release(buffer);
|
||||
send_buffer_release(buffer);
|
||||
s->buf_w = 1;
|
||||
s->buf_h = 1;
|
||||
s->buf_alpha = bgra[3] != 0xff;
|
||||
@@ -875,8 +959,9 @@ 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 (buffer_release_blocked(buffer)) return;
|
||||
if (paced && s) release_buffer(s, buffer, since_ns);
|
||||
else { wl_buffer_send_release(buffer); perf_note_release(since_ns); }
|
||||
else { send_buffer_release(buffer); perf_note_release(since_ns); }
|
||||
}
|
||||
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) {
|
||||
@@ -1096,7 +1181,7 @@ static void surface_hold_idle(struct surface *s, struct wl_resource *buffer) {
|
||||
struct wl_shm_buffer *shm = db ? NULL : wl_shm_buffer_get(buffer);
|
||||
int64_t px = db ? (int64_t)db->width * db->height
|
||||
: shm ? (int64_t)wl_shm_buffer_get_width(shm) * wl_shm_buffer_get_height(shm) : 0;
|
||||
if (px <= 0 || px > CURSOR_MAX_PX) { wl_buffer_send_release(buffer); return; }
|
||||
if (px <= 0 || px > CURSOR_MAX_PX) { send_buffer_release(buffer); return; }
|
||||
s->idle_buffer = buffer;
|
||||
s->idle_buffer_destroy.notify = on_idle_buffer_destroyed;
|
||||
wl_resource_add_destroy_listener(buffer, &s->idle_buffer_destroy);
|
||||
@@ -1114,7 +1199,7 @@ static struct wl_resource *surface_take_idle(struct surface *s) {
|
||||
static void surface_drop_idle(struct surface *s) {
|
||||
surface_forget_idle_px(s);
|
||||
struct wl_resource *buffer = surface_take_idle(s);
|
||||
if (buffer) wl_buffer_send_release(buffer);
|
||||
if (buffer) send_buffer_release(buffer);
|
||||
}
|
||||
|
||||
static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
@@ -1167,11 +1252,12 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
cursor_publish_hidden();
|
||||
} else {
|
||||
cursor_publish_buffer(s, buffer);
|
||||
wl_buffer_send_release(buffer);
|
||||
send_buffer_release(buffer);
|
||||
}
|
||||
} else if (s->role == ROLE_NONE) {
|
||||
if (buffer) surface_hold_idle(s, buffer);
|
||||
} else if (db) {
|
||||
buf_trace_attach(buffer);
|
||||
take_dmabuf(s, db, buffer);
|
||||
} else if (shm) {
|
||||
drop_dmabuf(s, 1);
|
||||
@@ -1182,7 +1268,7 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
} else {
|
||||
drop_dmabuf(s, 1);
|
||||
s->has_content = 0;
|
||||
if (buffer) wl_buffer_send_release(buffer);
|
||||
if (buffer) send_buffer_release(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1736,6 +1822,8 @@ static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t
|
||||
|
||||
struct draw_list { struct vkp_draw *d; int n, cap; };
|
||||
|
||||
static int add_cursor(struct draw_list *dl, int scene_w);
|
||||
|
||||
static struct vkp_image *surface_image(struct surface *s) {
|
||||
if (!s->has_content) return NULL;
|
||||
if (s->shm_img) return s->shm_img;
|
||||
@@ -1909,7 +1997,7 @@ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int fra
|
||||
}
|
||||
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;
|
||||
if (dl->n <= 0 || !(hs->is_hdr = calloc((size_t)dl->n + CURSOR_MAX_DRAWS, 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;
|
||||
@@ -2011,12 +2099,6 @@ static void hdr_count_copied(const struct draw_list *dl, const char *reason) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Stream mode (the Xbox Cloud Gaming native player, in its own process): no Wayland clients, the scene is one
|
||||
* picture handed over as AHardwareBuffers (ImageReader images of the decoded video). Each buffer is imported once
|
||||
* and kept while the reader cycles through its few buffers; the newest frame is the scene, so the screen effects
|
||||
* and frame generation apply to it as to a game.
|
||||
*/
|
||||
#define STREAM_SLOTS 6
|
||||
static pthread_mutex_t g_stream_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static atomic_int g_stream_on;
|
||||
@@ -2036,7 +2118,6 @@ static void stream_forget_all(void) {
|
||||
g_stream_img = NULL;
|
||||
}
|
||||
|
||||
/* Compositor thread: the newest handed-over buffer becomes the scene picture. */
|
||||
static void stream_take(void) {
|
||||
pthread_mutex_lock(&g_stream_lock);
|
||||
AHardwareBuffer *ahb = g_stream_pending;
|
||||
@@ -2050,7 +2131,7 @@ static void stream_take(void) {
|
||||
if (g_stream_slots[i].used < g_stream_slots[oldest].used) oldest = i;
|
||||
}
|
||||
if (slot >= 0) {
|
||||
AHardwareBuffer_release(ahb); /* the slot keeps its own reference */
|
||||
AHardwareBuffer_release(ahb);
|
||||
} else {
|
||||
struct vkp_image *img = vkp_image_from_ahb(ahb);
|
||||
if (!img) {
|
||||
@@ -2062,7 +2143,7 @@ static void stream_take(void) {
|
||||
droiddeck_log("stream", "decoded frame buffer imported (%dx%d)", vkp_image_width(img), vkp_image_height(img));
|
||||
slot = oldest;
|
||||
if (g_stream_slots[slot].img) {
|
||||
vkp_wait_idle(); /* the GPU may still read the picture being replaced */
|
||||
vkp_wait_idle();
|
||||
if (g_stream_img == g_stream_slots[slot].img) g_stream_img = NULL;
|
||||
vkp_image_destroy(g_stream_slots[slot].img);
|
||||
}
|
||||
@@ -2108,7 +2189,6 @@ void droiddeck_stream_set(int on) {
|
||||
(void)n;
|
||||
}
|
||||
|
||||
/* Any thread: hands over one decoded frame (acquires its own reference). */
|
||||
void droiddeck_stream_frame(AHardwareBuffer *ahb) {
|
||||
if (!ahb || !atomic_load(&g_stream_on) || g_input_pipe[1] < 0) return;
|
||||
AHardwareBuffer_acquire(ahb);
|
||||
@@ -2116,7 +2196,7 @@ void droiddeck_stream_frame(AHardwareBuffer *ahb) {
|
||||
AHardwareBuffer *dropped = g_stream_pending;
|
||||
g_stream_pending = ahb;
|
||||
pthread_mutex_unlock(&g_stream_lock);
|
||||
if (dropped) { AHardwareBuffer_release(dropped); return; } /* a wake-up is already queued */
|
||||
if (dropped) { AHardwareBuffer_release(dropped); return; }
|
||||
struct input_msg m = { 20, 0, 0, 0, 0 };
|
||||
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
|
||||
(void)n;
|
||||
@@ -2185,6 +2265,28 @@ static void render_scene(void) {
|
||||
hdr_note_precedence(hdr_route == 3 ? hs.who : NULL, fx_on, framegen);
|
||||
}
|
||||
if (hdr_route == 3) { fx_on = 0; framegen = 0; }
|
||||
struct surface *ahb_only = ahb_layer_only_candidate(w, h);
|
||||
struct surface *layer_only = ahb_only ? ahb_only : hdr_unimported_fullscreen(&dl, w, h);
|
||||
const int cursor_on = hdr_route == 3 || layer_only ? 0 : add_cursor(&dl, w);
|
||||
static int cursor_layer_said;
|
||||
if (layer_only && atomic_load(&g_cursor_composite) && !cursor_layer_said) {
|
||||
cursor_layer_said = 1;
|
||||
droiddeck_log("cursor", "the pointer stays out of the picture: the game's frames cannot be imported, "
|
||||
"so the game is shown on its own display layer");
|
||||
} else if (!layer_only) {
|
||||
cursor_layer_said = 0;
|
||||
}
|
||||
static int cursor_hdr_said;
|
||||
if (cursor_on && hdr_route == 0 && hs.n_hdr > 0 && !cardboard) {
|
||||
hdr_route = 2;
|
||||
if (!cursor_hdr_said) {
|
||||
cursor_hdr_said = 1;
|
||||
droiddeck_log("cursor", "the pointer needs a composed scene: the HDR picture goes through the screen "
|
||||
"swapchain instead of the game's own display layer");
|
||||
}
|
||||
} else {
|
||||
cursor_hdr_said = 0;
|
||||
}
|
||||
if (hdr_route == 1) {
|
||||
static int fell_back;
|
||||
vkp_apply_window_request();
|
||||
@@ -2210,21 +2312,19 @@ static void render_scene(void) {
|
||||
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 && vkp_quest_layer_presentation() && !framegen && !cardboard &&
|
||||
!ahb_layer_only_candidate(w, h)) {
|
||||
/* The complete SDR scene stays on one display layer. Do not hide/re-show GAME
|
||||
* on each desktop frame just because it is not a single fullscreen window. */
|
||||
if (hdr_route == 0 && vkp_quest_layer_presentation() && !framegen && !cardboard && !cursor_on &&
|
||||
!ahb_only) {
|
||||
rendered = vkp_render(w, h, dl.d, dl.n) == 0;
|
||||
} else if (hdr_route == 0 || hdr_route == 3) {
|
||||
const int pass_on = fx_on || framegen;
|
||||
int li = (g_zero_copy && !framegen && !cardboard) ? layer_candidate(&dl, w, h) : -1;
|
||||
int li = (g_zero_copy && !framegen && !cardboard && !cursor_on) ? layer_candidate(&dl, w, h) : -1;
|
||||
int over = li >= 0 ? dl.n - 1 - li : 0;
|
||||
const int fx_blocks = (li >= 0 && fx_on && over > 0);
|
||||
if (fx_blocks) { li = -1; over = 0; }
|
||||
const int ov_blocks = (li >= 0 && over > 0 && !fx_blocks && !sc_layer_overlay_affordable());
|
||||
if (ov_blocks) { li = -1; over = 0; }
|
||||
const int blocked = framegen ? 1 : (fx_blocks ? 2 : (ov_blocks ? 3 : 0));
|
||||
if (g_zero_copy && blocked != g_zero_copy_paused) {
|
||||
if (g_zero_copy && !cursor_on && blocked != g_zero_copy_paused) {
|
||||
g_zero_copy_paused = blocked;
|
||||
if (blocked == 1)
|
||||
droiddeck_log("framegen", "zero-copy paused: frame generation needs the compositor pass");
|
||||
@@ -2237,9 +2337,9 @@ static void render_scene(void) {
|
||||
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;
|
||||
if (li < 0) ls = ahb_layer_only_candidate(w, h);
|
||||
if (li < 0 && !ls) ls = hdr_unimported_fullscreen(&dl, w, h);
|
||||
if (li < 0) ls = layer_only;
|
||||
if (cardboard) ls = NULL;
|
||||
if (cursor_on) ls = NULL;
|
||||
if (ls && pass_on && li < 0 && !g_zero_copy_fx_skip_said) {
|
||||
g_zero_copy_fx_skip_said = 1;
|
||||
droiddeck_log(framegen ? "framegen" : "effects",
|
||||
@@ -2322,8 +2422,6 @@ static int on_fallback_timer(void *data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A new frame is drawn when it arrives instead of at the next screen tick, at most once per refresh,
|
||||
while frames go straight back to the game (no compositor limit), without streaming or frame generation. */
|
||||
static int commit_renders_on(void) {
|
||||
return g_render_on_commit && g_release_unpaced && !atomic_load(&g_stream_on) && !vkp_framegen_active();
|
||||
}
|
||||
@@ -2382,10 +2480,6 @@ static void on_vsync(int64_t frame_time_ns) {
|
||||
if (vkp_effects_sync()) tick_change = 1;
|
||||
if (vkp_cardboard_pose_changed()) tick_change = 1;
|
||||
if (tick_change) g_dirty = 1;
|
||||
/* A frame that arrived after this refresh was already drawn is left for the next arrival to draw;
|
||||
drawing it here would block that arrival too and keep every frame waiting for the tick. A client
|
||||
that draws again only after its frame callback sends no such arrival: once a held frame is still
|
||||
waiting at the next tick, the tick draws every frame for a second before holding one again. */
|
||||
if (g_tick_held && g_dirty) g_hold_missed_ns = now;
|
||||
g_tick_held = 0;
|
||||
if (!tick_change && commit_renders_on() && g_dirty_since_ns && now - g_dirty_since_ns < g_refresh_ns &&
|
||||
@@ -2409,6 +2503,7 @@ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint3
|
||||
if (g_cursor_surface) g_cursor_surface->role = ROLE_NONE;
|
||||
g_cursor_surface = NULL;
|
||||
cursor_publish_hidden();
|
||||
if (atomic_load(&g_cursor_composite)) schedule_render();
|
||||
return;
|
||||
}
|
||||
struct surface *s = wl_resource_get_user_data(surface);
|
||||
@@ -2416,18 +2511,21 @@ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint3
|
||||
if (s->role != ROLE_NONE && s->role != ROLE_CURSOR) return;
|
||||
s->role = ROLE_CURSOR;
|
||||
g_cursor_surface = s;
|
||||
pthread_mutex_lock(&g_cursor_lock);
|
||||
g_cursor_hx = hx;
|
||||
g_cursor_hy = hy;
|
||||
pthread_mutex_unlock(&g_cursor_lock);
|
||||
struct wl_resource *buffer = surface_take_idle(s);
|
||||
if (buffer) {
|
||||
cursor_publish_buffer(s, buffer);
|
||||
wl_buffer_send_release(buffer);
|
||||
send_buffer_release(buffer);
|
||||
} else if (g_cursor_shown == s) {
|
||||
cursor_publish_shown();
|
||||
} else if (s->idle_px) {
|
||||
cursor_publish_pixels((const uint8_t *)s->idle_px, s->idle_w, s->idle_h, (size_t)s->idle_w * 4, hx, hy);
|
||||
g_cursor_shown = s;
|
||||
}
|
||||
if (atomic_load(&g_cursor_composite)) schedule_render();
|
||||
}
|
||||
static void pointer_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
|
||||
static const struct wl_pointer_interface pointer_impl = {
|
||||
@@ -2609,6 +2707,97 @@ static int g_ptr_known;
|
||||
static double g_raw_x, g_raw_y;
|
||||
static int g_raw_valid;
|
||||
|
||||
static int pointer_locked_for_cursor(void) {
|
||||
return g_active_constraint && g_active_constraint->is_lock;
|
||||
}
|
||||
|
||||
static int cursor_scene_positions(int scene_w, int source, int ptr_x, int hx, int out[2]) {
|
||||
out[0] = ptr_x - hx; out[1] = -1;
|
||||
if (source != CARDBOARD_SOURCE_SBS || scene_w <= 1) return 1;
|
||||
const int half = scene_w / 2;
|
||||
out[1] = (ptr_x < half ? ptr_x + half : ptr_x - half) - hx;
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int cursor_clip_to_eye(int scene_w, int eye, int x0, int w, int *dx, int *sx, int *dw) {
|
||||
int left = x0, right = x0 + w;
|
||||
if (eye >= 0 && scene_w > 1) {
|
||||
const int half = scene_w / 2;
|
||||
const int band = eye ? half : 0;
|
||||
if (left < band) left = band;
|
||||
if (right > band + half) right = band + half;
|
||||
}
|
||||
if (right <= left) return 0;
|
||||
*dx = left;
|
||||
*sx = left - x0;
|
||||
*dw = right - left;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int add_cursor(struct draw_list *dl, int scene_w) {
|
||||
if (!atomic_load(&g_cursor_composite) || !g_ptr_known || pointer_locked_for_cursor()) return 0;
|
||||
|
||||
int hidden, w, h, hx, hy, serial;
|
||||
uint32_t *copy = NULL;
|
||||
pthread_mutex_lock(&g_cursor_lock);
|
||||
hidden = g_cursor_hidden;
|
||||
w = g_cursor_w; h = g_cursor_h;
|
||||
hx = g_cursor_hx; hy = g_cursor_hy;
|
||||
serial = g_cursor_serial;
|
||||
if (!hidden && w > 0 && h > 0 && serial != g_cursor_img_serial) {
|
||||
copy = malloc((size_t)w * (size_t)h * 4);
|
||||
if (copy) memcpy(copy, g_cursor_px, (size_t)w * (size_t)h * 4);
|
||||
}
|
||||
pthread_mutex_unlock(&g_cursor_lock);
|
||||
if (hidden || w <= 0 || h <= 0) return 0;
|
||||
|
||||
if (copy) {
|
||||
if (g_cursor_img && (g_cursor_img_w != w || g_cursor_img_h != h)) {
|
||||
vkp_wait_idle();
|
||||
vkp_image_destroy(g_cursor_img);
|
||||
g_cursor_img = NULL;
|
||||
}
|
||||
if (!g_cursor_img) g_cursor_img = vkp_image_create_shm(w, h);
|
||||
if (g_cursor_img) {
|
||||
g_cursor_img_w = w; g_cursor_img_h = h;
|
||||
vkp_image_upload_shm(g_cursor_img, copy, w * 4);
|
||||
g_cursor_img_serial = serial;
|
||||
}
|
||||
free(copy);
|
||||
}
|
||||
if (!g_cursor_img || g_cursor_img_serial != serial) return 0;
|
||||
|
||||
int xs[CURSOR_MAX_DRAWS];
|
||||
const int source = vkp_cardboard_source();
|
||||
const int ptr_x = (int)(g_ptr_x + 0.5);
|
||||
const int count = cursor_scene_positions(scene_w, source, ptr_x, hx, xs);
|
||||
static int said_w = -1, said_source = -1, said_count = -1;
|
||||
if (scene_w != said_w || source != said_source || count != said_count) {
|
||||
said_w = scene_w; said_source = source; said_count = count;
|
||||
droiddeck_log("cursor", "pointer drawn %d time(s) in a %d wide scene (%s picture)", count, scene_w,
|
||||
source == CARDBOARD_SOURCE_SBS ? "side-by-side" : "single");
|
||||
}
|
||||
if (dl->n + count > dl->cap) {
|
||||
int cap = dl->cap ? dl->cap : 32;
|
||||
while (cap < dl->n + count) cap *= 2;
|
||||
struct vkp_draw *d = realloc(dl->d, (size_t)cap * sizeof(*d));
|
||||
if (!d) return 0;
|
||||
dl->d = d; dl->cap = cap;
|
||||
}
|
||||
const int sbs = source == CARDBOARD_SOURCE_SBS && scene_w > 1;
|
||||
const int pointer_eye = ptr_x < scene_w / 2 ? 0 : 1;
|
||||
int appended = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int dx, sx, cw;
|
||||
const int eye = sbs ? (i ? 1 - pointer_eye : pointer_eye) : -1;
|
||||
if (!cursor_clip_to_eye(scene_w, eye, xs[i], w, &dx, &sx, &cw)) continue;
|
||||
dl->d[dl->n++] = (struct vkp_draw){g_cursor_img, (float)sx, 0, (float)cw, (float)h,
|
||||
dx, (int)(g_ptr_y + 0.5) - hy, cw, h, 1};
|
||||
appended++;
|
||||
}
|
||||
return appended ? 1 : 0;
|
||||
}
|
||||
|
||||
extern void droiddeck_on_pointer_lock(int locked, int x, int y);
|
||||
|
||||
static void sync_lock_notify(void) {
|
||||
@@ -2617,6 +2806,7 @@ static void sync_lock_notify(void) {
|
||||
if (locked == last) return;
|
||||
last = locked;
|
||||
droiddeck_on_pointer_lock(locked, (int)g_ptr_x, (int)g_ptr_y);
|
||||
if (atomic_load(&g_cursor_composite)) schedule_render();
|
||||
}
|
||||
|
||||
static void constraint_describe(const struct constraint *k, char *out, size_t size) {
|
||||
@@ -2696,7 +2886,10 @@ static void constraint_activate(struct constraint *k) {
|
||||
constraint_end(g_active_constraint, 1, "replaced by a newer request");
|
||||
k->active = 1;
|
||||
g_active_constraint = k;
|
||||
const double was_x = g_ptr_x, was_y = g_ptr_y;
|
||||
if (!k->is_lock) confine_clamp(k, &g_ptr_x, &g_ptr_y);
|
||||
if ((g_ptr_x != was_x || g_ptr_y != was_y) && atomic_load(&g_cursor_composite))
|
||||
schedule_render();
|
||||
constraint_origin(k, &ox, &oy);
|
||||
pointer_focus(k->surface->resource, wl_fixed_from_double(g_ptr_x - ox), wl_fixed_from_double(g_ptr_y - oy));
|
||||
int focused = 0;
|
||||
@@ -2755,6 +2948,7 @@ static void constraints_pointer_gone(struct wl_resource *pointer) {
|
||||
|
||||
static void constraints_surface_commit(struct surface *s) {
|
||||
struct constraint *k;
|
||||
const double was_x = g_ptr_x, was_y = g_ptr_y;
|
||||
wl_list_for_each(k, &g_constraints, link) {
|
||||
if (k->surface != s) continue;
|
||||
if (k->pending_hint_set) {
|
||||
@@ -2778,6 +2972,8 @@ static void constraints_surface_commit(struct surface *s) {
|
||||
if (k->active && !k->is_lock) confine_clamp(k, &g_ptr_x, &g_ptr_y);
|
||||
}
|
||||
}
|
||||
if ((g_ptr_x != was_x || g_ptr_y != was_y) && atomic_load(&g_cursor_composite))
|
||||
schedule_render();
|
||||
}
|
||||
|
||||
static void constraint_resource_destroy(struct wl_resource *r) {
|
||||
@@ -2963,6 +3159,7 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int
|
||||
if (k) confine_clamp(k, &x, &y);
|
||||
g_ptr_x = x; g_ptr_y = y;
|
||||
g_ptr_known = 1;
|
||||
if (atomic_load(&g_cursor_composite)) schedule_render();
|
||||
if (k) {
|
||||
target = k->surface;
|
||||
} else if (g_desktop) {
|
||||
@@ -2985,10 +3182,6 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int
|
||||
uint32_t t = now_ms();
|
||||
|
||||
pointer_focus(target->resource, fx, fy);
|
||||
// A button-only input must not replay a cached absolute position. A game
|
||||
// may have warped its cursor since the last motion (before taking lock,
|
||||
// or while releasing it), so that replay can become an unintended pan.
|
||||
// Absolute touch events still carry their position, including on clicks.
|
||||
int rel_consumed = send_relative_motion(client, dx, dy);
|
||||
if (!(k && k->is_lock) && !(relative && rel_consumed) && (!relative || dx != 0 || dy != 0))
|
||||
for_each_pointer_of(client, sp) wl_pointer_send_motion(sp->ptr, t, fx, fy);
|
||||
@@ -3020,9 +3213,6 @@ static void pointer_button(uint32_t button, int pressed) {
|
||||
pointer_input(0, 0, 1, button, pressed);
|
||||
return;
|
||||
}
|
||||
// Releases also clean up interrupted gestures. Deliver them to the existing
|
||||
// pointer focus without motion or hit-testing: native touch has independent
|
||||
// coordinates, so replaying g_ptr_x/y here can warp the game cursor.
|
||||
uint32_t t = now_ms();
|
||||
for (int i = 0; i < g_nptrs; i++) {
|
||||
struct seat_pointer *sp = &g_ptrs[i];
|
||||
@@ -3442,6 +3632,7 @@ static int on_input_readable(int fd, uint32_t mask, void *data) {
|
||||
case 12: axis_event(m.p1, m.p2, 1); break;
|
||||
case 20: stream_take(); break;
|
||||
case 21: if (!m.p1) stream_forget_all(); schedule_render(); break;
|
||||
case 22: schedule_render(); break;
|
||||
default: deliver_pointer(&m); break;
|
||||
}
|
||||
}
|
||||
@@ -3516,6 +3707,17 @@ void droiddeck_wayland_send_scene_input(int type, int a, int b) {
|
||||
(void)n;
|
||||
}
|
||||
|
||||
void droiddeck_wayland_set_cursor_composite(int on) {
|
||||
const int want = on ? 1 : 0;
|
||||
if (atomic_exchange(&g_cursor_composite_want, want) == want) return;
|
||||
atomic_store(&g_cursor_composite, want);
|
||||
droiddeck_log("cursor", "compositing the pointer into the output: %s", on ? "on" : "off");
|
||||
if (g_input_pipe[1] < 0) return;
|
||||
struct input_msg m = { 22, on ? 1 : 0, 0, 0, 0 };
|
||||
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
|
||||
(void)n;
|
||||
}
|
||||
|
||||
|
||||
const char *droiddeck_client_name(struct wl_client *client) { return client_name(client); }
|
||||
struct wl_display *droiddeck_get_display(void) { return g_display; }
|
||||
@@ -3695,8 +3897,6 @@ int droiddeck_wayland_run(void) {
|
||||
open_session_log();
|
||||
|
||||
if (vkp_ready() != 0) WLOGE("renderer unavailable; clients will connect but nothing is drawn");
|
||||
/* Presentation compatibility is not HDR capability. Set the initial layer mode before
|
||||
* clients bind, including when Desktop is the first session in this app process. */
|
||||
if (vkp_quest_layer_presentation() && sc_layer_available()) g_zero_copy = 1;
|
||||
|
||||
struct wl_display *display = wl_display_create();
|
||||
@@ -3717,6 +3917,10 @@ int droiddeck_wayland_run(void) {
|
||||
return 1;
|
||||
}
|
||||
droiddeck_log("display", "Wayland compositor listening on %s/wayland-0", rt ? rt : "?");
|
||||
wl_log_set_handler_server(wl_log_to_session);
|
||||
/* A client that falls behind reading (gamescope at a few fps) must not be dropped for a full
|
||||
* event buffer: libwayland's default is 4 KB per direction. */
|
||||
wl_display_set_default_max_buffer_size(display, 1u << 20);
|
||||
wl_display_add_client_created_listener(display, &g_client_created);
|
||||
|
||||
wl_global_create(display, &wl_compositor_interface, 6, NULL, bind_compositor);
|
||||
@@ -3727,6 +3931,7 @@ int droiddeck_wayland_run(void) {
|
||||
wl_global_create(display, &wl_output_interface, 4, NULL, bind_output);
|
||||
wl_global_create(display, &xdg_wm_base_interface, 1, NULL, bind_xdg_wm_base);
|
||||
wl_global_create(display, &zwp_linux_dmabuf_v1_interface, 4, NULL, bind_dmabuf);
|
||||
droiddeck_android_wlegl_init(display);
|
||||
wl_global_create(display, &wl_seat_interface, 9, NULL, bind_seat);
|
||||
wl_global_create(display, &banner_desktop_v1_interface, 1, NULL, bind_desktop);
|
||||
wl_global_create(display, &wp_presentation_interface, 2, NULL, bind_presentation);
|
||||
|
||||
@@ -61,3 +61,6 @@ extern volatile int g_no_render_node;
|
||||
|
||||
COMPOSITOR_INTERNAL extern dev_t g_main_device;
|
||||
COMPOSITOR_INTERNAL void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t id);
|
||||
|
||||
/* ---- wl_android_buffer.c */
|
||||
COMPOSITOR_INTERNAL void droiddeck_android_wlegl_init(struct wl_display *display);
|
||||
@@ -1,5 +1,6 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "effects_chain.h"
|
||||
#include "blend_pass.h"
|
||||
#include "vk_loader.h"
|
||||
#include "vk_present.h"
|
||||
#include <pthread.h>
|
||||
@@ -146,6 +147,16 @@ int vkp_effects_active(void) {
|
||||
return spatial_mode(g_cur.scaling) || effect_count(&g_cur) > 0;
|
||||
}
|
||||
|
||||
int vkp_effects_upscaling(void) {
|
||||
vkp_effects_sync();
|
||||
return spatial_mode(g_cur.scaling) && g_cur.scaling != 6;
|
||||
}
|
||||
|
||||
int vkp_effects_postprocessing(void) {
|
||||
vkp_effects_sync();
|
||||
return effect_count(&g_cur) > 0;
|
||||
}
|
||||
|
||||
VkFilter vkp_effects_blit_filter(void) {
|
||||
vkp_effects_sync();
|
||||
return g_cur.scaling == 2 ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
|
||||
@@ -233,6 +244,7 @@ static VkImageView make_view(VkImage img, VkFormat fmt) {
|
||||
}
|
||||
|
||||
static void target_destroy(struct fx_target *t) {
|
||||
if (t->img) blendp_forget_image(t->img);
|
||||
if (t->ds) g_vk.FreeDescriptorSets(g_dev, g_dpool, 1, &t->ds);
|
||||
if (t->fb) g_vk.DestroyFramebuffer(g_dev, t->fb, NULL);
|
||||
if (t->view) g_vk.DestroyImageView(g_dev, t->view, NULL);
|
||||
|
||||
@@ -10,6 +10,9 @@ int vkp_effects_sync(void);
|
||||
|
||||
int vkp_effects_active(void);
|
||||
|
||||
int vkp_effects_upscaling(void);
|
||||
int vkp_effects_postprocessing(void);
|
||||
|
||||
VkFilter vkp_effects_blit_filter(void);
|
||||
|
||||
VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int scene_h,
|
||||
|
||||
@@ -374,16 +374,11 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view,
|
||||
mark_failed(kind);
|
||||
return 0;
|
||||
}
|
||||
/* The ring and the engine's images are new on this frame. Dispatching into them now
|
||||
* null-dereferenced Turnip (vkp_framegen_run, first Steam frame, 2560x1440 to the panel).
|
||||
* Show this frame as it is; generation starts once the size has settled. */
|
||||
if (size_changed || ring == 2) {
|
||||
FGLOG("frame generation catches up at %dx%d; this frame is shown as it is", w, h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The pacer's source rate must be the game's, not this loop's: a loop slowed by its own
|
||||
* generations would otherwise read as a slower game and justify even more generations. */
|
||||
++g_source_frames;
|
||||
const uint64_t game_frames = droiddeck_game_frames();
|
||||
uint32_t n_gen = fge_plan(g_ring_n, game_frames ? game_frames : g_source_frames, g_presented_rate);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#include "gpu_busy.h"
|
||||
|
||||
#include <jni.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "gpuload_ring.h"
|
||||
|
||||
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static struct gpuload_vk g_v;
|
||||
static struct gpuload_ring g_ring;
|
||||
static uint32_t g_family, g_bits;
|
||||
static PFN_vkQueueSubmit g_submit;
|
||||
static int g_was_on;
|
||||
static _Atomic int g_on, g_lost, g_timing;
|
||||
static _Atomic uint64_t g_busy_ns;
|
||||
|
||||
static VkResult VKAPI_CALL timed_submit(VkQueue queue, uint32_t count, const VkSubmitInfo *infos, VkFence fence) {
|
||||
if (!count || !atomic_load(&g_on) || atomic_load(&g_lost)) return g_submit(queue, count, infos, fence);
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!atomic_load(&g_on) || atomic_load(&g_lost)) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return g_submit(queue, count, infos, fence);
|
||||
}
|
||||
if (g_ring.state == 0 && gpuload_ring_setup(&g_ring, &g_v, g_family, g_bits)) atomic_store(&g_timing, 1);
|
||||
VkSubmitInfo *mine = NULL;
|
||||
uint32_t used = 0;
|
||||
if (g_ring.state == 1) {
|
||||
uint64_t ns = gpuload_ring_harvest(&g_ring, &g_v);
|
||||
if (g_was_on) atomic_fetch_add(&g_busy_ns, ns); // the first harvest after a pause holds work timed before it
|
||||
g_was_on = 1;
|
||||
mine = gpuload_ring_wrap(&g_ring, count, infos, &used);
|
||||
}
|
||||
VkResult r = g_submit(queue, count, mine ? mine : infos, fence);
|
||||
if (mine && r != VK_SUCCESS) gpuload_ring_unwind(&g_ring, used);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
free(mine);
|
||||
return r;
|
||||
}
|
||||
|
||||
void vkp_gpu_busy_device(VkPhysicalDevice pd, VkDevice dev, uint32_t family) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
memset(&g_ring, 0, sizeof(g_ring));
|
||||
g_was_on = 0;
|
||||
atomic_store(&g_timing, 0);
|
||||
atomic_store(&g_lost, 0);
|
||||
PFN_vkGetDeviceProcAddr gdpa = g_vk.GetDeviceProcAddr;
|
||||
g_v = (struct gpuload_vk){
|
||||
.device = dev,
|
||||
.create_pool = g_vk.CreateCommandPool,
|
||||
.destroy_pool = (PFN_vkDestroyCommandPool)gdpa(dev, "vkDestroyCommandPool"),
|
||||
.alloc_cmds = g_vk.AllocateCommandBuffers,
|
||||
.begin = g_vk.BeginCommandBuffer,
|
||||
.end = g_vk.EndCommandBuffer,
|
||||
.cmd_reset = (PFN_vkCmdResetQueryPool)gdpa(dev, "vkCmdResetQueryPool"),
|
||||
.cmd_timestamp = (PFN_vkCmdWriteTimestamp)gdpa(dev, "vkCmdWriteTimestamp"),
|
||||
.create_queries = (PFN_vkCreateQueryPool)gdpa(dev, "vkCreateQueryPool"),
|
||||
.destroy_queries = (PFN_vkDestroyQueryPool)gdpa(dev, "vkDestroyQueryPool"),
|
||||
.results = (PFN_vkGetQueryPoolResults)gdpa(dev, "vkGetQueryPoolResults"),
|
||||
};
|
||||
VkPhysicalDeviceProperties props;
|
||||
g_vk.GetPhysicalDeviceProperties(pd, &props);
|
||||
gpuload_vk_period(&g_v, props.limits.timestampPeriod);
|
||||
VkQueueFamilyProperties families[16];
|
||||
uint32_t n = 16;
|
||||
g_vk.GetPhysicalDeviceQueueFamilyProperties(pd, &n, families);
|
||||
g_family = family;
|
||||
g_bits = family < n ? families[family].timestampValidBits : 0;
|
||||
// vk_loader_load_device has just put the driver's function back.
|
||||
if (g_vk.QueueSubmit != timed_submit) g_submit = g_vk.QueueSubmit;
|
||||
if (g_submit) g_vk.QueueSubmit = timed_submit;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
|
||||
void vkp_gpu_busy_device_lost(void) {
|
||||
atomic_store(&g_lost, 1);
|
||||
atomic_store(&g_timing, 0);
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeGpuBusyNs(JNIEnv *env, jclass cls) {
|
||||
return atomic_load(&g_timing) ? (jlong)atomic_load(&g_busy_ns) : -1;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTiming(JNIEnv *env, jclass cls, jboolean on) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!on) g_was_on = 0;
|
||||
atomic_store(&g_on, on ? 1 : 0);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef GPU_BUSY_H
|
||||
#define GPU_BUSY_H
|
||||
#include "vk_loader.h"
|
||||
|
||||
// Times the compositor's own GPU work for the HUD (tools/gpuload/src/gpuload_ring.h); call after the device and its
|
||||
// functions are loaded. Off until WaylandCompositor.nativeSetGpuTiming(true).
|
||||
void vkp_gpu_busy_device(VkPhysicalDevice pd, VkDevice dev, uint32_t family);
|
||||
void vkp_gpu_busy_device_lost(void);
|
||||
#endif
|
||||
@@ -103,7 +103,6 @@ static bool prepare_pin(void) {
|
||||
(off_t)pin.memory << 12);
|
||||
if (commands == MAP_FAILED) return false;
|
||||
pin.commands = commands;
|
||||
// A3xx/A4xx use type-3 CP_NOP; newer cores use type-7 with zero odd-parity bits.
|
||||
uint32_t *words = commands;
|
||||
words[0] = pin.nop;
|
||||
words[1] = 0;
|
||||
@@ -124,7 +123,6 @@ static void *hold_pin(void *unused) {
|
||||
if (errno == EINTR || errno == ETIMEDOUT) continue;
|
||||
break;
|
||||
}
|
||||
// Constraints expire after idle_timer (normally 80 ms); renew with a NOP, keeping devfreq alive.
|
||||
usleep(25000);
|
||||
if (atomic_load(&running) && !submit_pin()) break;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ struct droiddeck_headpose {
|
||||
float eye_aspect;
|
||||
uint32_t eye_w, eye_h;
|
||||
float display_hz;
|
||||
// Uses a formerly reserved word; older publishers leave this zero.
|
||||
uint32_t render_height;
|
||||
uint32_t reserved[10];
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include "hudframes.h"
|
||||
|
||||
/* Layout written by gamescope patch 0125 (tools/gamescope/patches). */
|
||||
#define HUD_FRAMES_MAGIC 0x46484444u
|
||||
#define HUD_FRAMES_HEADER 128u
|
||||
#define HUD_FRAMES_ENTRY 16u
|
||||
@@ -69,7 +68,6 @@ int hud_frames_poll(struct hud_frames *h, int64_t *frametimes, int64_t *times, i
|
||||
memcpy(&frametimes[n], e, 8);
|
||||
memcpy(×[n], e + 8, 8);
|
||||
}
|
||||
/* The writer may have lapped the ring while we copied: drop what it overwrote. */
|
||||
uint64_t after = load64(h->map + 16);
|
||||
uint64_t valid = after > cap ? after - cap : 0;
|
||||
if (from < valid) {
|
||||
|
||||
@@ -8,9 +8,7 @@ struct hud_frames;
|
||||
|
||||
struct hud_frames *hud_frames_open(const char *path);
|
||||
void hud_frames_close(struct hud_frames *h);
|
||||
/* New frames since the last poll, oldest first; -1 while the file holds no valid ring. */
|
||||
int hud_frames_poll(struct hud_frames *h, int64_t *frametimes, int64_t *times, int max);
|
||||
/* info: pid, output width, output height, refresh mHz, Steam focused. */
|
||||
int hud_frames_info(const struct hud_frames *h, int32_t *info, char *engine, size_t engine_len);
|
||||
|
||||
#endif
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
/* Presentation only; this does not select a GPU driver or expand driver support. */
|
||||
static inline int quest_device_matches(const char *model, const char *device, const char *maker) {
|
||||
return !strcasecmp(model, "Quest 2") || !strcasecmp(model, "Quest 3") ||
|
||||
!strcasecmp(model, "Quest 3S") ||
|
||||
|
||||
@@ -291,7 +291,7 @@ int sc_layer_base_black(int width, int height) {
|
||||
ARect src = {0, 0, 16, 16}, dst = {0, 0, width, height};
|
||||
api.setBuffer(tx, g_base_sc, black, -1);
|
||||
api.setBufferTransparency(tx, g_base_sc, ASC_TRANSPARENCY_OPAQUE);
|
||||
api.setZOrder(tx, g_base_sc, 0); /* GAME = 1, OVERLAY = 2 */
|
||||
api.setZOrder(tx, g_base_sc, 0);
|
||||
api.setGeometry(tx, g_base_sc, &src, &dst, 0);
|
||||
api.setVisibility(tx, g_base_sc, ASC_VISIBILITY_SHOW);
|
||||
api.txApply(tx); api.txDelete(tx);
|
||||
@@ -381,6 +381,26 @@ static int sniff_modifier(const struct droiddeck_native_handle *h, uint64_t *mod
|
||||
return 1;
|
||||
}
|
||||
|
||||
#define SC_MOD_GRALLOC (~0ULL - 1)
|
||||
|
||||
static const char *pool_kind(uint64_t mod) {
|
||||
return mod == MOD_QCOM_COMPRESSED ? "UBWC" : mod == SC_MOD_GRALLOC ? "gralloc-laid-out" : "linear";
|
||||
}
|
||||
|
||||
static void describe_handle(const struct droiddeck_native_handle *h, char *out, size_t n) {
|
||||
size_t o = 0;
|
||||
out[0] = 0;
|
||||
if (!h) { snprintf(out, n, "no handle"); return; }
|
||||
for (int i = 0; i < h->numFds && i < 4 && o < n; i++) {
|
||||
off_t sz = lseek(h->data[i], 0, SEEK_END);
|
||||
if (sz >= 0) lseek(h->data[i], 0, SEEK_SET);
|
||||
o += (size_t)snprintf(out + o, n - o, "%sfd%d %lld bytes", i ? ", " : "", i, (long long)sz);
|
||||
}
|
||||
if (o < n) o += (size_t)snprintf(out + o, n - o, "; ints");
|
||||
for (int i = 0; i < h->numInts && i < 8 && o < n; i++)
|
||||
o += (size_t)snprintf(out + o, n - o, " %#x", (unsigned)h->data[h->numFds + i]);
|
||||
}
|
||||
|
||||
static int g_alloc_failed;
|
||||
|
||||
enum { TIER_UBWC, TIER_PLAIN, TIER_LINEAR };
|
||||
@@ -416,22 +436,54 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm
|
||||
AHardwareBuffer_Desc got; AHardwareBuffer_describe(ahb, &got);
|
||||
const struct droiddeck_native_handle *nh = api.getNativeHandle(ahb);
|
||||
uint64_t mod = MOD_LINEAR;
|
||||
if (!sniff_modifier(nh, &mod)) {
|
||||
if (tier < TIER_LINEAR) {
|
||||
struct vkp_image *img = NULL;
|
||||
const char *path;
|
||||
char info[160] = "";
|
||||
if (sniff_modifier(nh, &mod)) {
|
||||
img = vkp_image_import_dmabuf(nh->data[0], fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888, mod, w, h,
|
||||
got.stride * 4, 0, 1);
|
||||
path = "dma-buf (QTI handle)";
|
||||
} else {
|
||||
if (tier == TIER_UBWC) {
|
||||
char hd[256];
|
||||
describe_handle(nh, hd, sizeof(hd));
|
||||
droiddeck_log("layer", "%s: gralloc handle is not a QTI one (%d fds, %d ints: %s): no UBWC request; "
|
||||
"pool buffers are imported %s", l->name, nh ? nh->numFds : -1, nh ? nh->numInts : -1, hd,
|
||||
vkp_can_import_ahb() ? "as AHardwareBuffers (the driver takes gralloc's own layout)"
|
||||
: "as linear dma-bufs (this driver has no AHardwareBuffer import)");
|
||||
AHardwareBuffer_release(ahb);
|
||||
l->alloc_tier = TIER_LINEAR;
|
||||
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);
|
||||
l->alloc_tier = vkp_can_import_ahb() ? TIER_PLAIN : TIER_LINEAR;
|
||||
continue;
|
||||
}
|
||||
mod = MOD_LINEAR;
|
||||
mod = SC_MOD_GRALLOC;
|
||||
path = "AHardwareBuffer";
|
||||
img = vkp_image_import_ahb(ahb, info, sizeof(info));
|
||||
if (!img && tier == TIER_LINEAR) {
|
||||
const off_t need = (off_t)got.stride * 4 * h;
|
||||
int fd = -1, which = -1;
|
||||
for (int i = 0; nh && i < nh->numFds; i++) {
|
||||
off_t sz = lseek(nh->data[i], 0, SEEK_END);
|
||||
if (sz >= 0) lseek(nh->data[i], 0, SEEK_SET);
|
||||
if (sz >= need) { fd = nh->data[i]; which = i; break; }
|
||||
}
|
||||
if (fd < 0 && nh && nh->numFds > 0) { fd = nh->data[0]; which = 0; }
|
||||
mod = MOD_LINEAR;
|
||||
path = "linear dma-buf";
|
||||
img = fd >= 0 ? vkp_image_import_dmabuf(fd, fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888, mod,
|
||||
w, h, got.stride * 4, 0, 1) : NULL;
|
||||
snprintf(info, sizeof(info), "handle fd %d of %d", which, nh ? nh->numFds : 0);
|
||||
if (vkp_can_import_ahb())
|
||||
droiddeck_log("layer", "%s: AHardwareBuffer import failed: the linear dma-buf in the handle's fd %d "
|
||||
"(of %d) -> %s", l->name, which, nh ? nh->numFds : 0, img ? "imported" : "failed too");
|
||||
}
|
||||
}
|
||||
int fd = (nh && nh->numFds > 0) ? nh->data[0] : -1;
|
||||
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) {
|
||||
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);
|
||||
char hd[256];
|
||||
describe_handle(nh, hd, sizeof(hd));
|
||||
droiddeck_log("layer", "%s: import of a %s %dx%d pool buffer (stride %u px, %s, usage %#llx) into the "
|
||||
"compositor's Turnip failed via %s; gralloc handle %d fds / %d ints: %s", l->name,
|
||||
pool_kind(mod), w, h, got.stride, tier_name(tier), (unsigned long long)got.usage, path,
|
||||
nh ? nh->numFds : -1, nh ? nh->numInts : -1, hd);
|
||||
AHardwareBuffer_release(ahb);
|
||||
if (tier < TIER_LINEAR) { l->alloc_tier = tier + 1; continue; }
|
||||
g_alloc_failed = 1;
|
||||
@@ -442,10 +494,11 @@ 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;
|
||||
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);
|
||||
droiddeck_log("layer", "%s: pool buffers %dx%d%s: %s (%s), stride %u px, up to %d buffers (gralloc handle %d fds / "
|
||||
"%d ints), imported as %s%s%s%s", l->name, w, h, fmt == AHB_RGB10A2 ? " 10-bit RGBA1010102" : "",
|
||||
mod == MOD_QCOM_COMPRESSED ? "UBWC (QCOM_COMPRESSED)" : pool_kind(mod), tier_name(tier),
|
||||
got.stride, l->pool_n, nh ? nh->numFds : -1, nh ? nh->numInts : -1, path,
|
||||
info[0] ? " (" : "", info, info[0] ? ")" : "");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -814,7 +867,7 @@ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const stru
|
||||
l->first_logged = 1;
|
||||
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);
|
||||
pool_kind(l->pool_modifier), l->pool_n);
|
||||
}
|
||||
vkp_signal_first_frame();
|
||||
return 0;
|
||||
@@ -836,7 +889,7 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int
|
||||
l->first_logged = 1;
|
||||
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);
|
||||
pool_kind(l->pool_modifier), l->pool_n);
|
||||
}
|
||||
vkp_signal_first_frame();
|
||||
return 0;
|
||||
@@ -874,12 +927,12 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct
|
||||
if (tm)
|
||||
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");
|
||||
pool_kind(l->pool_modifier));
|
||||
else {
|
||||
char cov[96];
|
||||
coverage_text(l, cov, sizeof(cov));
|
||||
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",
|
||||
pool_kind(l->pool_modifier),
|
||||
g_hdr_pool_fmt == AHB_RGB10A2 ? "10-bit" : "8-bit",
|
||||
color && color->dataspace == DROIDDECK_ADATASPACE_BT2020_PQ ? "BT2020_PQ" : "HDR",
|
||||
cov[0] ? ", " : "", cov);
|
||||
@@ -973,7 +1026,7 @@ void sc_layer_hide_overlay(void) {
|
||||
}
|
||||
|
||||
void sc_layer_window_gone(void) {
|
||||
base_hide(1); /* the backdrop belongs to the old window, just like GAME and OVERLAY */
|
||||
base_hide(1);
|
||||
if (api.state != 1) return;
|
||||
layers_init();
|
||||
for (int i = SC_LAYER_COUNT - 1; i >= 0; i--) retire_sc(&g_layers[i]);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef VK_EXTERNAL_IMAGE_SYNC_H
|
||||
#define VK_EXTERNAL_IMAGE_SYNC_H
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
struct vkp_external_client_barrier_pair {
|
||||
VkImageMemoryBarrier acquire;
|
||||
VkImageMemoryBarrier release;
|
||||
};
|
||||
|
||||
static inline int vkp_is_external_client_image(int dmabuf, int blit_dst) {
|
||||
return dmabuf && !blit_dst;
|
||||
}
|
||||
|
||||
/* Client-owned dma-bufs, including AHardwareBuffers, live in GENERAL while
|
||||
* FOREIGN owns them. Layer pool dma-bufs are destinations with a separate
|
||||
* acquire/release lifecycle and must never take this source-image path. */
|
||||
static inline int vkp_external_client_barrier_pair(
|
||||
VkImage image, uint32_t queue_family, int dmabuf, int blit_dst,
|
||||
struct vkp_external_client_barrier_pair *pair) {
|
||||
if (!vkp_is_external_client_image(dmabuf, blit_dst) || !pair) return 0;
|
||||
|
||||
const VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
pair->acquire = (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT,
|
||||
.dstQueueFamilyIndex = queue_family,
|
||||
.image = image,
|
||||
.subresourceRange = range,
|
||||
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
|
||||
};
|
||||
pair->release = (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = queue_family,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT,
|
||||
.image = image,
|
||||
.subresourceRange = range,
|
||||
.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT,
|
||||
};
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -12,7 +12,7 @@
|
||||
X(CreateAndroidSurfaceKHR) X(DestroySurfaceKHR) X(CreateDevice) \
|
||||
X(GetDeviceProcAddr) X(EnumerateDeviceExtensionProperties) \
|
||||
X(GetPhysicalDeviceMemoryProperties) X(GetPhysicalDeviceFormatProperties2) \
|
||||
X(GetPhysicalDeviceImageFormatProperties2)
|
||||
X(GetPhysicalDeviceImageFormatProperties2) X(GetPhysicalDeviceProperties2)
|
||||
|
||||
#define VK_DEVICE_FUNCS(X) \
|
||||
X(GetDeviceQueue) X(CreateSwapchainKHR) X(DestroySwapchainKHR) X(GetSwapchainImagesKHR) \
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,10 @@
|
||||
#ifndef VK_PRESENT_H
|
||||
#define VK_PRESENT_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <android/native_window.h>
|
||||
#include <android/hardware_buffer.h>
|
||||
|
||||
void vk_present_set_driver(const char *driver_path, const char *library_name,
|
||||
const char *native_lib_dir);
|
||||
|
||||
@@ -21,6 +24,9 @@ void vkp_output_size(int *w, int *h);
|
||||
|
||||
struct vkp_image;
|
||||
|
||||
int vkp_android_buffer_supported(void);
|
||||
struct vkp_image *vkp_image_from_android_buffer(AHardwareBuffer *buffer);
|
||||
|
||||
#define VKP_MOD_LINEAR 0x0000000000000000ULL
|
||||
#define VKP_MOD_QCOM_COMPRESSED 0x0500000000000001ULL
|
||||
#define VKP_MOD_INVALID 0x00ffffffffffffffULL
|
||||
@@ -33,6 +39,9 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
|
||||
int w, int h, uint32_t stride, uint32_t offset,
|
||||
int as_blit_dst);
|
||||
int vkp_image_is_dmabuf(const struct vkp_image *img);
|
||||
struct AHardwareBuffer;
|
||||
int vkp_can_import_ahb(void);
|
||||
struct vkp_image *vkp_image_import_ahb(struct AHardwareBuffer *ahb, char *info, size_t info_n);
|
||||
|
||||
struct vkp_image *vkp_image_create_shm(int w, int h);
|
||||
struct AHardwareBuffer;
|
||||
@@ -57,6 +66,10 @@ int vkp_ready(void);
|
||||
int vkp_has_window(void);
|
||||
int vkp_wait_window_request(int timeout_ms);
|
||||
int vkp_device_lost(void);
|
||||
/* Compositor thread only. Submitted reads may still be pending: retain imported
|
||||
* buffer references and withhold DMA-BUF releases until the app process exits.
|
||||
* This terminal state is distinct from genuine device loss. */
|
||||
int vkp_completion_unknown(void);
|
||||
|
||||
int vkp_render(int scene_w, int scene_h, const struct vkp_draw *draws, int n);
|
||||
int vkp_render_plain(int scene_w, int scene_h);
|
||||
|
||||
@@ -30,6 +30,7 @@ extern void droiddeck_wayland_send_key(int evdev, int state);
|
||||
extern int droiddeck_focus_takes_touch(void);
|
||||
extern int droiddeck_cursor_input_pos(int *x, int *y);
|
||||
extern void droiddeck_wayland_send_scene_input(int type, int a, int b);
|
||||
extern void droiddeck_wayland_set_cursor_composite(int on);
|
||||
extern void droiddeck_wayland_vsync(int64_t frame_time_ns);
|
||||
extern void droiddeck_stream_set(int on);
|
||||
extern void droiddeck_stream_frame(AHardwareBuffer *ahb);
|
||||
@@ -215,6 +216,24 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCompositorTid(JNIEnv
|
||||
return (jint)g_comp_tid;
|
||||
}
|
||||
|
||||
/* Why presenting failed ("" while it works), for the session-ended screen and the report. */
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCompositorFault(JNIEnv *env, jclass clazz) {
|
||||
(void)clazz;
|
||||
extern const char *vkp_fault(void);
|
||||
const char *f = vkp_fault();
|
||||
return (*env)->NewStringUTF(env, f ? f : "");
|
||||
}
|
||||
|
||||
/* The display driver as the driver itself reports it ("" before the compositor's device exists). */
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeDisplayDriver(JNIEnv *env, jclass clazz) {
|
||||
(void)clazz;
|
||||
extern const char *vkp_display_driver(void);
|
||||
const char *d = vkp_display_driver();
|
||||
return (*env)->NewStringUTF(env, d ? d : "");
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStart(JNIEnv *env, jclass clazz,
|
||||
jstring xdgRuntimeDir) {
|
||||
@@ -292,6 +311,13 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSerial(JNIEnv
|
||||
return droiddeck_cursor_serial();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCursorComposite(
|
||||
JNIEnv *env, jclass clazz, jboolean on) {
|
||||
(void)env; (void)clazz;
|
||||
droiddeck_wayland_set_cursor_composite(on ? 1 : 0);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendKey(
|
||||
JNIEnv *env, jclass clazz, jint evdev, jint state) {
|
||||
@@ -690,7 +716,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTurbo(JNIEnv *
|
||||
jclass error = (*env)->FindClass(env, "java/lang/IllegalStateException");
|
||||
if (error) (*env)->ThrowNew(env, error, strerror(saved_errno));
|
||||
}
|
||||
// Clear a legacy global pin when recovering its marker after an app upgrade or crash.
|
||||
if (on != JNI_TRUE) adrenotools_set_turbo(false);
|
||||
}
|
||||
|
||||
@@ -717,7 +742,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardSpatialP
|
||||
vkp_cardboard_set_spatial_pose(m, p, live ? 1 : 0);
|
||||
}
|
||||
|
||||
/* The Xbox Cloud Gaming native player: its decoded frames are the whole scene (no Wayland clients). */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStreamMode(JNIEnv *env, jclass clazz, jboolean on) {
|
||||
droiddeck_stream_set(on ? 1 : 0);
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/* android_wlegl buffer import for the existing libhybris Wayland WSI.
|
||||
* Protocol: Collabora/libhybris, vendored with its license in protocols/wayland-android.xml.
|
||||
* Native handles are cloned through Android; opaque vendor integers are never interpreted. */
|
||||
#include "compositor_internal.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "wayland-android-server-protocol.h"
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Stable native_handle_t ABI, unavailable in the public NDK. */
|
||||
struct android_native_handle {
|
||||
int version, num_fds, num_ints;
|
||||
int data[];
|
||||
};
|
||||
struct wire_handle {
|
||||
struct android_native_handle *handle;
|
||||
int received;
|
||||
};
|
||||
enum { MAX_HANDLE_FDS = 64, MAX_HANDLE_INTS = 256, MAX_BUFFER_SIDE = 8192 };
|
||||
/* VNDK ABI (not exposed by the NDK): frameworks/native/libs/nativewindow/include/
|
||||
* vndk/hardware_buffer.h, AHARDWAREBUFFER_CREATE_FROM_HANDLE_METHOD_CLONE = 3.
|
||||
* Method 1 is not accepted by AHardwareBuffer_createFromHandle. */
|
||||
enum { CREATE_FROM_HANDLE_CLONE = 3 };
|
||||
static int (*import_handle)(const AHardwareBuffer_Desc *, const struct android_native_handle *,
|
||||
int32_t, AHardwareBuffer **);
|
||||
static const struct android_native_handle *(*native_handle)(const AHardwareBuffer *);
|
||||
|
||||
static int valid_description(int32_t width, int32_t height, int32_t stride, int32_t format) {
|
||||
return width > 0 && height > 0 && width <= MAX_BUFFER_SIDE && height <= MAX_BUFFER_SIDE &&
|
||||
stride >= width && stride <= MAX_BUFFER_SIDE * 4 &&
|
||||
(format == 1 || format == 2 || format == 5);
|
||||
}
|
||||
|
||||
static void handle_resource_destroy(struct wl_resource *resource) {
|
||||
struct wire_handle *wire = wl_resource_get_user_data(resource);
|
||||
if (!wire) return;
|
||||
for (int i = 0; i < wire->received; i++) close(wire->handle->data[i]);
|
||||
free(wire->handle);
|
||||
free(wire);
|
||||
}
|
||||
|
||||
static void handle_add_fd(struct wl_client *client, struct wl_resource *resource, int32_t fd) {
|
||||
struct wire_handle *wire = wl_resource_get_user_data(resource);
|
||||
if (wire->received >= wire->handle->num_fds) {
|
||||
close(fd);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_HANDLE_ERROR_TOO_MANY_FDS,
|
||||
"more native handle descriptors than declared");
|
||||
return;
|
||||
}
|
||||
wire->handle->data[wire->received++] = fd;
|
||||
}
|
||||
|
||||
static void handle_destroy(struct wl_client *client, struct wl_resource *resource) {
|
||||
wl_resource_destroy(resource);
|
||||
}
|
||||
|
||||
static const struct android_wlegl_handle_interface handle_impl = {
|
||||
.add_fd = handle_add_fd, .destroy = handle_destroy,
|
||||
};
|
||||
|
||||
static void create_handle(struct wl_client *client, struct wl_resource *resource,
|
||||
uint32_t id, int32_t fds, struct wl_array *ints) {
|
||||
if (fds < 0 || fds > MAX_HANDLE_FDS || ints->size % sizeof(int32_t) ||
|
||||
ints->size > MAX_HANDLE_INTS * sizeof(int32_t)) {
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "invalid native handle size");
|
||||
return;
|
||||
}
|
||||
struct wire_handle *wire = calloc(1, sizeof(*wire));
|
||||
if (!wire) { wl_client_post_no_memory(client); return; }
|
||||
wire->handle = calloc(1, sizeof(*wire->handle) + (size_t)fds * sizeof(int32_t) + ints->size);
|
||||
if (!wire->handle) { free(wire); wl_client_post_no_memory(client); return; }
|
||||
wire->handle->version = sizeof(*wire->handle);
|
||||
wire->handle->num_fds = fds;
|
||||
wire->handle->num_ints = (int)(ints->size / sizeof(int32_t));
|
||||
if (ints->size) memcpy(wire->handle->data + fds, ints->data, ints->size);
|
||||
struct wl_resource *handle = wl_resource_create(client, &android_wlegl_handle_interface, 1, id);
|
||||
if (!handle) {
|
||||
free(wire->handle); free(wire); wl_client_post_no_memory(client); return;
|
||||
}
|
||||
wl_resource_set_implementation(handle, &handle_impl, wire, handle_resource_destroy);
|
||||
}
|
||||
|
||||
static void buffer_resource_destroy(struct wl_resource *resource) {
|
||||
dmabuf_buffer_unref(wl_resource_get_user_data(resource));
|
||||
}
|
||||
|
||||
static struct wl_resource *create_android_buffer(struct wl_client *client, uint32_t id,
|
||||
AHardwareBuffer *hardware) {
|
||||
AHardwareBuffer_Desc desc;
|
||||
AHardwareBuffer_describe(hardware, &desc);
|
||||
struct vkp_image *image = vkp_image_from_android_buffer(hardware);
|
||||
if (!image) return NULL;
|
||||
struct dmabuf_buffer *buffer = calloc(1, sizeof(*buffer));
|
||||
if (!buffer) { vkp_image_destroy(image); wl_client_post_no_memory(client); return NULL; }
|
||||
buffer->refs = 1;
|
||||
buffer->width = (int32_t)desc.width; buffer->height = (int32_t)desc.height;
|
||||
buffer->format = desc.format == 5 ? DRM_ARGB8888 : desc.format == 2 ? DRM_XBGR8888 : DRM_ABGR8888;
|
||||
buffer->modifier = MOD_INVALID; /* No DMA-BUF layout was inferred from the native handle. */
|
||||
buffer->img = image;
|
||||
for (int i = 0; i < MAX_PLANES; i++) buffer->fd[i] = -1;
|
||||
struct wl_resource *result = wl_resource_create(client, &wl_buffer_interface, 1, id);
|
||||
if (!result) { dmabuf_buffer_unref(buffer); wl_client_post_no_memory(client); return NULL; }
|
||||
wl_resource_set_implementation(result, &dbuf_buffer_impl, buffer, buffer_resource_destroy);
|
||||
struct client_info *info = client_info_of(client);
|
||||
if (info) info->dmabuf_buffers++;
|
||||
return result;
|
||||
}
|
||||
|
||||
static void create_buffer(struct wl_client *client, struct wl_resource *resource, uint32_t id,
|
||||
int32_t width, int32_t height, int32_t stride, int32_t format,
|
||||
int32_t usage, struct wl_resource *handle_resource) {
|
||||
/* Some vendor allocators (Samsung Xclipse) record no stride for GPU-only buffers. The import
|
||||
* must pass that 0 back unchanged: gralloc validates it against the handle's own value. */
|
||||
if (!valid_description(width, height, stride ? stride : width, format) ||
|
||||
!wl_resource_instance_of(handle_resource, &android_wlegl_handle_interface, &handle_impl)) {
|
||||
droiddeck_log("display", "android_wlegl rejected: %dx%d stride=%d format=%d usage=0x%x",
|
||||
width, height, stride, format, (uint32_t)usage);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "invalid Android buffer description");
|
||||
return;
|
||||
}
|
||||
struct wire_handle *wire = wl_resource_get_user_data(handle_resource);
|
||||
if (wire->received != wire->handle->num_fds || !wire->received) {
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "incomplete native handle");
|
||||
return;
|
||||
}
|
||||
AHardwareBuffer_Desc desc = {
|
||||
.width = (uint32_t)width, .height = (uint32_t)height, .layers = 1,
|
||||
.format = (uint32_t)format, .usage = (uint32_t)usage, .stride = (uint32_t)stride};
|
||||
AHardwareBuffer *hardware = NULL;
|
||||
int status = import_handle(&desc, wire->handle, CREATE_FROM_HANDLE_CLONE, &hardware);
|
||||
if (status != 0 || !hardware) {
|
||||
droiddeck_log("display", "android_wlegl import failed: status=%d %dx%d stride=%d format=%d usage=0x%x fds=%d ints=%d",
|
||||
status, width, height, stride, format, (uint32_t)usage,
|
||||
wire->handle->num_fds, wire->handle->num_ints);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE,
|
||||
"Android rejected the native handle (status %d)", status);
|
||||
return;
|
||||
}
|
||||
struct wl_resource *buffer = create_android_buffer(client, id, hardware);
|
||||
AHardwareBuffer_release(hardware);
|
||||
if (!buffer) wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "Vulkan cannot import this buffer");
|
||||
}
|
||||
|
||||
static void get_server_buffer(struct wl_client *client, struct wl_resource *resource, uint32_t id,
|
||||
int32_t width, int32_t height, int32_t format, int32_t usage) {
|
||||
if (!format) format = 1;
|
||||
if (!valid_description(width, height, width, format)) {
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "invalid buffer allocation request");
|
||||
return;
|
||||
}
|
||||
AHardwareBuffer_Desc desc = {
|
||||
.width = (uint32_t)width, .height = (uint32_t)height, .layers = 1,
|
||||
.format = (uint32_t)format,
|
||||
.usage = (uint32_t)usage | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT};
|
||||
AHardwareBuffer *hardware = NULL;
|
||||
if (AHardwareBuffer_allocate(&desc, &hardware) != 0 || !hardware) {
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "Android buffer allocation failed");
|
||||
return;
|
||||
}
|
||||
AHardwareBuffer_describe(hardware, &desc);
|
||||
const struct android_native_handle *handle = native_handle(hardware);
|
||||
if (!handle || handle->num_fds < 1 || handle->num_fds > MAX_HANDLE_FDS ||
|
||||
handle->num_ints < 0 || handle->num_ints > MAX_HANDLE_INTS) {
|
||||
AHardwareBuffer_release(hardware);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "invalid allocated native handle");
|
||||
return;
|
||||
}
|
||||
struct wl_resource *buffer = create_android_buffer(client, 0, hardware);
|
||||
if (!buffer) {
|
||||
AHardwareBuffer_release(hardware);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "Vulkan cannot import allocated buffer");
|
||||
return;
|
||||
}
|
||||
struct wl_resource *reply = wl_resource_create(client, &android_wlegl_server_buffer_handle_interface,
|
||||
wl_resource_get_version(resource), id);
|
||||
if (!reply) {
|
||||
wl_resource_destroy(buffer); AHardwareBuffer_release(hardware);
|
||||
wl_client_post_no_memory(client); return;
|
||||
}
|
||||
struct wl_array ints = {.size = (size_t)handle->num_ints * sizeof(int32_t),
|
||||
.alloc = 0, .data = (void *)(handle->data + handle->num_fds)};
|
||||
android_wlegl_server_buffer_handle_send_buffer_ints(reply, &ints);
|
||||
for (int i = 0; i < handle->num_fds; i++)
|
||||
android_wlegl_server_buffer_handle_send_buffer_fd(reply, handle->data[i]);
|
||||
/* No linear_layout event: Android's opaque allocation need not be linear. */
|
||||
android_wlegl_server_buffer_handle_send_buffer(reply, buffer, (int32_t)desc.format, (int32_t)desc.stride);
|
||||
wl_resource_destroy(reply);
|
||||
AHardwareBuffer_release(hardware);
|
||||
}
|
||||
|
||||
static const struct android_wlegl_interface wlegl_impl = {
|
||||
.create_handle = create_handle, .create_buffer = create_buffer,
|
||||
.get_server_buffer_handle = get_server_buffer,
|
||||
};
|
||||
|
||||
static void bind_android_wlegl(struct wl_client *client, void *data, uint32_t version, uint32_t id) {
|
||||
struct wl_resource *resource = wl_resource_create(client, &android_wlegl_interface,
|
||||
version < 3 ? version : 3, id);
|
||||
if (!resource) { wl_client_post_no_memory(client); return; }
|
||||
wl_resource_set_implementation(resource, &wlegl_impl, NULL, NULL);
|
||||
}
|
||||
|
||||
void droiddeck_android_wlegl_init(struct wl_display *display) {
|
||||
if (!vkp_android_buffer_supported()) return;
|
||||
void *library = dlopen("libnativewindow.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (!library) return;
|
||||
import_handle = dlsym(library, "AHardwareBuffer_createFromHandle");
|
||||
native_handle = dlsym(library, "AHardwareBuffer_getNativeHandle");
|
||||
if (!import_handle || !native_handle) { dlclose(library); return; }
|
||||
/* Keep the platform entry points valid for the compositor's lifetime. */
|
||||
if (wl_global_create(display, &android_wlegl_interface, 3, NULL, bind_android_wlegl))
|
||||
droiddeck_log("display", "android_wlegl v3: Android buffer import available");
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.droiddeck.launcher
|
||||
import android.app.Application
|
||||
import com.droiddeck.launcher.runtime.BwrapSpawner
|
||||
import com.droiddeck.launcher.session.CrashHandler
|
||||
import com.droiddeck.launcher.session.EnhancedSessionLogShare
|
||||
import com.droiddeck.launcher.session.GpuClockPin
|
||||
|
||||
class App : Application() {
|
||||
@@ -16,10 +17,10 @@ class App : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Thread({ runCatching { EnhancedSessionLogShare.cleanupAbandonedStages(this) } }, "diagnostic-stage-cleanup").start()
|
||||
com.droiddeck.launcher.ui.Themes.loadGradient(this)
|
||||
com.droiddeck.launcher.ui.BuildWatermark.install(this)
|
||||
HomeApp.trackRecents(this)
|
||||
// Helper processes must not initialize the main USB provider or guest graphics context.
|
||||
val process = if (android.os.Build.VERSION.SDK_INT >= 28) Application.getProcessName()
|
||||
else (getSystemService(ACTIVITY_SERVICE) as android.app.ActivityManager)
|
||||
.runningAppProcesses?.firstOrNull { it.pid == android.os.Process.myPid() }?.processName
|
||||
@@ -32,12 +33,11 @@ class App : Application() {
|
||||
return
|
||||
}
|
||||
if (process == "$packageName:xcloudnative") {
|
||||
// The native cloud player: the stores log and nothing that touches the Linux session.
|
||||
com.droiddeck.launcher.stores.StoresState.init(this)
|
||||
return
|
||||
}
|
||||
if (process == "$packageName:vr_recovery" || process == "$packageName:rgb_diagnostic" ||
|
||||
process == "$packageName:shortcuts_sync") return
|
||||
process == "$packageName:shortcuts_sync" || process == "$packageName:bottom_desktop") return
|
||||
CrashHandler.install(this)
|
||||
com.droiddeck.launcher.core.SteamIntegration.syncShortcutsListing(this)
|
||||
com.droiddeck.launcher.session.QuestExperimental.initialize(this)
|
||||
@@ -57,11 +57,12 @@ class App : Application() {
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.epic.EpicBackend)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.amazon.AmazonBackend)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.itch.ItchBackend)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.gamejolt.GameJoltBackend)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.xbox.XboxBackend)
|
||||
com.droiddeck.launcher.sync.Syncthing.init(this)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.battlenet.BattleNetBackend)
|
||||
}
|
||||
|
||||
/** Fetches the packs an existing setup already relies on, so the first use after an update does not wait. */
|
||||
private fun prefetchAddonPacks() {
|
||||
val app = this
|
||||
Thread({
|
||||
|
||||
@@ -5,8 +5,11 @@ import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.droiddeck.launcher.session.ComponentUpdater
|
||||
import com.droiddeck.launcher.session.ComponentUpdates
|
||||
import com.droiddeck.launcher.session.ComponentsManager
|
||||
import com.droiddeck.launcher.session.ProtonDefault
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
|
||||
internal class ComponentsMenu(private val activity: android.app.Activity, private val ui: Handler) {
|
||||
var compSnapshot by mutableStateOf<ComponentsManager.Snapshot?>(null)
|
||||
@@ -20,6 +23,10 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
get() = downloads
|
||||
private set(value) { downloads = value }
|
||||
val compDownloadRevision: Long get() = downloadRevision
|
||||
var compModes by mutableStateOf<Map<String, String>>(emptyMap())
|
||||
var compAuto by mutableStateOf<Map<String, List<ComponentUpdater.Status>>>(emptyMap())
|
||||
var compAutoHeld by mutableStateOf<Set<String>>(emptySet())
|
||||
var compAutoChecking by mutableStateOf(false)
|
||||
private var shownDefault: String? = null
|
||||
|
||||
fun refreshComponents(snapshotFirst: Boolean = false) {
|
||||
@@ -29,7 +36,14 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
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() }
|
||||
runCatching { ComponentUpdater.settle(activity) }
|
||||
val modes = ComponentsManager.COMPONENTS.associateWith { SessionPrefs.componentMode(activity, it) }
|
||||
val auto = ComponentsManager.COMPONENTS.associateWith { runCatching { ComponentUpdater.status(activity, it) }.getOrDefault(emptyList()) }
|
||||
val held = if (runCatching { ComponentsManager.hardwareDxvk(activity) }.getOrDefault(false)) setOf("dxvk") else emptySet()
|
||||
ui.post {
|
||||
compModes = modes
|
||||
compAuto = auto
|
||||
compAutoHeld = held
|
||||
compSnapshot = snap ?: ComponentsManager.Snapshot(emptyList(), emptyList())
|
||||
if (cat != null && cat.fetchedAt > 0) { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
|
||||
if (chosen != null && chosen != shownDefault) {
|
||||
@@ -42,6 +56,35 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
}, "components").start()
|
||||
}
|
||||
|
||||
fun chooseComponentMode(comp: String, mode: String) {
|
||||
SessionPrefs.setComponentMode(activity, comp, mode)
|
||||
compModes = compModes + (comp to mode)
|
||||
if (mode != ComponentUpdates.AUTO) return
|
||||
checkComponentUpdates(force = true)
|
||||
}
|
||||
|
||||
private var checkAgain: Boolean? = null
|
||||
|
||||
fun checkComponentUpdates(force: Boolean) {
|
||||
if (compAutoChecking) {
|
||||
checkAgain = force || checkAgain == true
|
||||
return
|
||||
}
|
||||
compAutoChecking = true
|
||||
Thread({
|
||||
val notes = runCatching { ComponentUpdater.ensure(activity, force) }.onFailure { Log.w(TAG, "component updates", it) }.getOrDefault(emptyList())
|
||||
ui.post {
|
||||
compAutoChecking = false
|
||||
checkAgain?.let { again ->
|
||||
checkAgain = null
|
||||
checkComponentUpdates(again)
|
||||
}
|
||||
if (notes.isNotEmpty()) android.widget.Toast.makeText(activity, notes.joinToString("\n"), android.widget.Toast.LENGTH_LONG).show()
|
||||
refreshComponents()
|
||||
}
|
||||
}, "component-updates").start()
|
||||
}
|
||||
|
||||
fun chooseProton(id: String) {
|
||||
val proton = compSnapshot?.protons?.firstOrNull { it.proton.id == id }?.proton ?: return
|
||||
compProton = id
|
||||
|
||||
@@ -14,10 +14,12 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||
import com.droiddeck.launcher.gpu.AutoDriverTrial
|
||||
import com.droiddeck.launcher.gpu.AutoPicks
|
||||
import com.droiddeck.launcher.gpu.DriverBundle
|
||||
import com.droiddeck.launcher.core.DeviceSupport
|
||||
import com.droiddeck.launcher.gpu.DriverPairs
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import com.droiddeck.launcher.gpu.TurnipReleases
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
@@ -42,7 +44,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
var canRestoreBundled by mutableStateOf(false)
|
||||
private val releaseProgress = HashMap<String, Int>()
|
||||
var linuxSelected by mutableStateOf("")
|
||||
val gpu: GpuInfo = GpuInfo.detect()
|
||||
var gpu: GpuInfo = GpuInfo.detect()
|
||||
private set
|
||||
var mode by mutableStateOf(SessionPrefs.GPU_DRIVERS_AUTO)
|
||||
var pairRows by mutableStateOf<List<PairRow>>(emptyList())
|
||||
var pairBusy by mutableStateOf<String?>(null)
|
||||
@@ -65,20 +68,23 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
}
|
||||
|
||||
fun state() = GpuDriversState(
|
||||
gpuName = gpu.displayName(activity), gpuFamily = gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity),
|
||||
gpuName = gpu.displayName(activity), gpuFamily = if (gpu.name.contains("Xclipse", ignoreCase = true)) gpu.family.label(gpu.name) else gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity, com.droiddeck.launcher.core.DeviceSupport.vkBridge(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.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,
|
||||
activeBundle = activeBundle, powerVr = gpu.family == GpuInfo.Family.POWERVR,
|
||||
systemPowerVr = SystemVulkanDriver.usesDefault(linuxSelected),
|
||||
mode = uiMode, display = displaySlot(), runtime = runtimeSlot(),
|
||||
canRestart = !sessionRunning,
|
||||
checkedAt = TurnipReleases.cached(activity)?.checkedAt ?: 0L,
|
||||
tpUbwcMode = tpUbwcMode.takeIf { gpu.model == 740 },
|
||||
tpUbwcRestartRequired = com.droiddeck.launcher.gpu.TpUbwcHint.restartRequired(activity),
|
||||
)
|
||||
|
||||
fun tpUbwcModeForDevice(): String? = tpUbwcMode.takeIf { gpu.model == 740 }
|
||||
|
||||
fun tpUbwcRestartRequired(): Boolean = com.droiddeck.launcher.gpu.TpUbwcHint.restartRequired(activity)
|
||||
|
||||
fun chooseTpUbwcMode(mode: String) {
|
||||
if (gpu.model != 740) return
|
||||
SessionPrefs.setTpUbwcMode(activity, mode)
|
||||
@@ -123,8 +129,14 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
)
|
||||
}
|
||||
|
||||
var restartBlocked: () -> Boolean = { SessionState.running }
|
||||
|
||||
fun loadTpUbwcMode() {
|
||||
tpUbwcMode = SessionPrefs.tpUbwcMode(activity)
|
||||
}
|
||||
|
||||
fun restartApp() {
|
||||
if (SessionState.running) return
|
||||
if (restartBlocked()) return
|
||||
val launch = activity.packageManager.getLaunchIntentForPackage(activity.packageName) ?: return
|
||||
activity.startActivity(android.content.Intent.makeRestartActivityTask(launch.component)
|
||||
.putExtra(com.droiddeck.launcher.stores.download.StoreDownloadService.EXTRA_NAV, "components"))
|
||||
@@ -142,7 +154,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
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("", activity.getString(R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false)
|
||||
if (id.isEmpty()) DriverRow("", activity.getString(if (gpu.family == GpuInfo.Family.POWERVR) R.string.gpu_system_powervr else R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false)
|
||||
else bundleRow(id, linux = true) ?: DriverRow(
|
||||
id, lm.getDriverName(id),
|
||||
listOfNotNull(
|
||||
@@ -177,6 +189,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
|
||||
fun summary(): String {
|
||||
val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle
|
||||
?: if (SystemVulkanDriver.usesDefault(linuxSelected) && androidSelected == TurnipDriver.AUTO) activity.getString(R.string.gpu_system_powervr) else null
|
||||
val display = displaySlot().name
|
||||
val base = when {
|
||||
uiMode == GPU_MODE_AUTO -> activity.getString(R.string.drivers_summary, activity.getString(R.string.common_auto), active ?: display)
|
||||
@@ -193,17 +206,28 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
|
||||
val recommended = if (QuestExperimental.isSupported()) null else recommendedKey(pairs)
|
||||
pairRows = pairs.map { p ->
|
||||
val bundle = p.bundle?.let { installedBundle(it) }
|
||||
val nad = DriverPairs.isNonAdreno(p.key)
|
||||
val bundle = p.bundle?.let { if (nad) installedBundleAnyBuild(it) else installedBundle(it) }
|
||||
val displayId = if (p.bundle != null) bundle?.androidId else p.display?.let { TurnipReleases.installedId(activity, it, td::isInstalled) }
|
||||
val linuxId = if (p.bundle != null) bundle?.linuxId else p.linux?.let { TurnipReleases.installedId(activity, it, lm::isInstalled) }
|
||||
val mb = p.assets.sumOf { it.size } / 1_048_576.0
|
||||
val update = nad && bundle != null && p.bundle?.let { TurnipReleases.updateAvailable(activity, it) } == true
|
||||
val builtOn = if (nad) p.bundle?.builtOn.orEmpty() else ""
|
||||
val version = when {
|
||||
!nad -> p.version
|
||||
bundle != null -> bundle.version.ifEmpty { p.version }
|
||||
builtOn.isNotEmpty() -> activity.getString(R.string.drivers_built_on, builtOn)
|
||||
else -> p.version
|
||||
}
|
||||
PairRow(
|
||||
key = p.key, name = p.name, version = p.version,
|
||||
key = p.key, name = p.name, version = version, update = if (update) builtOn.takeIf { it.isNotEmpty() }?.let { activity.getString(R.string.drivers_built_on, it) } ?: p.version else null,
|
||||
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 &&
|
||||
SessionPrefs.androidDriver(activity) == displayId && SessionPrefs.linuxDriver(activity) == linuxId,
|
||||
active = displayId != null && linuxId != null && SessionPrefs.linuxDriver(activity) == linuxId &&
|
||||
(SessionPrefs.androidDriver(activity) == displayId ||
|
||||
// A Mali pair under Auto: the display is the phone's own driver.
|
||||
(gpu.family.mali && DriverPairs.isNonAdreno(p.key) && SessionPrefs.androidDriver(activity) == TurnipDriver.AUTO)),
|
||||
)
|
||||
}.sortedByDescending { it.recommended }
|
||||
}
|
||||
@@ -237,7 +261,9 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
private fun releasesStale(): Boolean {
|
||||
val cached = TurnipReleases.cached(activity)
|
||||
return cached == null || System.currentTimeMillis() - cached.checkedAt > RELEASE_CHECK_INTERVAL_MS ||
|
||||
cached.assets.none { it.pair.isNotEmpty() }
|
||||
cached.assets.none { it.pair.isNotEmpty() } ||
|
||||
// A list checked before this device's open non-Adreno drivers were offered has none of them.
|
||||
(!DeviceSupport.adreno() && cached.assets.none { DriverPairs.isNonAdreno(it.pair) })
|
||||
}
|
||||
|
||||
fun ensureAuto(force: Boolean) {
|
||||
@@ -247,9 +273,33 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
if (force) autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
return
|
||||
}
|
||||
if (gpu.family == GpuInfo.Family.POWERVR && !SystemVulkanDriver.isXclipse()) {
|
||||
// Auto owns both choices. Manual imports remain available when Manual is selected.
|
||||
SessionPrefs.ensurePowerVrAutoDrivers(activity)
|
||||
autoStatus = activity.getString(R.string.drivers_powervr_builtin)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
// A Mali pair set by an earlier build put its PanVK Android half on the display, which fails
|
||||
// on Mali gralloc. Under Auto the display is the phone's own driver (Android's loader).
|
||||
if (gpu.family.mali) {
|
||||
// Builds before 0.5.0-nad1 turned the bridge on under Auto without recording it, and
|
||||
// left it on for good (it wins over the pair). Under Auto that switch is taken as Auto's.
|
||||
if (!SessionPrefs.maliFallbackKnown(activity))
|
||||
SessionPrefs.setMaliFallbackByAuto(activity, if (SessionPrefs.vkBridge(activity)) "bridge" else "")
|
||||
// A lone imported PanVK Android driver set by hand stalls the display the same way
|
||||
// ("black screen with the start-up logo"), so under Auto any imported display goes.
|
||||
val display = SessionPrefs.androidDriver(activity)
|
||||
if (display != TurnipDriver.AUTO) {
|
||||
SessionPrefs.setAndroidDriver(activity, TurnipDriver.AUTO)
|
||||
Log.i(TAG, "auto: Mali display back on the phone's own driver (was $display)")
|
||||
}
|
||||
}
|
||||
AutoDriverTrial.rollbackDue(activity)?.let { trial ->
|
||||
AutoDriverTrial.rollBack(activity, trial)
|
||||
autoStatus = activity.getString(R.string.drivers_rolled_back, trial.name)
|
||||
// The drivers before the trial on a Mali are the runtime's Turnip, which finds no GPU there.
|
||||
if (gpu.family.mali && maliHasNoRoute()) useMaliFallback(trial.name)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
@@ -263,8 +313,31 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
if (gpu.family.mali && DriverPairs.recommendedKey(gpu, emptyList()) == null &&
|
||||
com.droiddeck.launcher.gpu.VulkanInfo.cachedOrNull() == null) {
|
||||
releaseChecking = true
|
||||
autoStatus = activity.getString(R.string.drivers_checking_latest)
|
||||
Thread({
|
||||
com.droiddeck.launcher.gpu.VulkanInfo.query()
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
gpu = GpuInfo.detect()
|
||||
Log.i(TAG, "auto: Mali named by its driver: ${gpu.name} -> ${gpu.family}")
|
||||
lastAutoCheck = 0L
|
||||
ensureAuto(force)
|
||||
}
|
||||
}, "gpu-name").start()
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "auto: ${gpu.name} (${gpu.family}) -> " + (DriverPairs.recommendedKey(gpu, emptyList()) ?: "no pair"))
|
||||
if (gpu.family.mali && DriverPairs.recommendedKey(gpu, emptyList()) == null) {
|
||||
// No PanVK build covers this Mali (e.g. the G1 series, arch v14): the phone's own driver.
|
||||
autoStatus = useMaliFallback(null)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
if (DriverPairs.recommendedKey(gpu, emptyList()) == null) {
|
||||
autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText(activity).lowercase())
|
||||
autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText(activity, com.droiddeck.launcher.core.DeviceSupport.vkBridge(activity)).lowercase())
|
||||
return
|
||||
}
|
||||
releaseChecking = true
|
||||
@@ -275,6 +348,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
runCatching { com.droiddeck.launcher.gpu.VulkanInfo.query() }
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
if (gpu.family == GpuInfo.Family.POWERVR) gpu = GpuInfo.detect()
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
|
||||
@@ -284,10 +358,37 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
when {
|
||||
row?.active == true -> autoStatus = problem?.let { activity.getString(R.string.drivers_up_to_date_check_failed, it) }
|
||||
?: activity.getString(R.string.upd_current_status)
|
||||
gpu.family == GpuInfo.Family.POWERVR && (pair == null || !pair.complete) -> {
|
||||
// RADV is not published right now: the Xclipse falls back to the system route.
|
||||
SessionPrefs.ensurePowerVrAutoDrivers(activity)
|
||||
autoStatus = activity.getString(R.string.drivers_powervr_builtin)
|
||||
refreshDrivers()
|
||||
}
|
||||
gpu.family.mali && (pair == null || !pair.complete) &&
|
||||
(problem != null || TurnipReleases.cached(activity)?.failed?.contains(TurnipReleases.NON_ADRENO_LABEL) == true) -> {
|
||||
// The list could not be read: nothing is known to be missing, so a working route stays.
|
||||
// With no open driver, Venus or bridge the session would run the runtime's Turnip,
|
||||
// which finds no GPU on a Mali, so the stand-in goes on anyway.
|
||||
autoStatus = activity.getString(R.string.upd_couldnt_check, problem ?: TurnipReleases.NON_ADRENO_LABEL)
|
||||
if (maliHasNoRoute()) {
|
||||
useMaliFallback(null)
|
||||
refreshDrivers()
|
||||
}
|
||||
}
|
||||
gpu.family.mali && (pair == null || !pair.complete) -> {
|
||||
// No open driver for this Mali published yet: the phone's own driver until it is.
|
||||
autoStatus = useMaliFallback(key?.let { DriverPairs.nonAdrenoName(it) } ?: gpu.name)
|
||||
refreshDrivers()
|
||||
}
|
||||
pair == null || !pair.complete ->
|
||||
autoStatus = problem?.let { activity.getString(R.string.upd_couldnt_check, it) } ?: activity.getString(R.string.drivers_recommended_unpublished)
|
||||
AutoDriverTrial.release(pair) in AutoDriverTrial.bad(activity) ->
|
||||
AutoDriverTrial.release(pair) in AutoDriverTrial.bad(activity) -> {
|
||||
autoStatus = activity.getString(R.string.drivers_skipping_bad, pair.name, pair.version)
|
||||
if (gpu.family.mali && maliHasNoRoute()) {
|
||||
useMaliFallback(pair.name)
|
||||
refreshDrivers()
|
||||
}
|
||||
}
|
||||
SessionState.running -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
holdBack && row?.installed != true -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_network, pair.name, pair.version)
|
||||
else -> installPair(pair, auto = true)
|
||||
@@ -296,6 +397,53 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
}, "gpu-driver-auto").start()
|
||||
}
|
||||
|
||||
/**
|
||||
* A Mali with no open driver pair to install ([missing] names the unpublished pair, or null when
|
||||
* no PanVK build covers this GPU) runs through the phone's own driver: Mali (Venus) where this
|
||||
* apk carries it (Steam runs on it from the G57 to the G1 series), the Mali bridge otherwise.
|
||||
* Venus needs Vulkan 1.2 from the phone's driver; the bridge raises a Vulkan 1.1 driver
|
||||
* (r32p1 on the G77, G78 and G610) for gamescope itself, so there the bridge stands in.
|
||||
* The Linux driver choice is left alone (it is the bridge's fallback), and Auto
|
||||
* switches only its own choice off again once a pair is installed ([endMaliFallback]).
|
||||
*/
|
||||
private fun useMaliFallback(missing: String?): String {
|
||||
val oldVulkan = com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(activity)
|
||||
val venus = oldVulkan == null && com.droiddeck.launcher.session.VenusComponent.available(activity)
|
||||
if (oldVulkan != null) Log.i(TAG, "auto: Mali driver reports Vulkan $oldVulkan; Venus needs 1.2, the bridge stands in")
|
||||
if (venus && !SessionPrefs.venus(activity)) {
|
||||
SessionPrefs.setVenus(activity, true)
|
||||
SessionPrefs.setMaliFallbackByAuto(activity, "venus")
|
||||
} else if (!venus && !SessionPrefs.vkBridge(activity)) {
|
||||
SessionPrefs.setVkBridge(activity, true)
|
||||
SessionPrefs.setMaliFallbackByAuto(activity, "bridge")
|
||||
}
|
||||
val route = activity.getString(if (venus) R.string.drivers_route_venus else R.string.drivers_route_bridge)
|
||||
Log.i(TAG, "auto: Mali stand-in route $route (missing pair: ${missing ?: "none covers ${gpu.name}"})")
|
||||
if (oldVulkan != null) return activity.getString(R.string.drivers_mali_old_vulkan_bridge, missing ?: gpu.name, oldVulkan)
|
||||
return if (missing == null) activity.getString(R.string.drivers_mali_no_pair_auto, route)
|
||||
else activity.getString(R.string.drivers_mali_unpublished_auto, missing, route)
|
||||
}
|
||||
|
||||
/** Neither an installed open Linux driver nor Venus or the bridge: the runtime's Turnip would run. */
|
||||
private fun maliHasNoRoute(): Boolean {
|
||||
if (SessionPrefs.vkBridge(activity)) return false
|
||||
// Venus on a Vulkan 1.1 driver does not start (VenusComponent.wanted), so it is no route.
|
||||
if (SessionPrefs.venus(activity) && com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(activity) == null) return false
|
||||
val linux = SessionPrefs.linuxDriver(activity)
|
||||
return linux.isEmpty() || !LinuxVulkanDriverManager(activity).isInstalled(linux)
|
||||
}
|
||||
|
||||
/** A pair is installed for this Mali: the route Auto switched on in its place goes off again. */
|
||||
private fun endMaliFallback() {
|
||||
when (SessionPrefs.maliFallbackByAuto(activity)) {
|
||||
"venus" -> SessionPrefs.setVenus(activity, false)
|
||||
"bridge" -> SessionPrefs.setVkBridge(activity, false)
|
||||
else -> return
|
||||
}
|
||||
Log.i(TAG, "auto: pair installed; ${SessionPrefs.maliFallbackByAuto(activity)} (Auto's stand-in) off")
|
||||
SessionPrefs.setMaliFallbackByAuto(activity, "")
|
||||
}
|
||||
|
||||
fun selectPair(key: String) {
|
||||
if (pairBusy != null) return
|
||||
val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return
|
||||
@@ -304,7 +452,15 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
installPair(pair, auto = false)
|
||||
}
|
||||
|
||||
private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean) {
|
||||
/** A newer build of an installed open non-Adreno pair: replaced in place, still selected if it was. */
|
||||
fun updatePair(key: String) {
|
||||
if (pairBusy != null) return
|
||||
val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return
|
||||
val wasActive = pairRows.firstOrNull { it.key == key }?.active == true
|
||||
installPair(pair, auto = wasActive && mode == SessionPrefs.GPU_DRIVERS_AUTO, force = true, select = wasActive)
|
||||
}
|
||||
|
||||
private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean, force: Boolean = false, select: Boolean = true) {
|
||||
if (!pair.complete) return
|
||||
pairBusy = pair.key
|
||||
pairPercent = 0
|
||||
@@ -314,7 +470,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val result = runCatching {
|
||||
val zips = pair.assets
|
||||
val ids = zips.mapIndexed { i, asset ->
|
||||
installedIdOf(asset)
|
||||
(if (force) null else installedIdOf(asset))
|
||||
?: installAsset(asset) { pct -> ui.post { pairPercent = (i * 100 + pct) / zips.size } }.also { downloaded = true }
|
||||
}
|
||||
if (pair.bundle != null) DriverBundle.get(activity, ids[0])!!.let { listOf(it.androidId, it.linuxId) } else ids
|
||||
@@ -322,23 +478,39 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
ui.post {
|
||||
pairBusy = null
|
||||
pairPercent = -1
|
||||
result.onSuccess { (displayId, linuxId) ->
|
||||
result.onSuccess { (pairDisplayId, linuxId) ->
|
||||
if (!select) {
|
||||
android.widget.Toast.makeText(activity, activity.getString(R.string.drivers_pair_updated, pair.name), android.widget.Toast.LENGTH_LONG).show()
|
||||
return@onSuccess
|
||||
}
|
||||
if (auto && SessionState.running) {
|
||||
lastAutoCheck = 0L
|
||||
autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
return@onSuccess
|
||||
}
|
||||
// A Mali pair under Auto: the compositor draws on the phone's own Mali driver
|
||||
// (dma-buf, modifiers and AHB all work there) and the guest runs the pair's
|
||||
// PanVK. Its Android half stays installed for a manual pick.
|
||||
// The same when the pair is picked by hand: a PanVK Android half on the display
|
||||
// stalls Mali gralloc ("Waiting for Steam's first frame"); it stays a manual pick
|
||||
// under "Runtime and display drivers separately".
|
||||
val displayId = if (gpu.family.mali && DriverPairs.isNonAdreno(pair.key)) TurnipDriver.AUTO else pairDisplayId
|
||||
val previousDisplay = SessionPrefs.androidDriver(activity)
|
||||
val previousLinux = SessionPrefs.linuxDriver(activity)
|
||||
val displayChanged = previousDisplay != displayId
|
||||
SessionPrefs.setAndroidDriver(activity, displayId)
|
||||
SessionPrefs.setLinuxDriver(activity, linuxId)
|
||||
// A route Auto switched on in place of a missing pair goes off now that one is
|
||||
// here. A Mali bridge or Venus switch the user set stays: it wins for the session
|
||||
// (DeviceSupport.vkBridge, VenusComponent.wanted) and the pair waits.
|
||||
if (gpu.family.mali && DriverPairs.isNonAdreno(pair.key)) endMaliFallback()
|
||||
val restart = displayChanged && CompositorHost.isStarted
|
||||
if (auto) {
|
||||
val trial = AutoDriverTrial.current(activity)
|
||||
val lastGood = trial?.let { setOf(it.previousDisplay, it.previousLinux) } ?: setOf(previousDisplay, previousLinux)
|
||||
val changed = previousDisplay != displayId || previousLinux != linuxId
|
||||
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId))
|
||||
// A Mali pair's display is the phone's own driver (AUTO): not a download to own or prune.
|
||||
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId).filter { it.isNotEmpty() && it != TurnipDriver.AUTO })
|
||||
if (changed) AutoDriverTrial.begin(
|
||||
activity, AutoDriverTrial.release(pair), "${pair.name} ${pair.version}", displayId, linuxId,
|
||||
trial?.previousDisplay ?: previousDisplay, trial?.previousLinux ?: previousLinux,
|
||||
@@ -366,6 +538,9 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
private fun installedBundle(asset: TurnipReleases.Asset): DriverBundle.Bundle? =
|
||||
TurnipReleases.installedId(activity, asset) { DriverBundle.get(activity, it) != null }?.let { DriverBundle.get(activity, it) }
|
||||
|
||||
private fun installedBundleAnyBuild(asset: TurnipReleases.Asset): DriverBundle.Bundle? =
|
||||
TurnipReleases.installedIdAnyBuild(activity, asset) { DriverBundle.get(activity, it) != null }?.let { DriverBundle.get(activity, it) }
|
||||
|
||||
private fun installedIdOf(asset: TurnipReleases.Asset): String? = when {
|
||||
asset.bundle -> installedBundle(asset)?.id
|
||||
asset.linux -> TurnipReleases.installedId(activity, asset, LinuxVulkanDriverManager(activity)::isInstalled)
|
||||
|
||||
@@ -40,6 +40,8 @@ import com.droiddeck.launcher.ui.PackageRow
|
||||
import com.droiddeck.launcher.ui.exposeTestTags
|
||||
import com.droiddeck.launcher.session.OfflineMode
|
||||
import com.droiddeck.launcher.session.ProtonExtras
|
||||
import com.droiddeck.launcher.session.ComponentUpdater
|
||||
import com.droiddeck.launcher.session.ComponentUpdates
|
||||
import com.droiddeck.launcher.session.ComponentsManager
|
||||
import com.droiddeck.launcher.ui.ComponentsPage
|
||||
import com.droiddeck.launcher.session.GameSaves
|
||||
@@ -61,9 +63,7 @@ import com.droiddeck.launcher.ui.ModeSettings
|
||||
import com.droiddeck.launcher.ui.ModeSettingsActions
|
||||
import com.droiddeck.launcher.ui.ConfirmDialog
|
||||
import com.droiddeck.launcher.ui.ControllerActions
|
||||
import com.droiddeck.launcher.ui.ControllerMappingPage
|
||||
import com.droiddeck.launcher.input.ControllerPrefs
|
||||
import com.droiddeck.launcher.input.ControllerEditorActivity
|
||||
import com.droiddeck.launcher.ui.FrontEndScreen
|
||||
import com.droiddeck.launcher.ui.FrontEndState
|
||||
import com.droiddeck.launcher.ui.FrontEndActions
|
||||
@@ -79,6 +79,7 @@ import com.droiddeck.launcher.session.SessionArtifacts
|
||||
import com.droiddeck.launcher.session.SessionPhase
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import com.droiddeck.launcher.session.GameStorage
|
||||
import com.droiddeck.launcher.session.SteamLibraries
|
||||
import com.droiddeck.launcher.input.SecondScreenDisplay
|
||||
import com.droiddeck.launcher.input.SecondScreenDisplays
|
||||
|
||||
@@ -93,7 +94,11 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private val components = ComponentsMenu(this, ui)
|
||||
private val decky = DeckyMenu(this, ui)
|
||||
private val updates = UpdatesMenu(this, ui)
|
||||
private val protons = ProtonMenu(this, ui) { !busy && flood == null && compatLaunch == null && pkgStage == null && components.compBusy == null }
|
||||
private val protons = ProtonMenu(
|
||||
this, ui,
|
||||
idle = { !busy && flood == null && compatLaunch == null && pkgStage == null && components.compBusy == null },
|
||||
onInstalled = { if (!OfflineMode.enabled(this)) components.checkComponentUpdates(force = false) },
|
||||
)
|
||||
|
||||
private var installed by mutableStateOf<String?>(null)
|
||||
private var flood by mutableStateOf<PendingFlood?>(null)
|
||||
@@ -162,6 +167,12 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var gameCores by mutableStateOf<Set<Int>>(emptySet())
|
||||
private var tuSysmem by mutableStateOf(false)
|
||||
private var zinkLazy by mutableStateOf(false)
|
||||
private var vkBridge by mutableStateOf(false)
|
||||
private var venus by mutableStateOf(false)
|
||||
private var venusHostVulkan by mutableStateOf<String?>(null)
|
||||
private var vkBridgeSafe by mutableStateOf(true)
|
||||
private var bridgeProtonDxvk by mutableStateOf(false)
|
||||
private val adrenoGpu by lazy { com.droiddeck.launcher.core.DeviceSupport.adreno() }
|
||||
private var noXalia by mutableStateOf(true)
|
||||
private var fastSync by mutableStateOf(false)
|
||||
private var syncFallback by mutableStateOf(true)
|
||||
@@ -193,6 +204,8 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var gameStoresEnabled by mutableStateOf(true)
|
||||
private var gameStoresSpeedTier by mutableStateOf("fast")
|
||||
private var navRequest by mutableStateOf<String?>(null)
|
||||
private val gameLaunchRequest = java.util.concurrent.atomic.AtomicLong()
|
||||
private var preparingGames by mutableStateOf(emptySet<String>())
|
||||
private var storesShowMature by mutableStateOf(false)
|
||||
private var storesSort by mutableStateOf(SessionPrefs.STORES_SORTS.first())
|
||||
private var storesView by mutableStateOf(SessionPrefs.DEFAULT_STORES_VIEW)
|
||||
@@ -204,6 +217,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var wifiDiscoveryBlocked by mutableStateOf(false)
|
||||
private var integrationState by mutableStateOf(SteamIntegration.State())
|
||||
private var integrationFocus by mutableStateOf<SteamIntegration.Feature?>(null)
|
||||
private var pendingSessionLogExport: File? = null
|
||||
private val wifiLocationReceiver = object : android.content.BroadcastReceiver() {
|
||||
override fun onReceive(context: android.content.Context?, intent: Intent?) {
|
||||
refreshWifiDiscovery()
|
||||
@@ -262,6 +276,26 @@ class FrontEndActivity : ComponentActivity() {
|
||||
val then = onSavePicked.also { onSavePicked = null } ?: return@registerForActivityResult
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedFile(r.data)?.let(then)
|
||||
}
|
||||
private val pickSessionLogExport = registerForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { uri ->
|
||||
val folder = pendingSessionLogExport.also { pendingSessionLogExport = null } ?: return@registerForActivityResult
|
||||
if (uri != null) {
|
||||
SessionLogShare.prepare(this, { folder }) { zip ->
|
||||
if (zip == null) {
|
||||
android.widget.Toast.makeText(this, R.string.setup_log_export_failed, android.widget.Toast.LENGTH_LONG).show()
|
||||
} else Thread({
|
||||
val saved = runCatching {
|
||||
contentResolver.openOutputStream(uri)?.use { output -> zip.inputStream().buffered().use { it.copyTo(output) } }
|
||||
?: error("Could not open destination")
|
||||
}.isSuccess
|
||||
ui.post {
|
||||
android.widget.Toast.makeText(this,
|
||||
if (saved) R.string.setup_log_export_saved else R.string.setup_log_export_failed,
|
||||
android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "save-session-logs").start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshWifiDiscovery() {
|
||||
wifiDiscoveryPermission = WifiDiscovery.permissionGranted(this)
|
||||
@@ -316,18 +350,11 @@ class FrontEndActivity : ComponentActivity() {
|
||||
refreshWifiDiscovery()
|
||||
}
|
||||
SteamIntegration.Feature.BRIGHTNESS -> integrationSettings(SteamIntegration.brightnessSettings(this))
|
||||
SteamIntegration.Feature.HOME -> if (HomeApp.isDefault(this)) HomeApp.openSystemHomeSettings(this) else {
|
||||
HomeApp.setHomeScreenEnabled(this, true)
|
||||
refreshHomeAppState()
|
||||
manageHomeApp()
|
||||
}
|
||||
SteamIntegration.Feature.HOME -> if (HomeApp.isDefault(this)) HomeApp.openSystemHomeSettings(this) else offerHomeScreen()
|
||||
SteamIntegration.Feature.SHORTCUTS -> {
|
||||
if (!integrationState.shortcuts) shortcutsTried = true
|
||||
if (!HomeApp.isDefault(this)) configureIntegration(SteamIntegration.Feature.HOME)
|
||||
else {
|
||||
SteamIntegration.setShortcutsListed(this, true)
|
||||
integrationSettings(SteamIntegration.shortcutSettings(this), android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||
}
|
||||
SteamIntegration.setShortcutsListed(this, true)
|
||||
integrationSettings(SteamIntegration.shortcutSettings(this), android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,6 +440,27 @@ class FrontEndActivity : ComponentActivity() {
|
||||
pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), getString(R.string.main_saves_pick_zip, name), GameSaves.savesDir().parentFile?.parentFile?.path))
|
||||
}
|
||||
|
||||
private fun importAddons(name: String, target: com.droiddeck.launcher.runtime.WowAddons.Target) {
|
||||
if (SessionState.running) {
|
||||
android.widget.Toast.makeText(this, R.string.main_addons_close_session, android.widget.Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
onSavePicked = { zip ->
|
||||
saveAction(getString(R.string.main_addons_importing, name)) {
|
||||
if (SessionState.running) return@saveAction getString(R.string.main_addons_close_session)
|
||||
val imported = try {
|
||||
com.droiddeck.launcher.runtime.WowAddons.import(zip, target)
|
||||
} catch (_: com.droiddeck.launcher.runtime.WowAddons.TooLarge) {
|
||||
return@saveAction getString(R.string.main_addons_too_large, zip.name)
|
||||
}
|
||||
if (imported.isEmpty()) getString(R.string.main_addons_none, zip.name)
|
||||
else resources.getQuantityString(R.plurals.main_addons_imported, imported.size, imported.size, target.flavor)
|
||||
}
|
||||
}
|
||||
val downloads = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS).path
|
||||
pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), getString(R.string.main_addons_pick_zip, name), downloads))
|
||||
}
|
||||
|
||||
private fun exportSaves(name: String, layout: GameSaves.Layout, game: () -> GameSaves.Game) {
|
||||
onSavePicked = { dir ->
|
||||
saveAction(getString(R.string.main_saves_exporting, name)) {
|
||||
@@ -460,7 +508,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var forceSsbs by mutableStateOf(false)
|
||||
private var steamClientArch by mutableStateOf("arm64")
|
||||
private var runSteamAtStartup by mutableStateOf(false)
|
||||
private var startupSession by mutableStateOf(SessionPrefs.STARTUP_OFF)
|
||||
private var bootMovie by mutableStateOf(com.droiddeck.launcher.session.BootMovie.DROIDDECK_30)
|
||||
private var bootMovieSound by mutableStateOf(true)
|
||||
private var bootMovieCustomName by mutableStateOf<String?>(null)
|
||||
@@ -476,6 +524,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var suspendPolicy by mutableStateOf(SessionPrefs.SUSPEND_MANUAL)
|
||||
private var steamDownloadsInBackground by mutableStateOf(false)
|
||||
private var oscMode by mutableStateOf(SessionPrefs.OSC_AUTO)
|
||||
private var backHold by mutableStateOf(true)
|
||||
private var backActionsInverted by mutableStateOf(false)
|
||||
private var gameStorage by mutableStateOf("")
|
||||
private var slowUsb by mutableStateOf<com.droiddeck.launcher.session.UsbLink.SlowDrive?>(null)
|
||||
@@ -493,6 +542,40 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private val pickGameStorage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedPath(r.data)?.let { path -> setGameStorage(path, GameStorage.labelFor(this, path)) }
|
||||
}
|
||||
private var steamLibraries by mutableStateOf<List<SteamLibraries.Entry>>(emptyList())
|
||||
private var foundSteamLibraries by mutableStateOf<List<SteamLibraries.Found>>(emptyList())
|
||||
private var allFilesAccessMissing by mutableStateOf(false)
|
||||
private var steamLibraryScan = 0
|
||||
private val pickSteamLibrary = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
val path = if (r.resultCode == RESULT_OK) InAppFilePicker.pickedPath(r.data) else null
|
||||
if (path != null) addSteamLibrary(path)
|
||||
}
|
||||
|
||||
private fun addSteamLibrary(path: String) {
|
||||
Thread({
|
||||
val result = SteamLibraries.add(this, path)
|
||||
ui.post {
|
||||
steamLibraries = SteamLibraries.list(this)
|
||||
refreshFoundSteamLibraries()
|
||||
refresh()
|
||||
when {
|
||||
result is SteamLibraries.AddResult.Refused ->
|
||||
android.widget.Toast.makeText(this, getString(R.string.main_storage_not_usable, result.reason), android.widget.Toast.LENGTH_LONG).show()
|
||||
result is SteamLibraries.AddResult.Added && result.entry.path != path ->
|
||||
android.widget.Toast.makeText(this, getString(R.string.libraries_used_inner, result.entry.label), android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}, "steam-library-add").start()
|
||||
}
|
||||
|
||||
private fun refreshFoundSteamLibraries() {
|
||||
allFilesAccessMissing = com.droiddeck.launcher.files.AllFilesAccess.missing()
|
||||
val scan = ++steamLibraryScan
|
||||
Thread({
|
||||
val found = SteamLibraries.found(this)
|
||||
ui.post { if (scan == steamLibraryScan) foundSteamLibraries = found }
|
||||
}, "steam-library-scan").start()
|
||||
}
|
||||
private var romsDir by mutableStateOf<String?>(null)
|
||||
private var steamGames by mutableStateOf<List<Library.SteamGame>>(emptyList())
|
||||
private var steamGamesX64 by mutableStateOf(false)
|
||||
@@ -501,10 +584,15 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var emulatorList by mutableStateOf<List<Library.Emulator>>(emptyList())
|
||||
private var runningLabel by mutableStateOf<String?>(null)
|
||||
private var logsEnabled by mutableStateOf(true)
|
||||
private var enhancedDiagnosticsArmed by mutableStateOf(false)
|
||||
private var gameDebugLogsEnabled by mutableStateOf(false)
|
||||
private var gameDebugLogsNeedsRestart by mutableStateOf(false)
|
||||
private var vacDiagnostics by mutableStateOf(false)
|
||||
private var agentCommands by mutableStateOf(false)
|
||||
private var graphicsLogs by mutableStateOf(false)
|
||||
private var steamGpuDrawing by mutableStateOf(false)
|
||||
private var bridgeFellBack by mutableStateOf(false)
|
||||
private var bridgeFellBackMemory by mutableStateOf(false)
|
||||
private var clearCachedApps by mutableStateOf(false)
|
||||
private var adbPaired by mutableStateOf(false)
|
||||
private var memoryHud by mutableStateOf(SessionPrefs.MEMORY_HUD_OFF)
|
||||
@@ -533,7 +621,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var dualDisplayId by mutableStateOf(-1)
|
||||
private var dualScreenDevice by mutableStateOf(false)
|
||||
private var dualScreenMode by mutableStateOf(com.droiddeck.launcher.input.DualScreen.MODE_AUTO)
|
||||
private var dualScreenLauncher by mutableStateOf(true)
|
||||
private var dualScreenSessions by mutableStateOf(com.droiddeck.launcher.input.DualScreen.MODE_AUTO)
|
||||
private var dualScreenControls by mutableStateOf(true)
|
||||
private var launcherStarted = false
|
||||
private lateinit var displayManager: DisplayManager
|
||||
@@ -542,6 +630,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
override fun onDisplayRemoved(displayId: Int) = refreshSecondScreenDisplays()
|
||||
override fun onDisplayChanged(displayId: Int) = refreshSecondScreenDisplays()
|
||||
}
|
||||
private val bottomScreenListener: () -> Unit = { refreshSecondScreenDisplays() }
|
||||
private val secondScreenTouchListener = object : android.hardware.input.InputManager.InputDeviceListener {
|
||||
override fun onInputDeviceAdded(deviceId: Int) = refreshSecondScreenDisplays()
|
||||
override fun onInputDeviceRemoved(deviceId: Int) = refreshSecondScreenDisplays()
|
||||
@@ -549,8 +638,17 @@ class FrontEndActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private val homeRoleRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
homeChoiceOpen = false
|
||||
HomeApp.releaseUnusedHomeEntry(this)
|
||||
refreshHomeAppState()
|
||||
}
|
||||
private val gameSpaceAdd = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
homeChoiceOpen = false
|
||||
if (HomeApp.inGameSpace(this)) enableHomeScreen()
|
||||
else refreshHomeAppState()
|
||||
}
|
||||
private var homeChoiceOpen = false
|
||||
private var gameSpaceOffered = false
|
||||
|
||||
override fun startActivity(intent: Intent?) {
|
||||
val compat = protons.protonBusyId != null || ProtonExtras.installInProgress
|
||||
@@ -716,7 +814,6 @@ class FrontEndActivity : ComponentActivity() {
|
||||
LauncherIcon.set(this, style)
|
||||
launcherIcon = LauncherIcon.current(this)
|
||||
if (launcherIcon == before) return false
|
||||
// Android finishes activities started through the alias that was just disabled.
|
||||
startActivity(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
.setComponent(LauncherIcon.activity(this))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
@@ -985,6 +1082,11 @@ class FrontEndActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
drivers.restartBlocked = ::runtimeChangesBlocked
|
||||
drivers.loadTpUbwcMode()
|
||||
pendingSessionLogExport = savedInstanceState?.getString(KEY_PENDING_SESSION_LOG_EXPORT)?.let(::File)
|
||||
homeChoiceOpen = savedInstanceState?.getBoolean(STATE_HOME_CHOICE_OPEN) == true
|
||||
gameSpaceOffered = savedInstanceState?.getBoolean(STATE_GAME_SPACE_OFFERED) == true
|
||||
if (savedInstanceState == null) readIntegrationIntent(intent)
|
||||
readGameIntent(intent)
|
||||
gameSyncFolder = GameFileSync.folder(this)
|
||||
@@ -1040,16 +1142,21 @@ class FrontEndActivity : ComponentActivity() {
|
||||
FrontEndScreen(
|
||||
FrontEndState(
|
||||
installed = installed, ready = ready, available = available?.version, removalPending = removalPending, runtimeActionsBlocked = runtimeChangesBlocked(),
|
||||
vkBridge = vkBridge, venus = venus, venusHostVulkan = venusHostVulkan, vkBridgeSafe = vkBridgeSafe, bridgeProtonDxvk = bridgeProtonDxvk, adreno = adrenoGpu,
|
||||
shortcutPicker = shortcutPicker,
|
||||
preparingGames = preparingGames,
|
||||
shortcutLibraryScanning = shortcutLibraryScanning,
|
||||
gameSyncFolder = gameSyncFolder,
|
||||
busy = busy, stage = stage, percent = percent,
|
||||
desktopInstalled = desktopInstalled, desktopPresent = desktopPresent,
|
||||
offlineAccount = offlineAccount, offline = offline,
|
||||
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled,
|
||||
enhancedDiagnosticsArmed = enhancedDiagnosticsArmed,
|
||||
gameDebugLogsEnabled = gameDebugLogsEnabled, gameDebugLogsNeedsRestart = gameDebugLogsNeedsRestart,
|
||||
vacDiagnostics = vacDiagnostics, agentCommands = agentCommands,
|
||||
graphicsLogs = graphicsLogs, steamGpuDrawing = steamGpuDrawing, bridgeFellBack = bridgeFellBack, bridgeFellBackMemory = bridgeFellBackMemory,
|
||||
clearCachedApps = clearCachedApps, adbPaired = adbPaired, memoryHud = memoryHud, backgroundAppLimit = backgroundAppLimit,
|
||||
tpUbwcMode = drivers.tpUbwcModeForDevice(), tpUbwcRestartRequired = drivers.tpUbwcRestartRequired(), canRestartApp = !runtimeChangesBlocked(),
|
||||
steamGames = steamGames, steamGamesX64 = steamGamesX64, emulators = emulatorList, running = runningLabel,
|
||||
frameGen = FrameGen.mode(this),
|
||||
lossless = lossless,
|
||||
@@ -1067,7 +1174,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
dualScreen = dualDisplayId >= 0 && com.droiddeck.launcher.ui.DualScreenPanel.available,
|
||||
dualScreenDevice = dualScreenDevice,
|
||||
dualScreenMode = dualScreenMode,
|
||||
dualScreenLauncher = dualScreenLauncher,
|
||||
dualScreenSessions = dualScreenSessions,
|
||||
dualScreenControls = dualScreenControls,
|
||||
packages = packageRows,
|
||||
packageCatalogLoading = catalogLoading,
|
||||
@@ -1117,9 +1224,26 @@ class FrontEndActivity : ComponentActivity() {
|
||||
},
|
||||
onSteamGame = { g ->
|
||||
if (shortcutPicker) chooseGameShortcut(g)
|
||||
else {
|
||||
com.droiddeck.launcher.frontend.AutoComponents.beforeLaunch(this, g)
|
||||
com.droiddeck.launcher.stores.StoreLaunch.prepare(this, g) { launchGame(g) }
|
||||
else if (g.launchKey !in preparingGames) {
|
||||
preparingGames = preparingGames + g.launchKey
|
||||
prepareAndLaunch(g, g.launchKey)
|
||||
}
|
||||
},
|
||||
onStoreGame = { store, id ->
|
||||
val key = "${store.id}:$id"
|
||||
if (key !in preparingGames) {
|
||||
preparingGames = preparingGames + key
|
||||
val app = applicationContext
|
||||
Thread({
|
||||
val g = runCatching { com.droiddeck.launcher.stores.StoreLaunch.installedGame(app, store, id) }.getOrNull()
|
||||
runOnUiThread {
|
||||
if (g != null && !isFinishing && !isDestroyed) prepareAndLaunch(g, key)
|
||||
else {
|
||||
preparingGames = preparingGames - key
|
||||
if (g == null && !isFinishing && !isDestroyed) android.widget.Toast.makeText(this, R.string.stores_launch_not_registered, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}, "store-play").start()
|
||||
}
|
||||
},
|
||||
onEpicLauncher = { openEpicLauncher() },
|
||||
@@ -1188,6 +1312,24 @@ class FrontEndActivity : ComponentActivity() {
|
||||
onInstallPackage = { id -> installPackage(id) },
|
||||
onRemovePackage = { id -> removePackage(id) },
|
||||
onRuntime = { onRuntimeButton() },
|
||||
onVkBridge = { on ->
|
||||
SessionPrefs.setVkBridge(this, on); SessionPrefs.setMaliFallbackByAuto(this, ""); vkBridge = on
|
||||
if (!on) com.droiddeck.launcher.session.ComponentsManager.settleAutoDxvk(this)
|
||||
},
|
||||
onVenus = { on ->
|
||||
SessionPrefs.setVenus(this, on)
|
||||
SessionPrefs.setMaliFallbackByAuto(this, "")
|
||||
venus = on
|
||||
if (on && !com.droiddeck.launcher.core.DeviceSupport.adreno()) Thread({
|
||||
com.droiddeck.launcher.gpu.VulkanInfo.query()["api"]?.let { SessionPrefs.setHostVulkanApi(this, it) }
|
||||
runOnUiThread { venusHostVulkan = com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(this) }
|
||||
}, "venus-host-vulkan").start()
|
||||
},
|
||||
onVkBridgeSafe = { on -> SessionPrefs.setVkBridgeSafe(this, on); vkBridgeSafe = on },
|
||||
onBridgeProtonDxvk = { on ->
|
||||
SessionPrefs.setBridgeProtonDxvk(this, on); bridgeProtonDxvk = on
|
||||
if (on) com.droiddeck.launcher.session.ComponentsManager.settleAutoDxvk(this)
|
||||
},
|
||||
onFrameGenPick = { mode ->
|
||||
FrameGen.set(this, mode)
|
||||
frameGenLabel = FrameGen.label(this)
|
||||
@@ -1200,6 +1342,13 @@ class FrontEndActivity : ComponentActivity() {
|
||||
onGameComponents = { game -> openComponents(gameKey = game.profileKey) },
|
||||
onComponentsTab = { tab -> openComponents(focusContent = true, tab = tab) },
|
||||
onSaveImport = { sg -> importSaves(sg.name) { GameSaves.game(sg) } },
|
||||
onAddonImport = { sg, target -> importAddons(sg.name, target) },
|
||||
onAddonDelete = { target, name ->
|
||||
saveAction(getString(R.string.main_addons_deleting, name)) {
|
||||
com.droiddeck.launcher.runtime.WowAddons.delete(target, name)
|
||||
getString(R.string.main_addons_deleted, name)
|
||||
}
|
||||
},
|
||||
onSaveExport = { sg, layout -> exportSaves(sg.name, layout) { GameSaves.game(sg) } },
|
||||
onPerformance = { refreshCores(); showComponents = false; showMapping = false; showPerformance = true },
|
||||
onRoms = { showRoms = true },
|
||||
@@ -1217,6 +1366,15 @@ class FrontEndActivity : ComponentActivity() {
|
||||
gameDebugLogsEnabled = SessionPrefs.gameDebugLogsEnabled(this)
|
||||
gameDebugLogsNeedsRestart = gameDebugLogsNeedsRestart()
|
||||
},
|
||||
onEnhancedDiagnostics = {
|
||||
val requested = !com.droiddeck.launcher.session.EnhancedDiagnostics.isArmed(this)
|
||||
if (!com.droiddeck.launcher.session.EnhancedDiagnostics.setArmed(this, requested)) {
|
||||
android.widget.Toast.makeText(
|
||||
this, R.string.setup_logging_update_failed, android.widget.Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
enhancedDiagnosticsArmed = com.droiddeck.launcher.session.EnhancedDiagnostics.isArmed(this)
|
||||
},
|
||||
onGameDebugLogs = {
|
||||
val changed = SessionPrefs.setGameDebugLogsEnabled(this, !SessionPrefs.gameDebugLogsEnabled(this))
|
||||
if (!changed) android.widget.Toast.makeText(
|
||||
@@ -1225,6 +1383,14 @@ class FrontEndActivity : ComponentActivity() {
|
||||
gameDebugLogsEnabled = SessionPrefs.gameDebugLogsEnabled(this)
|
||||
gameDebugLogsNeedsRestart = gameDebugLogsNeedsRestart()
|
||||
},
|
||||
onSteamGpuDrawing = {
|
||||
SessionPrefs.setSteamGpuCompositing(this, !SessionPrefs.steamGpuCompositing(this))
|
||||
steamGpuDrawing = SessionPrefs.steamGpuCompositing(this)
|
||||
},
|
||||
onGraphicsLogs = {
|
||||
SessionPrefs.setDetailedGraphicsLogs(this, !SessionPrefs.detailedGraphicsLogs(this))
|
||||
graphicsLogs = SessionPrefs.detailedGraphicsLogs(this)
|
||||
},
|
||||
onVacDiagnostics = {
|
||||
SessionPrefs.setVacDiagnostics(this, !SessionPrefs.vacDiagnostics(this))
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
@@ -1235,16 +1401,47 @@ class FrontEndActivity : ComponentActivity() {
|
||||
},
|
||||
onClearCachedApps = { on -> SessionPrefs.setClearCachedApps(this, on); clearCachedApps = on },
|
||||
onMemoryHud = { value -> SessionPrefs.setMemoryHud(this, value); memoryHud = value },
|
||||
onTpUbwcMode = { drivers.chooseTpUbwcMode(it) },
|
||||
onRestartApp = { drivers.restartApp() },
|
||||
onBackgroundAppLimit = { limit ->
|
||||
SessionPrefs.setBackgroundAppLimit(this, limit); backgroundAppLimit = limit
|
||||
if (limit == com.droiddeck.launcher.core.BackgroundAppLimit.OFF) com.droiddeck.launcher.core.BackgroundAppLimit.restoreAsync(this)
|
||||
},
|
||||
onShareLogs = {
|
||||
SessionLogShare.prepare(this, { SessionLogShare.latest(this) }) { zip ->
|
||||
val folder = SessionLogShare.latest(this)
|
||||
if (folder == null) {
|
||||
android.widget.Toast.makeText(this, R.string.main_no_logs_yet, android.widget.Toast.LENGTH_LONG).show()
|
||||
} else if (com.droiddeck.launcher.session.EnhancedDiagnostics.isActive(this, folder)) {
|
||||
SessionLogShare.prepareEnhanced(this, { folder }) { zips ->
|
||||
if (zips.isEmpty()) android.widget.Toast.makeText(this, R.string.main_no_logs_yet, android.widget.Toast.LENGTH_LONG).show()
|
||||
else shareEnhancedArchives(zips)
|
||||
}
|
||||
} else {
|
||||
SessionLogShare.prepare(this, { folder }) { zip ->
|
||||
if (zip == null) android.widget.Toast.makeText(this, R.string.main_no_logs_yet, android.widget.Toast.LENGTH_LONG).show()
|
||||
else startActivity(SessionLogShare.shareIntent(this, zip))
|
||||
}
|
||||
}
|
||||
},
|
||||
onShareSessionLog = { folder ->
|
||||
SessionLogShare.prepare(this, { folder }) { zip ->
|
||||
if (zip == null) android.widget.Toast.makeText(this, R.string.main_no_logs_yet, android.widget.Toast.LENGTH_LONG).show()
|
||||
else startActivity(SessionLogShare.shareIntent(this, zip))
|
||||
}
|
||||
},
|
||||
onSaveSessionLog = { folder ->
|
||||
pendingSessionLogExport = folder
|
||||
pickSessionLogExport.launch("DroidDeck-${folder.name}.zip")
|
||||
},
|
||||
onDeleteSessionLog = { folder, done ->
|
||||
Thread({
|
||||
val deleted = runCatching { SessionLogShare.deleteSession(this, folder) }.getOrDefault(false)
|
||||
ui.post {
|
||||
if (!deleted) android.widget.Toast.makeText(this, R.string.setup_log_delete_failed, android.widget.Toast.LENGTH_SHORT).show()
|
||||
done()
|
||||
}
|
||||
}, "delete-session-logs").start()
|
||||
},
|
||||
onClearLogs = {
|
||||
Thread({
|
||||
val cleared = runCatching { SessionArtifacts.clearAll(this) }.getOrDefault(0)
|
||||
@@ -1301,8 +1498,8 @@ class FrontEndActivity : ComponentActivity() {
|
||||
SessionPrefs.setDualScreenMode(this, mode)
|
||||
refreshSecondScreenDisplays()
|
||||
},
|
||||
onDualScreenLauncher = { on ->
|
||||
SessionPrefs.setDualScreenLauncher(this, on)
|
||||
onDualScreenSessions = { mode ->
|
||||
SessionPrefs.setDualScreenSessionMode(this, mode)
|
||||
refreshSecondScreenDisplays()
|
||||
},
|
||||
onDualScreenControls = { on ->
|
||||
@@ -1340,8 +1537,11 @@ class FrontEndActivity : ComponentActivity() {
|
||||
integrationState = readIntegration()
|
||||
},
|
||||
onHomeScreen = { on ->
|
||||
HomeApp.setHomeScreenEnabled(this, on)
|
||||
refreshHomeAppState()
|
||||
if (on && HomeApp.hasGameSpace(this)) offerHomeScreen()
|
||||
else {
|
||||
HomeApp.setHomeScreenEnabled(this, on)
|
||||
refreshHomeAppState()
|
||||
}
|
||||
},
|
||||
onAndroidApp = { app, displayId -> launchAndroidApp(app, displayId) },
|
||||
onBackActionsInverted = { inverted ->
|
||||
@@ -1405,22 +1605,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
},
|
||||
updates = updates.actions(),
|
||||
controller = ControllerActions(
|
||||
onOsc = { o -> SessionPrefs.setOscMode(this, o); oscMode = o },
|
||||
onTint = { t -> ControllerPrefs.setTint(this, t); refreshController() },
|
||||
onOpacity = { o -> ControllerPrefs.setOpacity(this, o); refreshController() },
|
||||
onSize = { v -> ControllerPrefs.setSize(this, v); refreshController() },
|
||||
onStickClick = { on -> ControllerPrefs.setStickClick(this, on); refreshController() },
|
||||
onAdaptiveSticks = { on -> ControllerPrefs.setAdaptiveSticks(this, on); refreshController() },
|
||||
onRumble = { on -> ControllerPrefs.setRumble(this, on); refreshController() },
|
||||
onSteamButton = { on -> ControllerPrefs.setSteamButton(this, on); refreshController() },
|
||||
onQamButton = { on -> ControllerPrefs.setQamButton(this, on); refreshController() },
|
||||
onKeyboardButton = { on -> ControllerPrefs.setKeyboardButton(this, on); refreshController() },
|
||||
onSelectSteam = { on -> ControllerPrefs.setSelectSteam(this, on); refreshController() },
|
||||
onJoyConVr = { on -> ControllerPrefs.setJoyConVr(this, on); refreshController() },
|
||||
onEditLayout = { startActivity(Intent(this, ControllerEditorActivity::class.java)) },
|
||||
onResetLayout = { ControllerPrefs.resetAllLayouts(this); refreshController() },
|
||||
onMapping = { showPerformance = false; showComponents = false; showMapping = true },
|
||||
onResetAll = { ControllerPrefs.resetAll(this); refreshController() },
|
||||
),
|
||||
),
|
||||
page = page,
|
||||
@@ -1507,7 +1692,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) ui.post { startSteamAtStartupIfEnabled() }
|
||||
if (savedInstanceState == null) ui.post { startSteamAtStartupIfEnabled(cold = true) }
|
||||
else {
|
||||
restoreCompatLaunch(savedInstanceState)
|
||||
pendingSwitch = savedIntent(savedInstanceState, KEY_PENDING_SWITCH)
|
||||
@@ -1530,6 +1715,9 @@ class FrontEndActivity : ComponentActivity() {
|
||||
com.droiddeck.launcher.frontend.GamesFolderMigration.run(this)
|
||||
SessionLogShare.clear(this)
|
||||
SessionArtifacts.finishAbandoned(this)
|
||||
if (SessionPrefs.takeMemoryKillNotice(this)) runOnUiThread {
|
||||
android.widget.Toast.makeText(this, R.string.session_killed_for_memory, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
SessionArtifacts.scrubOlder(this)
|
||||
SessionArtifacts.prune(this)
|
||||
}, "finish-abandoned").start()
|
||||
@@ -1550,9 +1738,14 @@ class FrontEndActivity : ComponentActivity() {
|
||||
startSteamAtStartupIfEnabled()
|
||||
}
|
||||
|
||||
private fun startSteamAtStartupIfEnabled() {
|
||||
if (intent.action != Intent.ACTION_MAIN) return
|
||||
if (SessionPrefs.runSteamAtStartup(this) && !isFinishing && !isDestroyed) startSteamSession()
|
||||
private fun startSteamAtStartupIfEnabled(cold: Boolean = false) {
|
||||
if (intent.action != Intent.ACTION_MAIN && !(cold && intent.action == null)) return
|
||||
if (isFinishing || isDestroyed) return
|
||||
when (SessionPrefs.startupSession(this)) {
|
||||
SessionPrefs.STARTUP_STEAM -> startSteamSession()
|
||||
SessionPrefs.STARTUP_DESKTOP -> startSession(Intent(this, SessionActivity::class.java)
|
||||
.putExtra(SessionService.EXTRA_MODE, SessionService.MODE_DESKTOP))
|
||||
}
|
||||
}
|
||||
|
||||
private fun playSteam() {
|
||||
@@ -1660,6 +1853,11 @@ class FrontEndActivity : ComponentActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!homeChoiceOpen) {
|
||||
HomeApp.releaseUnusedHomeEntry(this)
|
||||
refreshHomeAppState()
|
||||
}
|
||||
if (settingsMode == SessionService.MODE_STEAM) refreshFoundSteamLibraries()
|
||||
compatLaunchVisible = true
|
||||
com.droiddeck.launcher.ui.DualScreenPanel.yieldTo(taskId, false)
|
||||
com.droiddeck.launcher.ui.DualScreenPanel.ownerPaused(taskId, false)
|
||||
@@ -1689,6 +1887,13 @@ class FrontEndActivity : ComponentActivity() {
|
||||
if (applied.isNotEmpty()) ui.post {
|
||||
android.widget.Toast.makeText(this, getString(R.string.main_applied, applied.joinToString(", ")), android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (!OfflineMode.enabled(this)) {
|
||||
val updated = runCatching { ComponentUpdater.ensure(this, force = false) }
|
||||
.onFailure { android.util.Log.w("ComponentUpdater", "automatic component updates", it) }.getOrDefault(emptyList())
|
||||
if (updated.isNotEmpty()) ui.post {
|
||||
android.widget.Toast.makeText(this, updated.joinToString("\n"), android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
ui.post {
|
||||
componentsQueueBusy = false
|
||||
if (showComponents) components.refreshComponents()
|
||||
@@ -1718,6 +1923,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
addDataScheme("file")
|
||||
})
|
||||
displayManager.registerDisplayListener(secondScreenDisplayListener, ui)
|
||||
com.droiddeck.launcher.input.DualScreen.watchTaken(bottomScreenListener)
|
||||
getSystemService(android.hardware.input.InputManager::class.java)?.registerInputDeviceListener(secondScreenTouchListener, ui)
|
||||
launcherStarted = true
|
||||
refreshSecondScreenDisplays()
|
||||
@@ -1734,6 +1940,9 @@ class FrontEndActivity : ComponentActivity() {
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(STATE_HOME_CHOICE_OPEN, homeChoiceOpen)
|
||||
outState.putBoolean(STATE_GAME_SPACE_OFFERED, gameSpaceOffered)
|
||||
pendingSessionLogExport?.let { outState.putString(KEY_PENDING_SESSION_LOG_EXPORT, it.absolutePath) }
|
||||
if (slowUsbNever) SessionPrefs.setSlowUsbWarningOff(this)
|
||||
saveCompatLaunch(outState)
|
||||
pendingSwitch?.let { outState.putParcelable(KEY_PENDING_SWITCH, it) }
|
||||
@@ -1747,13 +1956,32 @@ class FrontEndActivity : ComponentActivity() {
|
||||
if (Build.VERSION.SDK_INT >= 33) state.getParcelable(key, Intent::class.java)
|
||||
else @Suppress("DEPRECATION") state.getParcelable(key)
|
||||
|
||||
private fun prepareAndLaunch(g: Library.SteamGame, key: String) {
|
||||
val request = gameLaunchRequest.incrementAndGet()
|
||||
val done = { preparingGames = preparingGames - key }
|
||||
com.droiddeck.launcher.frontend.AutoComponents.beforeLaunch(this, g, onBlocked = done) {
|
||||
if (request != gameLaunchRequest.get() || isFinishing || isDestroyed) { done(); return@beforeLaunch }
|
||||
com.droiddeck.launcher.stores.StoreLaunch.prepare(this, g) {
|
||||
done()
|
||||
if (request == gameLaunchRequest.get() && !isFinishing && !isDestroyed) launchGame(g)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
gameLaunchRequest.incrementAndGet()
|
||||
cancelCompatLaunch()
|
||||
updates.unregister()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onRestart() {
|
||||
super.onRestart()
|
||||
homeChoiceOpen = false
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
if (!homeChoiceOpen) HomeApp.releaseUnusedHomeEntry(this)
|
||||
unregisterReceiver(wifiLocationReceiver)
|
||||
unregisterReceiver(mediaReceiver)
|
||||
getSystemService(android.content.pm.LauncherApps::class.java).unregisterCallback(packageCallback)
|
||||
@@ -1762,6 +1990,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
com.droiddeck.launcher.stores.StoresState.libraryListener = null
|
||||
com.droiddeck.launcher.stores.battlenet.BattleNetClient.opener = null
|
||||
displayManager.unregisterDisplayListener(secondScreenDisplayListener)
|
||||
com.droiddeck.launcher.input.DualScreen.unwatchTaken(bottomScreenListener)
|
||||
getSystemService(android.hardware.input.InputManager::class.java)?.unregisterInputDeviceListener(secondScreenTouchListener)
|
||||
launcherStarted = false
|
||||
com.droiddeck.launcher.ui.DualScreenPanel.hide(taskId)
|
||||
@@ -1796,15 +2025,14 @@ class FrontEndActivity : ComponentActivity() {
|
||||
|
||||
private fun refreshSecondScreenDisplays() {
|
||||
if (!::displayManager.isInitialized) return
|
||||
// The launcher can itself be shown on a non-default display (DeX, an external monitor); that one is not a "second screen".
|
||||
val ownDisplayId = if (android.os.Build.VERSION.SDK_INT >= 30) display?.displayId ?: android.view.Display.DEFAULT_DISPLAY else @Suppress("DEPRECATION") windowManager.defaultDisplay.displayId
|
||||
dualScreenMode = SessionPrefs.dualScreenMode(this)
|
||||
secondScreenDisplays = SecondScreenDisplays.available(displayManager, ownDisplayId)
|
||||
dualScreenDevice = com.droiddeck.launcher.input.DualScreen.anyDisplay(displayManager, ownDisplayId) != null
|
||||
dualScreenLauncher = SessionPrefs.dualScreenLauncher(this)
|
||||
dualScreenSessions = SessionPrefs.dualScreenSessionMode(this)
|
||||
dualScreenControls = SessionPrefs.dualScreenSessionControls(this)
|
||||
val target = com.droiddeck.launcher.input.DualScreen.display(this, displayManager, ownDisplayId)
|
||||
dualDisplayId = if (dualScreenLauncher) target?.displayId ?: -1 else -1
|
||||
dualDisplayId = target?.displayId ?: -1
|
||||
syncDualPanel()
|
||||
}
|
||||
|
||||
@@ -1833,12 +2061,32 @@ class FrontEndActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun offerHomeScreen() {
|
||||
if (!gameSpaceOffered && HomeApp.hasGameSpace(this) && !HomeApp.inGameSpace(this)) {
|
||||
gameSpaceOffered = true
|
||||
val opened = runCatching { gameSpaceAdd.launch(HomeApp.gameSpaceAddIntent()) }.isSuccess
|
||||
if (opened) {
|
||||
homeChoiceOpen = true
|
||||
android.widget.Toast.makeText(this, R.string.setup_home_game_space_first, android.widget.Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
}
|
||||
enableHomeScreen()
|
||||
}
|
||||
|
||||
private fun enableHomeScreen() {
|
||||
HomeApp.setHomeScreenEnabled(this, true)
|
||||
refreshHomeAppState()
|
||||
manageHomeApp()
|
||||
}
|
||||
|
||||
private fun manageHomeApp() {
|
||||
homeChoiceOpen = true
|
||||
val request = HomeApp.roleRequestIntent(this)
|
||||
if (request != null) {
|
||||
homeRoleRequest.launch(request)
|
||||
} else {
|
||||
HomeApp.openSystemHomeSettings(this)
|
||||
} else if (runCatching { homeRoleRequest.launch(HomeApp.settingsIntent()) }.isFailure) {
|
||||
homeChoiceOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1875,13 +2123,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
|
||||
@Composable
|
||||
private fun MappingHost() {
|
||||
val settings = controllerSettings ?: return
|
||||
ControllerMappingPage(
|
||||
mapping = settings.mapping,
|
||||
onPick = { id, target -> ControllerPrefs.setTarget(this, id, target); refreshController() },
|
||||
onReset = { ControllerPrefs.resetMapping(this); refreshController() },
|
||||
onBack = { showMapping = false },
|
||||
)
|
||||
com.droiddeck.launcher.ui.InputSettingsPage(onBack = { showMapping = false })
|
||||
}
|
||||
|
||||
private fun openComponents(focusContent: Boolean = true, tab: String? = null, gameKey: String? = null) {
|
||||
@@ -1905,9 +2147,11 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private fun importComponent(uri: Uri) {
|
||||
val name = displayNameOf(uri) ?: "imported.wcp"
|
||||
components.componentAction(getString(R.string.comp_busy_importing)) {
|
||||
val tmp = File(cacheDir, "component-import.wcp")
|
||||
contentResolver.openInputStream(uri)?.use { input -> tmp.outputStream().use { input.copyTo(it) } } ?: error(getString(R.string.comp_cannot_read))
|
||||
try { getString(R.string.comp_imported, ComponentsManager.importPackage(this, tmp, name).version) } finally { tmp.delete() }
|
||||
val tmp = File.createTempFile("component-import", ".wcp", cacheDir)
|
||||
try {
|
||||
contentResolver.openInputStream(uri)?.use { input -> tmp.outputStream().use { input.copyTo(it) } } ?: error(getString(R.string.comp_cannot_read))
|
||||
getString(R.string.comp_imported, ComponentUpdater.importPackage(this, tmp, name).version)
|
||||
} finally { tmp.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1931,7 +2175,15 @@ class FrontEndActivity : ComponentActivity() {
|
||||
onDeletePackage = { file -> components.componentAction(getString(R.string.comp_busy_deleting)) { ComponentsManager.deletePackage(this, file) } },
|
||||
onDeleteOriginal = { version -> components.compProton?.let { pid -> components.componentAction(getString(R.string.comp_busy_deleting)) { ComponentsManager.deleteOriginal(this, pid, components.compComp, version) } } },
|
||||
onDownload = { components.downloadComponent(it) },
|
||||
onRefresh = { components.refreshComponentCatalog() },
|
||||
onRefresh = {
|
||||
components.refreshComponentCatalog()
|
||||
if (components.compModes.values.any { it == ComponentUpdates.AUTO }) components.checkComponentUpdates(force = true)
|
||||
},
|
||||
compModes = components.compModes,
|
||||
compAuto = components.compAuto,
|
||||
compAutoHeld = components.compAutoHeld,
|
||||
compAutoChecking = components.compAutoChecking,
|
||||
onCompMode = { comp, mode -> components.chooseComponentMode(comp, mode) },
|
||||
onImport = { pickComponent.launch(InAppFilePicker.buildIntent(this, WCP_EXT, getString(R.string.comp_pick_wcp))) },
|
||||
onBack = { if (!com.droiddeck.launcher.ui.Hops.back { showComponents = false }) showComponents = false },
|
||||
games = steamGames,
|
||||
@@ -1942,6 +2194,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
gpuActions = com.droiddeck.launcher.ui.GpuDriversActions(
|
||||
onAuto = { on -> drivers.setMode(on) },
|
||||
onPair = { key -> drivers.selectPair(key) },
|
||||
onPairUpdate = { key -> drivers.updatePair(key) },
|
||||
onRefresh = { if (drivers.mode == SessionPrefs.GPU_DRIVERS_AUTO) drivers.ensureAuto(force = true) else drivers.checkLatestTurnip() },
|
||||
onSelectLinux = { id -> drivers.selectDriver(id, linux = true) },
|
||||
onSelectAndroid = { id -> drivers.selectDriver(id, linux = false) },
|
||||
@@ -1954,7 +2207,6 @@ class FrontEndActivity : ComponentActivity() {
|
||||
onRestoreBundled = { TurnipDriver(this).restoreBundled(); drivers.refreshDrivers() },
|
||||
onMode = { m -> drivers.chooseMode(m) },
|
||||
onRestart = { drivers.restartApp() },
|
||||
onTpUbwcMode = { drivers.chooseTpUbwcMode(it) },
|
||||
),
|
||||
protons = com.droiddeck.launcher.ui.ProtonsState(
|
||||
rows = protons.protonRows, busyId = protons.protonBusyId, stage = protons.protonStage, percent = protons.protonPercent,
|
||||
@@ -2110,6 +2362,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
unfoldedControls = if (mode == SessionService.MODE_STEAM) unfoldedControls else null,
|
||||
unfoldedControlsWithController = unfoldedControlsWithController,
|
||||
oscMode = if (mode == SessionService.MODE_STEAM) oscMode else null,
|
||||
backHold = backHold,
|
||||
backActionsInverted = backActionsInverted,
|
||||
directAudio = if (mode == SessionService.MODE_STEAM) directAudio else null,
|
||||
picturePosition = if (mode == SessionService.MODE_STEAM) picturePosition else null,
|
||||
@@ -2117,6 +2370,9 @@ class FrontEndActivity : ComponentActivity() {
|
||||
mic = if (mode == SessionService.MODE_STEAM) mic else null,
|
||||
gameStorage = if (mode == SessionService.MODE_STEAM) gameStorage else null,
|
||||
storageOptions = storageOptions,
|
||||
steamLibraries = steamLibraries,
|
||||
foundSteamLibraries = foundSteamLibraries,
|
||||
allFilesAccessMissing = allFilesAccessMissing,
|
||||
storageDiagnostics = mode == SessionService.MODE_STEAM && storageDiagnostics,
|
||||
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
|
||||
forceSsbs = forceSsbs,
|
||||
@@ -2127,7 +2383,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
steamRepairQueued = steamRepairQueued,
|
||||
mangoapp = mangoapp,
|
||||
steamController = if (mode == SessionService.MODE_STEAM) steamController else null,
|
||||
runSteamAtStartup = mode == SessionService.MODE_STEAM && runSteamAtStartup,
|
||||
startupSession = startupSession,
|
||||
bootMovie = if (mode == SessionService.MODE_STEAM) bootMovie else null,
|
||||
bootMovieSound = bootMovieSound,
|
||||
bootMovieCustomName = bootMovieCustomName,
|
||||
@@ -2162,6 +2418,8 @@ class FrontEndActivity : ComponentActivity() {
|
||||
SessionPrefs.setUnfoldedControlsWithController(this, on); unfoldedControlsWithController = on
|
||||
},
|
||||
onOsc = { o -> SessionPrefs.setOscMode(this, o); oscMode = o },
|
||||
onInputSettings = { showPerformance = false; showComponents = false; showMapping = true },
|
||||
onBackHold = { on -> SessionPrefs.setBackHold(this, on); backHold = on },
|
||||
onBackActionsInverted = { inverted ->
|
||||
SessionPrefs.setBackActionsInverted(this, inverted)
|
||||
backActionsInverted = inverted
|
||||
@@ -2180,6 +2438,22 @@ class FrontEndActivity : ComponentActivity() {
|
||||
onPickGameStorageFolder = {
|
||||
pickGameStorage.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_pick_game_storage), gameStorage.ifEmpty { null }))
|
||||
},
|
||||
onAddSteamLibrary = {
|
||||
pickSteamLibrary.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.libraries_pick), null))
|
||||
},
|
||||
onAddFoundSteamLibrary = { path -> addSteamLibrary(path) },
|
||||
onAllowAllFilesAccess = { com.droiddeck.launcher.files.AllFilesAccess.open(this) },
|
||||
onRenameSteamLibrary = { id, label ->
|
||||
SteamLibraries.rename(this, id, label)
|
||||
steamLibraries = SteamLibraries.list(this)
|
||||
refresh()
|
||||
},
|
||||
onRemoveSteamLibrary = { id ->
|
||||
SteamLibraries.remove(this, id)
|
||||
steamLibraries = SteamLibraries.list(this)
|
||||
refresh()
|
||||
refreshFoundSteamLibraries()
|
||||
},
|
||||
onStorageDiagnostics = { on ->
|
||||
SessionPrefs.setStorageDiagnosticsEnabled(this, on)
|
||||
storageDiagnostics = on
|
||||
@@ -2203,9 +2477,9 @@ class FrontEndActivity : ComponentActivity() {
|
||||
onReturnAfterGame = { on -> SessionPrefs.setReturnAfterGame(this, on); returnAfterGame = on },
|
||||
onShaderPrecache = { on -> SessionPrefs.setShaderPrecache(this, on); shaderPrecache = on },
|
||||
onSteamController = { id -> SessionPrefs.setSteamController(this, id); steamController = id },
|
||||
onRunSteamAtStartup = { on ->
|
||||
SessionPrefs.setRunSteamAtStartup(this, on)
|
||||
runSteamAtStartup = on
|
||||
onStartupSession = { id ->
|
||||
SessionPrefs.setStartupSession(this, id)
|
||||
startupSession = id
|
||||
},
|
||||
onBootMovie = { id -> com.droiddeck.launcher.session.BootMovie.select(this, id); loadBootMovie() },
|
||||
onBootMovieSound = { on -> SessionPrefs.setBootMovieSound(this, on); bootMovieSound = on },
|
||||
@@ -2396,7 +2670,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
returnAfterGame = SessionPrefs.returnAfterGame(this)
|
||||
loadShaderPrecache(mode)
|
||||
steamController = SessionPrefs.steamController(this)
|
||||
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
|
||||
startupSession = SessionPrefs.startupSession(this)
|
||||
loadBootMovie()
|
||||
hdrOn = SessionPrefs.hdr(this, mode)
|
||||
upscaler = SessionPrefs.upscaler(this)
|
||||
@@ -2406,6 +2680,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
suspendPolicy = SessionPrefs.suspendPolicy(this, mode)
|
||||
steamDownloadsInBackground = SessionPrefs.steamDownloadsInBackground(this)
|
||||
oscMode = SessionPrefs.oscMode(this)
|
||||
backHold = SessionPrefs.backHold(this)
|
||||
backActionsInverted = SessionPrefs.backActionsInverted(this)
|
||||
directAudio = SessionPrefs.directAudio(this)
|
||||
picturePosition = SessionPrefs.picturePosition(this)
|
||||
@@ -2413,6 +2688,8 @@ class FrontEndActivity : ComponentActivity() {
|
||||
mic = SessionPrefs.micEnabled(this)
|
||||
refreshWifiDiscovery()
|
||||
gameStorage = SessionPrefs.gameStorage(this)
|
||||
steamLibraries = SteamLibraries.list(this)
|
||||
if (mode == SessionService.MODE_STEAM) refreshFoundSteamLibraries()
|
||||
storageDiagnostics = SessionPrefs.storageDiagnosticsEnabled(this)
|
||||
settingsMode = mode
|
||||
Thread({
|
||||
@@ -2443,6 +2720,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
SessionPrefs.setGameStorage(this, path, label)
|
||||
gameStorage = path
|
||||
checkUsbLink()
|
||||
if (settingsMode == SessionService.MODE_STEAM) refreshFoundSteamLibraries()
|
||||
}
|
||||
|
||||
private fun checkUsbLink() {
|
||||
@@ -2466,6 +2744,11 @@ class FrontEndActivity : ComponentActivity() {
|
||||
gameCores = CpuCores.parse(SessionPrefs.gameCpus(this)).ifEmpty { CpuCores.all.toSet() }
|
||||
tuSysmem = SessionPrefs.tuSysmem(this)
|
||||
zinkLazy = SessionPrefs.zinkLazy(this)
|
||||
vkBridge = SessionPrefs.vkBridge(this)
|
||||
venus = SessionPrefs.venus(this)
|
||||
venusHostVulkan = com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(this)
|
||||
vkBridgeSafe = SessionPrefs.vkBridgeSafe(this)
|
||||
bridgeProtonDxvk = SessionPrefs.bridgeProtonDxvk(this)
|
||||
noXalia = SessionPrefs.noXalia(this)
|
||||
fastSync = SessionPrefs.fastSync(this)
|
||||
syncFallback = SessionPrefs.syncFallback(this)
|
||||
@@ -2486,8 +2769,33 @@ class FrontEndActivity : ComponentActivity() {
|
||||
SessionPrefs.logsEnabled(this) && SessionPrefs.gameDebugLogsEnabled(this) &&
|
||||
!SessionPaths.currentSessionLogsRetained(this)
|
||||
|
||||
private fun shareEnhancedArchives(zips: List<java.io.File>) {
|
||||
if (isFinishing || isDestroyed) return
|
||||
val choices = zips.map { zip ->
|
||||
val label = when {
|
||||
zip.name.endsWith("-session.zip") -> getString(R.string.diagnostics_share_session)
|
||||
zip.name.endsWith("-graphics.zip") -> getString(R.string.diagnostics_share_graphics)
|
||||
else -> getString(R.string.diagnostics_share_games)
|
||||
}
|
||||
label to listOf(zip)
|
||||
} + (getString(R.string.diagnostics_share_all) to zips)
|
||||
android.app.AlertDialog.Builder(this)
|
||||
.setTitle(R.string.diagnostics_share_title)
|
||||
.setItems(choices.map { it.first }.toTypedArray()) { _, which ->
|
||||
val selected = choices[which].second
|
||||
runCatching {
|
||||
startActivity(if (selected.size == 1) SessionLogShare.shareIntent(this, selected.single())
|
||||
else SessionLogShare.shareEnhancedIntent(this, selected))
|
||||
}.onFailure {
|
||||
android.util.Log.w("SessionLogShare", "could not share latest session logs", it)
|
||||
android.widget.Toast.makeText(this, R.string.session_logs_share_failed, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
// Until the scan finishes, the other client's games would launch in this one.
|
||||
val listX64 = SessionPrefs.activeSteamClientArch(this) == "x86_64"
|
||||
if (listX64 != steamGamesX64) {
|
||||
steamGamesX64 = listX64
|
||||
@@ -2503,15 +2811,23 @@ class FrontEndActivity : ComponentActivity() {
|
||||
offline = OfflineMode.enabled(this)
|
||||
installed = LinuxRuntimeInstaller.installedVersion(this)
|
||||
ready = LinuxRuntime.isInstalled(this)
|
||||
vkBridge = SessionPrefs.vkBridge(this)
|
||||
vkBridgeSafe = SessionPrefs.vkBridgeSafe(this)
|
||||
bridgeProtonDxvk = SessionPrefs.bridgeProtonDxvk(this)
|
||||
removalPending = LinuxRuntimeInstaller.hasRemovalPending(this)
|
||||
ui.post { autoUpdateRuntime() }
|
||||
frameGenLabel = FrameGen.label(this)
|
||||
romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
enhancedDiagnosticsArmed = com.droiddeck.launcher.session.EnhancedDiagnostics.isArmed(this)
|
||||
gameDebugLogsEnabled = SessionPrefs.gameDebugLogsEnabled(this)
|
||||
gameDebugLogsNeedsRestart = gameDebugLogsNeedsRestart()
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
agentCommands = com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this)
|
||||
graphicsLogs = SessionPrefs.detailedGraphicsLogs(this)
|
||||
steamGpuDrawing = SessionPrefs.steamGpuCompositing(this)
|
||||
bridgeFellBack = SessionPrefs.bridgeFellBack(this)
|
||||
bridgeFellBackMemory = SessionPrefs.bridgeFellBackForMemory(this)
|
||||
clearCachedApps = SessionPrefs.clearCachedApps(this)
|
||||
adbPaired = WirelessAdbFix.hasSavedPairing(this)
|
||||
memoryHud = SessionPrefs.memoryHud(this)
|
||||
@@ -2607,7 +2923,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
Thread({ checkCatalog() }, "catalog").start()
|
||||
return
|
||||
}
|
||||
if (installed == null && !com.droiddeck.launcher.core.DeviceSupport.adreno()) { showNonAdreno = release; return }
|
||||
if (installed == null && !com.droiddeck.launcher.core.DeviceSupport.canDraw(this)) { showNonAdreno = release; return }
|
||||
install(release)
|
||||
}
|
||||
|
||||
@@ -2626,7 +2942,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
showPhantomGate = true
|
||||
return false
|
||||
}
|
||||
val warn = installed == null && !com.droiddeck.launcher.core.DeviceSupport.adreno()
|
||||
val warn = installed == null && !com.droiddeck.launcher.core.DeviceSupport.canDraw(this)
|
||||
if (warn && available != null) { showNonAdreno = available; return false }
|
||||
startActivity(intent)
|
||||
return true
|
||||
@@ -2696,6 +3012,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private fun removeRuntime() {
|
||||
if (runtimeChangesBlocked()) return
|
||||
val removal = LinuxRuntimeInstaller.beginUninstall(this) ?: return
|
||||
com.droiddeck.launcher.runtime.RuntimeAutoUpdate.seed(this)
|
||||
ready = false
|
||||
com.droiddeck.launcher.runtime.RuntimeInstallService.keepRemovalAlive(this)
|
||||
followRuntimeOperation { removal.run(it) }
|
||||
@@ -2722,6 +3039,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
android.widget.Toast.makeText(this, it, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
refresh()
|
||||
if (ok == true && LinuxRuntime.isInstalled(this)) protons.checkUpdates()
|
||||
}
|
||||
}, "runtime-operation").start()
|
||||
}
|
||||
@@ -2729,9 +3047,12 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private fun autoUpdateRuntime() {
|
||||
val release = available?.takeIf { catalogFresh } ?: return
|
||||
if (!lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED) || runtimeChangesBlocked() || removalPending) return
|
||||
if (release.version == installed || installed == null) return
|
||||
if (installed != null) com.droiddeck.launcher.runtime.RuntimeAutoUpdate.seed(this)
|
||||
if (release.version == installed) return
|
||||
if (installed == null && (OfflineMode.enabled(this) || !com.droiddeck.launcher.core.DeviceSupport.canDraw(this) ||
|
||||
!com.droiddeck.launcher.frontend.AutoComponents.online(this) || com.droiddeck.launcher.frontend.AutoComponents.unfavourable(this))) return
|
||||
if (com.droiddeck.launcher.runtime.RuntimeAutoUpdate.claim(this, installed, release.version)) {
|
||||
Log.i(TAG, "runtime: updating $installed to ${release.version} by itself")
|
||||
Log.i(TAG, "runtime: installing ${release.version} over ${installed ?: "nothing"} by itself")
|
||||
install(release)
|
||||
}
|
||||
}
|
||||
@@ -2748,6 +3069,9 @@ class FrontEndActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STATE_HOME_CHOICE_OPEN = "home_choice_open"
|
||||
private const val KEY_PENDING_SESSION_LOG_EXPORT = "pending_session_log_export"
|
||||
private const val STATE_GAME_SPACE_OFFERED = "game_space_offered"
|
||||
private const val TAG = "MainActivity"
|
||||
private const val MEDIA_SETTLE_MS = 1500L
|
||||
private const val COMPONENT_RESTORE_MS = 60_000L
|
||||
|
||||
@@ -24,6 +24,9 @@ import kotlin.math.roundToInt
|
||||
|
||||
object HomeApp {
|
||||
private const val ICON_DP = 72
|
||||
private const val GAME_SPACE = "cn.nubia.gamelauncher"
|
||||
private const val GAME_SPACE_ADD = "cn.nubia.gamelauncher.action.GAME_MANAGER"
|
||||
private val GAME_SPACE_GAMES = Uri.parse("content://cn.nubia.gamelauncher.db.AppAddProvider/appadd")
|
||||
|
||||
data class LaunchableApp(
|
||||
val label: String,
|
||||
@@ -54,6 +57,24 @@ object HomeApp {
|
||||
)
|
||||
}
|
||||
|
||||
fun hasGameSpace(context: Context): Boolean =
|
||||
runCatching { context.packageManager.getPackageInfo(GAME_SPACE, 0) }.isSuccess
|
||||
|
||||
fun inGameSpace(context: Context): Boolean =
|
||||
runCatching {
|
||||
context.contentResolver.query(GAME_SPACE_GAMES, arrayOf("component"), null, null, null)?.use { rows ->
|
||||
val prefix = context.packageName + ","
|
||||
generateSequence { if (rows.moveToNext()) rows.getString(0) else null }.any { it.startsWith(prefix) }
|
||||
}
|
||||
}.getOrNull() ?: true
|
||||
|
||||
fun gameSpaceAddIntent(): Intent = Intent(GAME_SPACE_ADD).setPackage(GAME_SPACE)
|
||||
|
||||
fun releaseUnusedHomeEntry(context: Context) {
|
||||
if (isHomeScreenEnabled(context) && !isDefault(context) && hasGameSpace(context))
|
||||
setHomeScreenEnabled(context, false)
|
||||
}
|
||||
|
||||
fun isDefault(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val roles = context.getSystemService(RoleManager::class.java)
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
/** Shortcuts other apps asked to put on the home screen. The system keeps the pinned set per launcher. */
|
||||
object HomePins {
|
||||
class Pinned(val info: ShortcutInfo, val icon: Bitmap?, val appLabel: String, val userSerial: Long) {
|
||||
val key get() = "pin:${info.`package`}/${info.id}" + if (userSerial != 0L) "#$userSerial" else ""
|
||||
|
||||
@@ -197,7 +197,6 @@ object HomeWidgets {
|
||||
if (pendingId == AppWidgetManager.INVALID_APPWIDGET_ID) add(activity, provider.info, null)
|
||||
}
|
||||
|
||||
/** Places a widget another app asked for; its own app has already configured it. */
|
||||
fun addPinned(activity: Activity, request: LauncherApps.PinItemRequest): Boolean {
|
||||
if (pendingId != AppWidgetManager.INVALID_APPWIDGET_ID) return false
|
||||
val info = request.getAppWidgetProviderInfo(activity) ?: return false
|
||||
|
||||
@@ -18,9 +18,9 @@ import com.droiddeck.launcher.runtime.LinuxRuntimeInstaller
|
||||
internal class ProtonMenu(
|
||||
private val activity: android.app.Activity,
|
||||
private val ui: Handler,
|
||||
private val onInstalled: () -> Unit = {},
|
||||
private val idle: () -> Boolean = { true },
|
||||
) {
|
||||
// Install workers and their results outlive the activity that starts them.
|
||||
private object Operation {
|
||||
val rows = mutableStateOf<List<ProtonRow>>(emptyList())
|
||||
val busyId = mutableStateOf<String?>(null)
|
||||
@@ -62,6 +62,7 @@ internal class ProtonMenu(
|
||||
if (automatic) Log.w("ProtonMenu", "auto-update ${tool.id}: $problem")
|
||||
else android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (problem == null) onInstalled()
|
||||
if (automatic) ensureAutoUpdates()
|
||||
}
|
||||
}, "install-proton-$id").start()
|
||||
|
||||
@@ -71,7 +71,6 @@ class QuestXrActivity : NativeActivity() {
|
||||
}
|
||||
} finally {
|
||||
synchronized(steamVrLock) {
|
||||
// A retiring activity must not stop a replacement's SteamVR session.
|
||||
if (steamVrOwner === this) {
|
||||
if (sessionAlive()) SteamVr.run(this, "stop")
|
||||
steamVrOwner = null
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -18,12 +18,15 @@ 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.SessionService
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -93,6 +96,7 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
}
|
||||
"prefs" -> prefs(context, request)
|
||||
"glasses" -> glasses(context, request)
|
||||
"logs" -> logs(context)
|
||||
"access" -> JSONObject().put("ok", true).put("commands", AgentAccess.commandsAllowed(context))
|
||||
.put("debugBuild", BuildConfig.DEBUG).put("inbox", AgentEnv.inbox(context).absolutePath)
|
||||
else -> error("UNKNOWN_COMMAND", "Unknown agent command '$method'")
|
||||
@@ -197,9 +201,46 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
private fun hex(text: String, max: Int): Int? =
|
||||
text.removePrefix("0x").removePrefix("0X").takeIf { it.isNotEmpty() }?.toIntOrNull(16)?.takeIf { it in 0..max }
|
||||
|
||||
private fun logDir(context: Context): File? =
|
||||
SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: SessionPaths.sessionFolders(context).lastOrNull()
|
||||
|
||||
private fun cleanupExpiredLogExports(exports: File) {
|
||||
val now = System.currentTimeMillis()
|
||||
val canonicalRoot = exports.canonicalFile
|
||||
exports.listFiles()?.forEach { candidate ->
|
||||
val isUuid = runCatching { UUID.fromString(candidate.name).toString() == candidate.name }.getOrDefault(false)
|
||||
if (!isUuid || !candidate.isDirectory || Files.isSymbolicLink(candidate.toPath())) return@forEach
|
||||
if (candidate.canonicalFile.parentFile != canonicalRoot) return@forEach
|
||||
if (now - candidate.lastModified() >= LOG_EXPORT_RETENTION_MS) candidate.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
// The session folder is app-private, so adb pulls this copy from external files instead.
|
||||
private fun logs(context: Context): JSONObject {
|
||||
val dir = logDir(context)?.takeIf { it.isDirectory }
|
||||
?: return error("NO_SESSION_LOGS", "No session artifact directory is available")
|
||||
val externalFiles = context.getExternalFilesDir(null)
|
||||
?: return error("NO_SESSION_LOGS", "External app storage is unavailable")
|
||||
val exports = File(externalFiles, "agent-logs")
|
||||
if ((!exports.exists() && !exports.mkdirs()) || !exports.isDirectory) {
|
||||
return error("NO_SESSION_LOGS", "Could not create the session export directory")
|
||||
}
|
||||
cleanupExpiredLogExports(exports)
|
||||
val output = File(exports, UUID.randomUUID().toString())
|
||||
val zip = try {
|
||||
SessionLogShare.zipFolder(context, dir, output)
|
||||
} catch (error: Exception) {
|
||||
output.deleteRecursively()
|
||||
throw error
|
||||
}
|
||||
?: return error("NO_SESSION_LOGS", "The session artifact directory is empty")
|
||||
return JSONObject().put("ok", true).put("command", "logs").put("sessionId", dir.name)
|
||||
.put("zip", zip.absolutePath).put("cleanupDir", output.absolutePath)
|
||||
}
|
||||
|
||||
private fun state(context: Context): JSONObject {
|
||||
val runtimeVersion = LinuxRuntimeInstaller.installedVersion(context)
|
||||
val dir = SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: SessionPaths.sessionFolders(context).lastOrNull()
|
||||
val dir = logDir(context)
|
||||
val session = JSONObject()
|
||||
.put("id", SessionState.sessionId ?: dir?.name ?: JSONObject.NULL)
|
||||
.put("phase", SessionState.phase.name)
|
||||
@@ -351,6 +392,7 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
const val RESULT_JSON = "json"
|
||||
const val EXTRA_REQUEST = "request"
|
||||
const val SCHEMA = 2
|
||||
private const val LOG_EXPORT_RETENTION_MS = 24 * 60 * 60 * 1000L
|
||||
private val recoveryComplete = CountDownLatch(1)
|
||||
|
||||
fun awaitRecovery() {
|
||||
|
||||
@@ -18,7 +18,6 @@ object AgentEnv {
|
||||
private const val TAG = "AgentEnv"
|
||||
private const val FILE = "agent-env.json"
|
||||
private const val GUEST_BIN = "/opt/droiddeck-agent/bin"
|
||||
private const val BASE_PATH = "/usr/local/bin:/usr/bin:/bin"
|
||||
private val NAME = Regex("[A-Za-z0-9._-]{1,64}")
|
||||
private val KEY = Regex("[A-Za-z_][A-Za-z0-9_]*")
|
||||
private val lock = Any()
|
||||
@@ -160,7 +159,7 @@ object AgentEnv {
|
||||
}
|
||||
|
||||
private fun pathOf(lines: List<String>) =
|
||||
lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: BASE_PATH
|
||||
lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: LinuxRuntime.GUEST_PATH
|
||||
|
||||
private fun bin(context: Context): File {
|
||||
val root = LinuxRuntime.rootDir(context).canonicalFile.toPath()
|
||||
|
||||
@@ -26,6 +26,10 @@ object AgentGuest {
|
||||
private set
|
||||
var keyboardRequests by mutableIntStateOf(0)
|
||||
private set
|
||||
var gameTextField by mutableStateOf(false)
|
||||
private set
|
||||
var gameProgram by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var keyboardDismissals by mutableIntStateOf(0)
|
||||
private set
|
||||
@Volatile private var keyboardRoute = false
|
||||
@@ -43,7 +47,7 @@ object AgentGuest {
|
||||
observer?.stopWatching()
|
||||
observer = null
|
||||
lastFocusKey = null
|
||||
main.post { steamKeyboard = false; desktopTextField = false }
|
||||
main.post { steamKeyboard = false; desktopTextField = false; gameTextField = false; gameProgram = null }
|
||||
val dir = dir(context)
|
||||
dir.deleteRecursively()
|
||||
dir.mkdirs()
|
||||
@@ -59,6 +63,7 @@ object AgentGuest {
|
||||
"focus.json" -> readJson(File(dir, path))?.let(::recordFocus)
|
||||
"keyboard.json" -> readJson(File(dir, path))?.let { recordKeyboard(this, it) }
|
||||
"ime.json" -> readJson(File(dir, path))?.let { recordTextField(this, it) }
|
||||
"wine-ime.json" -> readJson(File(dir, path))?.let { recordGameTextField(this, it) }
|
||||
"keyboard-request.json" -> readJson(File(dir, path))?.let { recordKeyboardRequest(this) }
|
||||
"keyboard-dismiss.json" -> readJson(File(dir, path))?.let { recordKeyboardDismiss(this) }
|
||||
}
|
||||
@@ -71,7 +76,7 @@ object AgentGuest {
|
||||
synchronized(lock) {
|
||||
observer?.stopWatching()
|
||||
observer = null
|
||||
main.post { steamKeyboard = false; desktopTextField = false }
|
||||
main.post { steamKeyboard = false; desktopTextField = false; gameTextField = false; gameProgram = null }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +134,16 @@ object AgentGuest {
|
||||
SessionEvents.record("ime.changed", mapOf("textField" to focused, "seq" to snapshot.optInt("seq")))
|
||||
}
|
||||
|
||||
private fun recordGameTextField(source: FileObserver, snapshot: JSONObject) {
|
||||
val focused = snapshot.optBoolean("textField")
|
||||
val program = snapshot.optString("program").takeIf { it.isNotEmpty() }
|
||||
synchronized(lock) {
|
||||
if (source !== observer) return
|
||||
main.post { gameProgram = program; gameTextField = focused }
|
||||
}
|
||||
SessionEvents.record("game.ime.changed", mapOf("textField" to focused, "program" to program, "seq" to snapshot.optInt("seq")))
|
||||
}
|
||||
|
||||
private fun recordFocus(snapshot: JSONObject) {
|
||||
val fields = linkedMapOf<String, Any?>(
|
||||
"focusedApp" to snapshot.opt("focusedApp"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.droiddeck.launcher.agent
|
||||
|
||||
import com.droiddeck.launcher.input.PadBridge
|
||||
import com.droiddeck.launcher.input.PadState
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
@@ -143,9 +144,10 @@ object AgentInput {
|
||||
val right = request.optString("stick", "left") == "right"
|
||||
val x = request.optDouble("x", 0.0).toFloat().coerceIn(-1f, 1f)
|
||||
val y = request.optDouble("y", 0.0).toFloat().coerceIn(-1f, 1f)
|
||||
bridge.applyTouch { if (right) { it.rightX = x; it.rightY = y } else { it.leftX = x; it.leftY = y } }
|
||||
val stickWrite = if (right) PadBridge.RIGHT_STICK else PadBridge.LEFT_STICK
|
||||
bridge.applyTouch(stickWrite) { if (right) { it.rightX = x; it.rightY = y } else { it.leftX = x; it.leftY = y } }
|
||||
hold(request, 300)
|
||||
bridge.applyTouch { if (right) { it.rightX = 0f; it.rightY = 0f } else { it.leftX = 0f; it.leftY = 0f } }
|
||||
bridge.applyTouch(stickWrite) { if (right) { it.rightX = 0f; it.rightY = 0f } else { it.leftX = 0f; it.leftY = 0f } }
|
||||
return JSONObject().put("stick", if (right) "right" else "left")
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,6 @@ public class DirectAudioRelayComponent extends SessionPart {
|
||||
}
|
||||
}
|
||||
|
||||
// Android keeps the input stream's wake lock while a frozen helper still has it started.
|
||||
public boolean awaitMicReleased(long timeoutMs) {
|
||||
long deadline = System.currentTimeMillis() + timeoutMs;
|
||||
synchronized (micLock) {
|
||||
|
||||
@@ -130,22 +130,46 @@ public class PulseAudioComponent extends SessionPart {
|
||||
|
||||
private void ensureClientSink(java.io.PrintWriter out) {
|
||||
try {
|
||||
long deadline = System.currentTimeMillis() + 15000L;
|
||||
// Nothing that may block (a sink whose AAudio open hangs: a Galaxy S24 FE (US) hung the
|
||||
// daemon's start-up loading one from default.pa) is loaded by the daemon itself; the
|
||||
// fallbacks are loaded here, after it is up, each pactl call bounded by its timeout.
|
||||
long started = System.currentTimeMillis();
|
||||
long deadline = started + 30000L;
|
||||
String sinks = null;
|
||||
boolean socketSeen = false;
|
||||
while (System.currentTimeMillis() < deadline && pid > 1) {
|
||||
if (socket().exists() && (sinks = pactl("list", "short", "sinks")) != null) break;
|
||||
socketSeen |= socket().exists();
|
||||
if (socketSeen && (sinks = pactl("list", "short", "sinks")) != null) break;
|
||||
try { Thread.sleep(250L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
|
||||
}
|
||||
note(out, "client sink: daemon " + (sinks != null ? "answered" : "did not answer") + " after "
|
||||
+ (System.currentTimeMillis() - started) + " ms (socket " + (socketSeen ? "present" : "never appeared") + ", pactl "
|
||||
+ (new File(workingDir, "pactl").isFile() ? "present" : "MISSING") + ", daemon pid " + pid + ")");
|
||||
String line;
|
||||
if (sinks == null) {
|
||||
line = "== client sink: the daemon did not answer in 15 s; no fallback attempted";
|
||||
line = "== client sink: the daemon did not answer in 30 s; no fallback attempted";
|
||||
} else if (sinks.contains("\tDirectAudio\t") || sinks.startsWith("DirectAudio\t") || sinks.contains("\tDirectAudio\n") || sinks.matches("(?s).*\\bDirectAudio\\b.*")) {
|
||||
line = "== client sink: DirectAudio (module-directaudio-native-sink)";
|
||||
} else {
|
||||
boolean loaded = pactl("load-module", "module-aaudio-sink", "sink_name=AAudioSink", "performance_mode=1", "adaptive=1", "volume=1.0", "control_dir=" + deviceControls) != null
|
||||
&& pactl("set-default-sink", "AAudioSink") != null;
|
||||
line = loaded ? "== client sink: DirectAudio did not come up; FALLBACK to the plain AAudio sink (AAudioSink)"
|
||||
: "== client sink: DirectAudio did not come up and the AAudio fallback failed too - no client sound";
|
||||
// AAudio can refuse the first open with AAUDIO_ERROR_DISCONNECTED while Android is
|
||||
// still switching the output route as the session starts (seen on a Galaxy S24 FE):
|
||||
// try DirectAudio again, then the plain sink, a few times over ten seconds.
|
||||
String loadedAs = null;
|
||||
for (int attempt = 1; attempt <= 5 && loadedAs == null && pid > 1; attempt++) {
|
||||
try { Thread.sleep(2000L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
|
||||
if (pactl("load-module", "module-directaudio-native-sink", "sink_name=DirectAudio", "performance_mode=1",
|
||||
"adaptive=1", "decay=0", "limiter=1", "watchdog=1", "volume=1.0", "control_dir=" + deviceControls) != null
|
||||
&& pactl("set-default-sink", "DirectAudio") != null) {
|
||||
loadedAs = "DirectAudio on attempt " + attempt;
|
||||
} else if (pactl("load-module", "module-aaudio-sink", "sink_name=AAudioSink",
|
||||
"performance_mode=" + (attempt == 1 ? "1" : "0"), "adaptive=1", "volume=1.0", "control_dir=" + deviceControls) != null
|
||||
&& pactl("set-default-sink", "AAudioSink") != null) {
|
||||
loadedAs = "the plain AAudio sink (AAudioSink) on attempt " + attempt;
|
||||
}
|
||||
}
|
||||
line = loadedAs != null ? "== client sink: DirectAudio did not come up at start; retried and got " + loadedAs
|
||||
: "== client sink: DirectAudio did not come up and the AAudio fallback failed too after 5 retries - no client sound."
|
||||
+ " If audio.log shows AAUDIO_ERROR_DISCONNECTED, Android refused the audio output: rebooting the phone usually fixes this";
|
||||
Log.w(TAG, line);
|
||||
}
|
||||
Log.i(TAG, line);
|
||||
@@ -155,6 +179,14 @@ public class PulseAudioComponent extends SessionPart {
|
||||
}
|
||||
}
|
||||
|
||||
/** A timestamped line in the session's audio log. */
|
||||
private void note(java.io.PrintWriter out, String what) {
|
||||
String line = "== " + new java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.US).format(new java.util.Date()) + " " + what;
|
||||
Log.i(TAG, line);
|
||||
if (out != null) synchronized (out) { out.println(line); out.flush(); }
|
||||
}
|
||||
|
||||
/** Runs pactl against this daemon; its stdout, or null when it failed or timed out. */
|
||||
private String pactl(String... args) {
|
||||
File pactl = new File(workingDir, "pactl");
|
||||
if (!pactl.isFile() || pid <= 1) return null;
|
||||
|
||||
@@ -13,15 +13,10 @@ import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
/**
|
||||
* A pack built by CI alongside this APK (tools/packs). Main and release builds publish it to
|
||||
* DroidDeck-Components under a content-addressed tag; a PR test build attaches packs that are not there
|
||||
* yet to its pr-N release on DroidDeck-CI. The APK pins the pack's SHA-256, so only the bytes this build
|
||||
* was made with install, whichever host serves them.
|
||||
*/
|
||||
internal class AddonPack(
|
||||
val name: String, private val sha256: String, val size: Long, private val required: List<String>,
|
||||
private val reuseDeviceCopy: Boolean = sha256.isEmpty() && !BuildConfig.CI_BUILD,
|
||||
private val bundle: (Context) -> java.io.InputStream = { it.assets.open("packs/$name.tar.zst") },
|
||||
) {
|
||||
sealed interface State {
|
||||
data object Missing : State
|
||||
@@ -42,13 +37,11 @@ internal class AddonPack(
|
||||
)
|
||||
|
||||
private fun root(context: Context) = File(context.filesDir, "addons/$name")
|
||||
/** App-private and written only after the archive matched the pinned digest. */
|
||||
fun dir(context: Context) = File(root(context), version(context))
|
||||
fun file(context: Context, relative: String) = File(dir(context), relative)
|
||||
|
||||
fun installed(context: Context): Boolean = version(context).isNotEmpty() && File(dir(context), COMPLETE).isFile
|
||||
|
||||
// A build made outside CI pins no packs, so it runs the newest one a CI build already verified and installed.
|
||||
private fun version(context: Context): String = if (!reuseDeviceCopy) sha256 else deviceCopy(context).orEmpty()
|
||||
|
||||
private fun deviceCopy(context: Context): String? = root(context).listFiles().orEmpty()
|
||||
@@ -65,7 +58,6 @@ internal class AddonPack(
|
||||
.also { mutableState.value = it }
|
||||
}
|
||||
|
||||
/** Starts the download in the background unless the pack is already present or on its way. */
|
||||
fun ensure(context: Context, onReady: (Context) -> Unit = {}) {
|
||||
val app = context.applicationContext
|
||||
synchronized(lock) {
|
||||
@@ -80,7 +72,6 @@ internal class AddonPack(
|
||||
}
|
||||
}
|
||||
|
||||
/** Blocks a worker thread until the pack is installed, starting the download if needed. */
|
||||
@JvmOverloads
|
||||
fun await(context: Context, running: () -> Boolean = { true }, onState: (State) -> Unit = {}): Boolean {
|
||||
if (installed(context)) return true
|
||||
@@ -104,7 +95,7 @@ internal class AddonPack(
|
||||
}
|
||||
val root = root(context).apply { mkdirs() }
|
||||
val archive = File(root, "$sha256.tar.zst")
|
||||
val downloaded = urls.any { url ->
|
||||
val downloaded = bundled(context, archive) || urls.any { url ->
|
||||
Downloader.downloadFile(url, archive, true, sha256) { p -> mutableState.value = State.Downloading(p.coerceIn(0f, 1f)) }
|
||||
.also { if (!it) archive.delete() }
|
||||
}
|
||||
@@ -126,6 +117,16 @@ internal class AddonPack(
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun bundled(context: Context, archive: File): Boolean {
|
||||
val copied = runCatching {
|
||||
bundle(context).use { input -> archive.outputStream().use { input.copyTo(it) } }
|
||||
}.isSuccess
|
||||
if (copied && Hashes.sha256(archive).equals(sha256, ignoreCase = true)) return true
|
||||
if (copied) Log.w(TAG, "$name: the bundled pack does not match the pinned digest")
|
||||
archive.delete()
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AddonPack"
|
||||
private const val REPO = "Droid-Deck/DroidDeck-Components"
|
||||
@@ -148,5 +149,8 @@ internal class AddonPack(
|
||||
listOf("version", "comet"))
|
||||
val BUTLER = AddonPack("butler", BuildConfig.PACK_BUTLER_SHA256, BuildConfig.PACK_BUTLER_SIZE,
|
||||
listOf("version", "butler", "7z.so", "libc7zip.so"))
|
||||
@JvmField
|
||||
val POWERVR = AddonPack("powervr", BuildConfig.PACK_POWERVR_SHA256, BuildConfig.PACK_POWERVR_SIZE,
|
||||
listOf("runtime.tzst", "version.txt"))
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import android.os.PowerManager
|
||||
import android.os.StatFs
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.gpu.DeviceInfo
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import android.view.Display
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
|
||||
@@ -42,7 +44,6 @@ object DeviceReport {
|
||||
}
|
||||
}
|
||||
|
||||
/** Name of an ApplicationExitInfo reason code, so a report says "LOW_MEMORY" instead of "3". */
|
||||
internal fun exitReasonName(reason: Int): String = when (reason) {
|
||||
0 -> "UNKNOWN"
|
||||
1 -> "EXIT_SELF"
|
||||
@@ -111,6 +112,7 @@ object DeviceReport {
|
||||
runCatching {
|
||||
val info = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
k("Version", "${info.versionName} (${info.longVersionCode})")
|
||||
k("App build", "${com.droiddeck.launcher.BuildConfig.BUILD_LABEL} (source ${com.droiddeck.launcher.BuildConfig.SOURCE_COMMIT})")
|
||||
}
|
||||
k("Package", context.packageName)
|
||||
k("targetSdk", context.applicationInfo.targetSdkVersion)
|
||||
@@ -187,7 +189,11 @@ object DeviceReport {
|
||||
h("GPU")
|
||||
k("KGSL gpu_model", readSys("/sys/class/kgsl/kgsl-3d0/gpu_model"))
|
||||
k("KGSL chip id", readSys("/sys/class/kgsl/kgsl-3d0/gpu_chipid"))
|
||||
k("System Vulkan ICD", if (File("/vendor/lib64/hw/vulkan.adreno.so").exists()) "/vendor/lib64/hw/vulkan.adreno.so" else "not at the usual path")
|
||||
k("System Vulkan ICD", when {
|
||||
SystemVulkanDriver.isPowerVr() -> SystemVulkanDriver.hal()
|
||||
File("/vendor/lib64/hw/vulkan.adreno.so").exists() -> "/vendor/lib64/hw/vulkan.adreno.so"
|
||||
else -> "not at the usual path"
|
||||
})
|
||||
|
||||
h("Display")
|
||||
k("Session output", SessionState.outputSize?.let { "${it.first}x${it.second}" })
|
||||
@@ -209,7 +215,7 @@ object DeviceReport {
|
||||
|
||||
h("Drivers")
|
||||
val gpu = com.droiddeck.launcher.gpu.GpuInfo.detect()
|
||||
k("GPU", "${gpu.name} · ${gpu.family.label} · ${gpu.supportText}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "")
|
||||
k("GPU", "${gpu.name} · ${gpu.family.label(gpu.name)} · ${gpu.supportText(SessionPrefs.vkBridge(context))}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "")
|
||||
if (gpu.model == 740) {
|
||||
val hint = com.droiddeck.launcher.gpu.TpUbwcHint
|
||||
k("TP UBWC hint", hint.forThisDevice(context) ?: "driver default")
|
||||
@@ -222,19 +228,25 @@ object DeviceReport {
|
||||
com.droiddeck.launcher.gpu.AutoDriverTrial.bad(context))?.let { " (recommended pair: $it)" } ?: ""))
|
||||
val turnip = TurnipDriver(context)
|
||||
val androidChoice = SessionPrefs.androidDriver(context)
|
||||
k("Display driver (chosen)", if (androidChoice.isEmpty()) "Auto -> ${turnip.autoId()}" else androidChoice)
|
||||
k(" name / version", "${turnip.displayName(if (androidChoice.isEmpty()) turnip.autoId() else androidChoice)} ${turnip.driverVersion(if (androidChoice.isEmpty()) turnip.autoId() else androidChoice)}".trim())
|
||||
val displayId = androidChoice.ifEmpty { turnip.autoId() }
|
||||
val displayLabel = if (displayId == "system") DeviceInfo.builtinDriverLabel(SystemVulkanDriver.isPowerVr(), runtime = false)
|
||||
else "${turnip.displayName(displayId)} ${turnip.driverVersion(displayId)}".trim()
|
||||
k("Display driver (chosen)", if (androidChoice.isEmpty()) "Auto -> $displayLabel" else androidChoice)
|
||||
k(" name / version", displayLabel)
|
||||
k(" imported available", turnip.enumerateImported().ifEmpty { listOf("none") }.joinToString(", "))
|
||||
val lm = LinuxVulkanDriverManager(context)
|
||||
run {
|
||||
val id = SessionPrefs.linuxDriver(context)
|
||||
val label = if (id.isEmpty()) "runtime default (the Turnip built into the runtime)"
|
||||
val label = if (id.isEmpty()) DeviceInfo.builtinDriverLabel(SystemVulkanDriver.isPowerVr(), runtime = true)
|
||||
else "${lm.getDriverName(id)} ${lm.getDriverVersion(id)}".trim() +
|
||||
lm.getMinGlibc(id).let { if (it.isEmpty()) "" else " (glibc $it+)" } +
|
||||
if (lm.isInstalled(id)) "" else " [MISSING - falls back to the runtime's own]"
|
||||
if (lm.isInstalled(id)) "" else if (SystemVulkanDriver.isPowerVr()) " [MISSING - select an installed driver or Auto]"
|
||||
else " [MISSING - falls back to the runtime's own]"
|
||||
k("Linux driver", label)
|
||||
}
|
||||
k("Runtime's own ICD", LinuxRuntime.vulkanIcd(context)?.path)
|
||||
k("Mali (Venus)", if (com.droiddeck.launcher.session.VenusComponent.wanted(context)) "in use"
|
||||
else if (com.droiddeck.launcher.session.VenusComponent.available(context)) "available, off" else "not in this build")
|
||||
|
||||
h("Runtime")
|
||||
k("Installed version", LinuxRuntimeInstaller.installedVersion(context))
|
||||
|
||||
@@ -3,11 +3,43 @@ package com.droiddeck.launcher.core
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import java.io.File
|
||||
|
||||
object DeviceSupport {
|
||||
fun adreno(): Boolean =
|
||||
private val adrenoGpu by lazy {
|
||||
File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists()
|
||||
}
|
||||
|
||||
fun adreno(): Boolean = adrenoGpu
|
||||
|
||||
fun graphicsAvailable(): Boolean = graphicsAvailable(adreno(), SystemVulkanDriver.isAvailable())
|
||||
|
||||
internal fun graphicsAvailable(adreno: Boolean, powerVrReady: Boolean): Boolean = adreno || powerVrReady
|
||||
|
||||
fun vkBridge(context: Context): Boolean =
|
||||
bridgeUsable() && !com.droiddeck.launcher.session.VenusComponent.wanted(context) &&
|
||||
com.droiddeck.launcher.session.SessionPrefs.vkBridge(context)
|
||||
|
||||
fun mali(): Boolean = isMali
|
||||
|
||||
private val isMali by lazy {
|
||||
!adreno() && (File("/dev/mali0").exists() ||
|
||||
com.droiddeck.launcher.gpu.GpuInfo.detect().family.name.startsWith("MALI"))
|
||||
}
|
||||
|
||||
fun bridgeUsable(): Boolean = !adreno() && !SystemVulkanDriver.isAvailable() && File("/dev/mali0").exists()
|
||||
|
||||
fun openDriverActive(context: Context): Boolean {
|
||||
if (adreno()) return false
|
||||
if (com.droiddeck.launcher.session.VenusComponent.wanted(context)) return false
|
||||
if (bridgeUsable() && com.droiddeck.launcher.session.SessionPrefs.vkBridge(context)) return false
|
||||
val linux = com.droiddeck.launcher.session.SessionPrefs.linuxDriver(context).ifEmpty { return false }
|
||||
return com.droiddeck.launcher.gpu.DriverPairs.isNonAdreno(com.droiddeck.launcher.gpu.DriverBundle.containing(context, linux, true)?.id)
|
||||
}
|
||||
|
||||
fun canDraw(context: Context): Boolean = graphicsAvailable() || openDriverActive(context) || vkBridge(context) ||
|
||||
com.droiddeck.launcher.session.VenusComponent.wanted(context)
|
||||
|
||||
fun gpuName(context: Context): String {
|
||||
val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it.isNotBlank() && it != Build.UNKNOWN } else null
|
||||
|
||||
@@ -126,11 +126,9 @@ object DownloadSource {
|
||||
|
||||
fun isGithub(url: String): Boolean = GITHUB_HOSTS.any { url.startsWith("https://$it/") }
|
||||
|
||||
/** Catalogs carry the digests everything else is checked against, so a mirror must never supply one. */
|
||||
fun isCatalog(url: String): Boolean = CATALOGS.any { url.startsWith(it) }
|
||||
|
||||
fun candidates(target: String): List<String> {
|
||||
// GitHub API metadata carries executable URLs and digests; never proxy it.
|
||||
if (isCatalog(target) || target.startsWith("https://api.github.com/")) return listOf(target)
|
||||
val prefs = prefs() ?: return listOf(target)
|
||||
if (!isGithub(target)) return listOf(target)
|
||||
@@ -184,7 +182,6 @@ object DownloadSource {
|
||||
prefs(context)?.edit()?.putString(KEY_PROXY, normalizeProxy(value))?.apply()
|
||||
}
|
||||
|
||||
/** An HTTP proxy every Downloader connection uses, or null when none is configured. */
|
||||
fun httpProxy(): Proxy? = proxyOf(prefs()?.getString(KEY_PROXY, "") ?: "")
|
||||
|
||||
internal fun proxyOf(value: String): Proxy? {
|
||||
|
||||
@@ -58,7 +58,7 @@ public final class Downloader {
|
||||
|
||||
private static void closeAsync(InputStream active) {
|
||||
CANCEL_EXECUTOR.execute(() -> {
|
||||
try { active.close(); } catch (IOException ignored) { }
|
||||
try { active.close(); } catch (IOException | IllegalStateException ignored) { }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ public final class Downloader {
|
||||
if (isCancelled(cancelled)) return null;
|
||||
if (i > 0) Log.i(TAG, "retrying " + url + " via " + sources.get(i));
|
||||
Fetch fetch = fetch(sources.get(i), cancelled);
|
||||
// A cancelled fetch says nothing about the source, so it must not count as a failure.
|
||||
if (isCancelled(cancelled)) return null;
|
||||
if (fetch.body != null) {
|
||||
DownloadSource.INSTANCE.succeeded(sources.get(i));
|
||||
@@ -103,7 +102,6 @@ public final class Downloader {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Fetches without the mirror list, for a URL whose body is a digest the payload is checked against. */
|
||||
public static String downloadDirect(String url) {
|
||||
return fetch(url).body;
|
||||
}
|
||||
@@ -113,13 +111,11 @@ public final class Downloader {
|
||||
return downloadFile(url, destination, resume, null, progress);
|
||||
}
|
||||
|
||||
/** Verify each candidate before accepting it, so a bad mirror can fall back. */
|
||||
public static boolean downloadFile(String url, File destination, boolean resume, String expectedSha256,
|
||||
java.util.function.Consumer<Float> progress) {
|
||||
return downloadFile(url, destination, resume, expectedSha256, null, progress);
|
||||
}
|
||||
|
||||
/** {@code cancelled} is polled while bytes arrive; a cancel stops at once and leaves the partial file for the caller. */
|
||||
public static boolean downloadFile(String url, File destination, boolean resume, String expectedSha256,
|
||||
java.util.function.BooleanSupplier cancelled,
|
||||
java.util.function.Consumer<Float> progress) {
|
||||
@@ -128,13 +124,11 @@ public final class Downloader {
|
||||
return downloadCandidates(url, sources(url, expectedSha256), destination, resume, expectedSha256, cancelled, progress);
|
||||
}
|
||||
|
||||
/** A third-party mirror may only serve bytes that can be checked against a digest from a direct source. */
|
||||
static List<String> sources(String url, String expectedSha256) {
|
||||
return expectedSha256 == null ? java.util.Collections.singletonList(url) : DownloadSource.INSTANCE.candidates(url);
|
||||
|
||||
}
|
||||
|
||||
// Package-private for the local multi-source HTTP test.
|
||||
static boolean downloadCandidates(String original, List<String> sources, File destination, boolean resume,
|
||||
String expectedSha256, java.util.function.Consumer<Float> progress) {
|
||||
return downloadCandidates(original, sources, destination, resume, expectedSha256, null, progress);
|
||||
@@ -149,7 +143,6 @@ public final class Downloader {
|
||||
if (isCancelled(cancelled)) return false;
|
||||
if (i > 0) Log.i(TAG, "retrying " + original + " via " + source);
|
||||
Outcome outcome = attempt(source, destination, resume, identity, expectedSha256, cancelled, progress);
|
||||
// A cancel is not a source failure: no other mirror, and no DownloadSource.failed().
|
||||
if (outcome == Outcome.CANCELLED) return false;
|
||||
if (outcome == Outcome.DONE) {
|
||||
if (expectedSha256 != null) {
|
||||
@@ -192,7 +185,6 @@ public final class Downloader {
|
||||
return value != null && !value.isEmpty() && !value.startsWith("W/");
|
||||
}
|
||||
|
||||
/** Only reuse saved bytes when we persisted a strong validator and their source. */
|
||||
private static Identity loadIdentity(File dest, boolean resume, String expectedSha256) {
|
||||
Identity identity = new Identity();
|
||||
File marker = resumeFile(dest);
|
||||
@@ -213,7 +205,6 @@ public final class Downloader {
|
||||
return identity;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Legacy partials or interrupted metadata writes have no trustworthy identity.
|
||||
}
|
||||
if (!discardPartial(dest, identity)) Log.w(TAG, "could not discard unidentified partial " + dest);
|
||||
return identity;
|
||||
@@ -239,7 +230,7 @@ public final class Downloader {
|
||||
try (FileOutputStream out = new FileOutputStream(marker)) {
|
||||
p.store(out, "DroidDeck partial download validator");
|
||||
} catch (IOException e) {
|
||||
marker.delete(); // No valid metadata means no future resume.
|
||||
marker.delete();
|
||||
Log.w(TAG, "cannot persist download validator", e);
|
||||
}
|
||||
}
|
||||
@@ -252,7 +243,6 @@ public final class Downloader {
|
||||
long have = resume && destination.isFile() ? destination.length() : 0;
|
||||
try {
|
||||
if (have > 0 && (!url.equals(identity.source) || !strongETag(identity.etag))) {
|
||||
// A new source or unvalidated bytes must restart from byte zero.
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
have = 0;
|
||||
}
|
||||
@@ -286,7 +276,6 @@ public final class Downloader {
|
||||
return attempt(url, destination, false, identity, expectedSha256, cancelled, progress);
|
||||
}
|
||||
if (!appending && have > 0) {
|
||||
// If-Range may legitimately return full 200 when representation changed.
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
have = 0;
|
||||
}
|
||||
@@ -333,14 +322,12 @@ public final class Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
/** True when a `Content-Range: bytes START-END/TOTAL` header starts exactly at {@code start}. */
|
||||
static boolean rangeStartsAt(String contentRange, long start) {
|
||||
if (contentRange == null) return false;
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile("^\\s*bytes\\s+(\\d+)-\\d+/(?:\\d+|\\*)\\s*$").matcher(contentRange);
|
||||
return m.matches() && Long.parseLong(m.group(1)) == start;
|
||||
}
|
||||
|
||||
/** The complete length in a `Content-Range` header, or -1 when the header leaves it unknown. */
|
||||
static long rangeTotal(String contentRange) {
|
||||
if (contentRange == null) return -1;
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile("^\\s*bytes\\s+\\d+-\\d+/(\\d+)\\s*$").matcher(contentRange);
|
||||
@@ -390,7 +377,6 @@ public final class Downloader {
|
||||
}
|
||||
|
||||
private static HttpURLConnection open(String url, int readTimeoutMs) throws java.io.IOException {
|
||||
// An explicit Proxy routes the connection through the app setting instead of the system-wide proxy.
|
||||
Proxy proxy = DownloadSource.INSTANCE.httpProxy();
|
||||
HttpURLConnection connection = (HttpURLConnection) (proxy != null
|
||||
? new URL(url).openConnection(proxy) : new URL(url).openConnection());
|
||||
|
||||
@@ -30,8 +30,6 @@ object GameEnvironment {
|
||||
fun defaults(preset: String, forceSsbs: Boolean = false): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
"MESA_SHADER_CACHE_DISABLE" to "false",
|
||||
"DROIDDECK_ANTICHEAT_OFFLINE" to "1",
|
||||
"VKD3D_FEATURE_LEVEL" to "12_2",
|
||||
"VKD3D_SHADER_MODEL" to "6_9",
|
||||
"DOTNET_GCRegionRange" to "800000000",
|
||||
"DOTNET_EnableWriteXorExecute" to "0",
|
||||
).apply {
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.net.Uri
|
||||
import android.provider.Settings
|
||||
|
||||
object GlobalSettings {
|
||||
// Settings.Global caches values in-process and misses deletions made over adb; ask the provider directly.
|
||||
fun read(context: Context, name: String): String? {
|
||||
val uri = Uri.withAppendedPath(Settings.Global.CONTENT_URI, name)
|
||||
context.contentResolver.query(uri, arrayOf(Settings.NameValueTable.VALUE), null, null, null)?.use { cursor ->
|
||||
@@ -19,6 +18,5 @@ object GlobalSettings {
|
||||
|
||||
fun developerOptionsOn(context: Context): Boolean = isOn(context, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED)
|
||||
|
||||
// Settings.Global.ADB_WIFI_ENABLED is hidden; the key is stable since Android 11.
|
||||
fun wirelessDebuggingOn(context: Context): Boolean = isOn(context, "adb_wifi_enabled")
|
||||
}
|
||||
@@ -12,6 +12,9 @@ object Hashes {
|
||||
@JvmStatic
|
||||
fun sha512(file: File): String = hex(file, "SHA-512")
|
||||
|
||||
@JvmStatic
|
||||
fun md5(file: File): String = hex(file, "MD5")
|
||||
|
||||
@JvmStatic
|
||||
fun isGithubSha256(digest: String?): Boolean = digest != null && digest.matches(GITHUB_SHA256)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ object LogRedactor {
|
||||
size > 32
|
||||
}
|
||||
|
||||
// Android matchers own native state, so reuse them instead of creating one for every log line.
|
||||
private val matchers = object : ThreadLocal<MatcherCache>() {
|
||||
override fun initialValue(): MatcherCache = MatcherCache()
|
||||
}
|
||||
@@ -132,43 +131,81 @@ object LogRedactor {
|
||||
@Volatile
|
||||
private var accounts: List<Learned> = emptyList()
|
||||
|
||||
fun learnAccounts(vararg loginUsers: java.io.File) {
|
||||
internal const val MAX_LEARN_INPUT_BYTES = 2 * 1024 * 1024
|
||||
private const val MAX_LEARNED_ACCOUNTS = 2048
|
||||
private const val MAX_LEARNED_ADDRESSES = 1024
|
||||
private const val MAX_ACCOUNT_LENGTH = 256
|
||||
|
||||
private fun boundedText(file: java.io.File): String? = try {
|
||||
file.inputStream().buffered().use { input ->
|
||||
val out = java.io.ByteArrayOutputStream()
|
||||
val buffer = ByteArray(16 * 1024)
|
||||
var remaining = MAX_LEARN_INPUT_BYTES
|
||||
while (remaining > 0) {
|
||||
val read = input.read(buffer, 0, minOf(buffer.size, remaining))
|
||||
if (read < 0) return out.toString(Charsets.UTF_8.name())
|
||||
if (read == 0) continue
|
||||
out.write(buffer, 0, read)
|
||||
remaining -= read
|
||||
}
|
||||
if (input.read() != -1) null else out.toString(Charsets.UTF_8.name())
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
fun learnAccounts(vararg loginUsers: java.io.File): Boolean {
|
||||
val names = LinkedHashSet<String>()
|
||||
val kv = Regex("\"(AccountName|PersonaName)\"\\s+\"([^\"]*)\"")
|
||||
var unread = false
|
||||
for (file in loginUsers) {
|
||||
if (!file.isFile) continue
|
||||
val text = try { file.readText() } catch (e: Exception) { unread = true; continue }
|
||||
kv.findAll(text).map { it.groupValues[2].trim() }.filter { it.length >= 3 }.forEach { names += it }
|
||||
val text = boundedText(file)
|
||||
if (text == null) {
|
||||
unread = true
|
||||
continue
|
||||
}
|
||||
for (match in kv.findAll(text)) {
|
||||
val name = match.groupValues[2].trim()
|
||||
if (name.length < 3) continue
|
||||
if (name.length > MAX_ACCOUNT_LENGTH || names.size >= MAX_LEARNED_ACCOUNTS) {
|
||||
unread = true
|
||||
break
|
||||
}
|
||||
names += name
|
||||
}
|
||||
}
|
||||
val learned = names.map { name ->
|
||||
val needs = name.lowercase().takeIf { name.all { it.code < 0x80 } }
|
||||
Learned(Regex("(?i)(?<![A-Za-z0-9_])" + Regex.escape(name) + "(?![A-Za-z0-9_])"), needs)
|
||||
}
|
||||
// A file that could not be read keeps the names learned from it before.
|
||||
accounts = if (unread) (accounts + learned).distinctBy { it.regex.pattern } else learned
|
||||
accounts = if (unread) (learned + accounts).distinctBy { it.regex.pattern }.take(MAX_LEARNED_ACCOUNTS) else learned
|
||||
return !unread
|
||||
}
|
||||
|
||||
fun learnFromRuntime(root: java.io.File) {
|
||||
learnOwnAddresses(java.io.File(root, "etc/droiddeck-net"))
|
||||
learnAccounts(
|
||||
fun learnFromRuntime(root: java.io.File): Boolean {
|
||||
val addressesComplete = learnOwnAddresses(java.io.File(root, "etc/droiddeck-net"))
|
||||
val accountsComplete = learnAccounts(
|
||||
java.io.File(root, "root/.local/share/Steam/config/loginusers.vdf"),
|
||||
java.io.File(root, "root/.droiddeck-x64/.local/share/Steam/config/loginusers.vdf"),
|
||||
)
|
||||
matchers.remove()
|
||||
return addressesComplete && accountsComplete
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var own: List<Learned> = emptyList()
|
||||
|
||||
fun learnOwnAddresses(linkFile: java.io.File) {
|
||||
fun learnOwnAddresses(linkFile: java.io.File): Boolean {
|
||||
val found = ArrayList<Learned>()
|
||||
try {
|
||||
if (linkFile.isFile) linkFile.forEachLine { line ->
|
||||
val addr = line.trim().takeIf { it.startsWith("addr ") }?.split(Regex("\\s+"))?.getOrNull(1) ?: return@forEachLine
|
||||
val text = if (linkFile.isFile) boundedText(linkFile) ?: return false else ""
|
||||
text.lineSequence().forEach { line ->
|
||||
val addr = line.trim().takeIf { it.startsWith("addr ") }?.split(Regex("\\s+"))?.getOrNull(1) ?: return@forEach
|
||||
if (found.size >= MAX_LEARNED_ADDRESSES) return false
|
||||
when (kind(addr)) {
|
||||
"public IPv6" -> {
|
||||
val groups = expand6(addr)?.take(4) ?: return@forEachLine
|
||||
val groups = expand6(addr)?.take(4) ?: return@forEach
|
||||
val prefix = groups.joinToString(":") { g -> "0{0,3}" + Regex.escape(g.trimStart('0').ifEmpty { "0" }) }
|
||||
found += Learned(
|
||||
Regex("(?i)(?<![0-9A-Fa-f:])$prefix(?::[0-9A-Fa-f]{0,4}){1,4}(?:%[A-Za-z0-9_.]+)?"),
|
||||
@@ -179,9 +216,10 @@ object LogRedactor {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
own = found
|
||||
return true
|
||||
}
|
||||
|
||||
fun kind(address: String): String? {
|
||||
@@ -228,7 +266,6 @@ object LogRedactor {
|
||||
return out
|
||||
}
|
||||
|
||||
/** The at-rest rewrite reads a file into memory twice; larger logs are only scrubbed on the way out. */
|
||||
const val IN_MEMORY_SCRUB_MAX = 64L * 1024 * 1024
|
||||
|
||||
fun isBinary(file: java.io.File): Boolean = try {
|
||||
@@ -241,8 +278,6 @@ object LogRedactor {
|
||||
true
|
||||
}
|
||||
|
||||
fun isText(file: java.io.File): Boolean = file.isFile && file.length() < IN_MEMORY_SCRUB_MAX && !isBinary(file)
|
||||
|
||||
const val RULES_VERSION = 3
|
||||
|
||||
private val LAN_IPV4 = Regex(
|
||||
|
||||
@@ -52,8 +52,6 @@ object SessionLogCapture {
|
||||
if (t.parentFile?.absolutePath == dir.absolutePath) stop()
|
||||
}
|
||||
|
||||
/** flush() only reaches the page cache, and a hard reset on f2fs lost the last minutes of every
|
||||
* log. Every 2 s the session folder's files (ours and the native parts') are synced to storage. */
|
||||
private fun startSyncing(dir: File): Thread = Thread({
|
||||
while (true) {
|
||||
try { Thread.sleep(2000) } catch (e: InterruptedException) { break }
|
||||
|
||||
@@ -45,8 +45,6 @@ object SteamIntegration {
|
||||
suggested = !SessionPrefs.sessionPrefs(context).getBoolean("steam_integration_seen", false),
|
||||
)
|
||||
|
||||
// Android 13+ blocks accessibility services for sideloaded APKs until the user allows restricted settings in App info.
|
||||
// MODE_DEFAULT is inconclusive on Android 15, so only an explicit denial counts here.
|
||||
private fun restrictedSettings(context: Context): Boolean {
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) return false
|
||||
val mode = runCatching {
|
||||
@@ -90,7 +88,6 @@ object SteamIntegration {
|
||||
fun brightnessSettings(context: Context) = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
|
||||
Uri.parse("package:${context.packageName}"))
|
||||
|
||||
// ACCESSIBILITY_DETAILS_SETTINGS is restricted to system apps; the fragment args key highlights our entry in the list.
|
||||
fun shortcutSettings(context: Context): Intent {
|
||||
val service = ComponentName(context, HomeButtonService::class.java).flattenToString()
|
||||
return Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||
|
||||
@@ -311,87 +311,3 @@ fun CollapsibleRail(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RailTopTabs(
|
||||
items: List<RailItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
links: List<RailLink> = emptyList(),
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
items.forEach { item ->
|
||||
TopTabCell(
|
||||
icon = item.icon,
|
||||
label = item.label,
|
||||
selected = item.selected,
|
||||
onClick = item.onClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
if (links.isNotEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.width(1.dp)
|
||||
.height(30.dp)
|
||||
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.4f)),
|
||||
)
|
||||
links.forEach { link ->
|
||||
TopTabCell(link.icon, link.label, selected = false, onClick = link.onClick, isTab = false, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopTabCell(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isTab: Boolean = true,
|
||||
) {
|
||||
val tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(9.dp))
|
||||
.background(if (selected) MaterialTheme.colorScheme.primary.copy(alpha = 0.14f) else Color.Transparent)
|
||||
.focusGlide(RoundedCornerShape(9.dp))
|
||||
.then(
|
||||
if (isTab) {
|
||||
Modifier.selectable(selected = selected, role = androidx.compose.ui.semantics.Role.Tab, onClick = onClick)
|
||||
} else {
|
||||
Modifier.clickable(role = androidx.compose.ui.semantics.Role.Button, onClick = onClick)
|
||||
},
|
||||
)
|
||||
.padding(vertical = 6.dp, horizontal = 1.dp),
|
||||
) {
|
||||
Icon(icon, contentDescription = label, tint = tint, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Text(
|
||||
label,
|
||||
color = tint,
|
||||
fontSize = 8.5.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 0.2.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(2.dp)
|
||||
.width(if (selected) 16.dp else 0.dp)
|
||||
.clip(RoundedCornerShape(3.dp))
|
||||
.background(if (selected) MaterialTheme.colorScheme.primary else Color.Transparent),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,6 @@ object FileOps {
|
||||
return true
|
||||
}
|
||||
if (!copyWithProgress(src, dst, progress, isCancelled)) return false
|
||||
// Once the destination is committed, finish deleting the source.
|
||||
return delete(src)
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ object StorageRoots {
|
||||
}
|
||||
return true
|
||||
}
|
||||
// vold-managed locations only hold removable volumes, whatever the fstype is.
|
||||
return path.startsWith("/storage/") || path.startsWith("/mnt/media_rw/")
|
||||
}
|
||||
|
||||
@@ -91,9 +90,8 @@ object StorageRoots {
|
||||
.filter { present(it.state) }
|
||||
.forEachIndexed { index, volume ->
|
||||
val uuid = volume.uuid?.takeIf { it.isNotBlank() }
|
||||
val dirName = volume.directory?.name?.takeIf { it.isNotBlank() }
|
||||
// Unmounted volumes often report neither uuid nor directory; never drop the
|
||||
// volume or a plugged-in drive stays invisible with no explanation.
|
||||
val directory = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) volume.directory else null
|
||||
val dirName = directory?.name?.takeIf { it.isNotBlank() }
|
||||
val descSlug = volume.getDescription(context)?.trim()
|
||||
?.takeIf { it.isNotBlank() && !it.equals("android", ignoreCase = true) }
|
||||
?.replace(Regex("[^A-Za-z0-9._-]+"), "-")
|
||||
@@ -101,15 +99,11 @@ object StorageRoots {
|
||||
entry.removable = !volume.isPrimary && volume.isRemovable
|
||||
if (entry.label == null) entry.label = labelFor(context, volume, uuid ?: dirName ?: descSlug)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
volume.directory?.let { entry.candidates += it }
|
||||
}
|
||||
directory?.let { entry.candidates += it }
|
||||
val name = uuid ?: dirName
|
||||
if (name != null) {
|
||||
entry.candidates += File("/storage/$name")
|
||||
entry.candidates += File("/mnt/media_rw/$name")
|
||||
// Only strict uuid matches own an app directory; a loose match would
|
||||
// attach another volume's folder to this entry.
|
||||
if (uuid != null) appDirs.filter { belongsTo(storageManager!!, it, volume) }
|
||||
.forEach { entry.seeds += it }
|
||||
}
|
||||
@@ -122,8 +116,6 @@ object StorageRoots {
|
||||
File("/mnt/media_rw").listFiles().orEmpty().forEach { child ->
|
||||
if (child.isDirectory) volumeOf(child.name).candidates += child
|
||||
}
|
||||
// USB OTG drives do not always appear as storage volumes or under /storage for
|
||||
// third-party apps, but their kernel mounts are still visible in /proc/mounts.
|
||||
for (mount in externalMounts()) {
|
||||
val entry = volumeOf(mount.name)
|
||||
if (entry.label == null) entry.label = mount.name
|
||||
|
||||
@@ -243,6 +243,8 @@ object AddedGameArt {
|
||||
val steam: (String, File, Set<Slot>) -> Boolean,
|
||||
val sgdbGame: (String, Int?, String) -> Int?,
|
||||
val sgdb: (String, Int, File, Set<Slot>) -> Boolean,
|
||||
val sgdbExactGame: (String, Int?, String) -> Int? = { _, _, _ -> null },
|
||||
val battleNetArt: (Context, BattleNet.Title) -> Boolean = { _, _ -> false },
|
||||
)
|
||||
|
||||
private val network = Sources(
|
||||
@@ -251,6 +253,8 @@ object AddedGameArt {
|
||||
steam = ::download,
|
||||
sgdbGame = SteamGridDb::gameId,
|
||||
sgdb = SteamGridDb::download,
|
||||
sgdbExactGame = SteamGridDb::exactGameId,
|
||||
battleNetArt = { context, title -> BattleNet.fetchCovers(context, listOf(BattleNet.artTitle(title))) },
|
||||
)
|
||||
|
||||
private val EXTRAS = setOf(Slot.LOGO, Slot.ICON)
|
||||
@@ -288,6 +292,9 @@ object AddedGameArt {
|
||||
var changed = false
|
||||
for (game in games) {
|
||||
if (forgetOtherAppsArt(context, game)) changed = true
|
||||
if (forgetNameMatchedBattleNetArt(context, game)) changed = true
|
||||
val battleNetTitle = game.storeId?.takeIf { game.source == Store.BATTLENET.id }?.let(BattleNet::title)
|
||||
if (battleNetTitle != null && runCatching { sources.battleNetArt(context, battleNetTitle) }.getOrDefault(false)) changed = true
|
||||
val cache = cacheDir(context, game)
|
||||
val start = missing(context, game)
|
||||
val again = refetch(cache) intersect start
|
||||
@@ -318,7 +325,7 @@ object AddedGameArt {
|
||||
if (need.isNotEmpty() && sgdbKey.isNotEmpty()) {
|
||||
if (sgdbDown) asked = false else try {
|
||||
val gridId = remembered(File(cache, "sgdb-id"), keyChangedAt) {
|
||||
sources.sgdbGame(sgdbKey, match.appId, game.name)?.toString()
|
||||
(if (battleNetTitle != null) sources.sgdbExactGame else sources.sgdbGame)(sgdbKey, match.appId, game.name)?.toString()
|
||||
}?.toIntOrNull()
|
||||
if (gridId != null && sources.sgdb(sgdbKey, gridId, cache, need)) changed = true
|
||||
} catch (e: Exception) {
|
||||
@@ -343,6 +350,21 @@ object AddedGameArt {
|
||||
if (slots.isEmpty()) File(cache, REFETCH).delete() else File(cache, REFETCH).writeText(slots.joinToString(",") { it.key })
|
||||
}
|
||||
|
||||
internal const val BATTLENET_ART_CHECKED = "battlenet-art-2"
|
||||
|
||||
internal fun forgetNameMatchedBattleNetArt(context: Context, game: AddedGames.Game): Boolean {
|
||||
val title = game.storeId?.takeIf { game.source == Store.BATTLENET.id }?.let(BattleNet::title) ?: return false
|
||||
val cache = cacheDir(context, game)
|
||||
val checked = File(cache, BATTLENET_ART_CHECKED)
|
||||
if (title.steam != null || checked.isFile) return false
|
||||
val dropped = EXTRAS.filter { File(cache, SteamGridDb.cacheName(it)).delete() }
|
||||
EXTRAS.forEach { File(cache, SteamGridDb.cacheName(it) + ".src").delete() }
|
||||
listOf("sgdb-id", EXTRAS_TRIED).forEach { File(cache, it).delete() }
|
||||
cache.mkdirs()
|
||||
checked.writeText("")
|
||||
return dropped.isNotEmpty()
|
||||
}
|
||||
|
||||
internal fun forgetOtherAppsArt(context: Context, game: AddedGames.Game): Boolean {
|
||||
val cache = cacheDir(context, game)
|
||||
val recorded = SteamMatch.get(context, game.folder.path)?.takeIf { it.appId != null }
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import org.json.JSONObject
|
||||
import com.droiddeck.launcher.session.GameStorage
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.SteamLibraries
|
||||
import com.droiddeck.launcher.stores.Store
|
||||
import com.droiddeck.launcher.stores.StoreGameSidecar
|
||||
import com.droiddeck.launcher.stores.StoreInstallRoot
|
||||
@@ -69,6 +70,10 @@ object AddedGames {
|
||||
if (path.startsWith("$roms/")) return "/root/ROMs/" + path.removePrefix("$roms/")
|
||||
}
|
||||
StoreInstallRoot.externalGuestPath(context, host)?.let { return it }
|
||||
for (lib in SteamLibraries.list(context)) {
|
||||
if (path == lib.path) return lib.guest
|
||||
if (path.startsWith("${lib.path}/")) return lib.guest + "/" + path.removePrefix("${lib.path}/")
|
||||
}
|
||||
GameStorage.effective(context)?.let { lib ->
|
||||
if (path.startsWith("${lib.path}/")) return "$LIBRARY/" + path.removePrefix("${lib.path}/")
|
||||
}
|
||||
@@ -83,6 +88,7 @@ object AddedGames {
|
||||
if (!guest.startsWith("/") || guest.split('/').any { it == ".." }) return null
|
||||
val mapped = roots(context).map { it.host to it.guest } + StoreInstallRoot.externalRoots(context) +
|
||||
listOfNotNull(GameStorage.effective(context)?.let { File(it.path) to LIBRARY }) +
|
||||
SteamLibraries.list(context).map { it.host to it.guest } +
|
||||
(android.os.Environment.getExternalStorageDirectory() to "/root/Storage")
|
||||
for ((host, root) in mapped.sortedByDescending { it.second.length }) {
|
||||
if (guest == root) return host
|
||||
@@ -93,10 +99,11 @@ object AddedGames {
|
||||
|
||||
fun candidates(folder: File): List<File> = rank(folder).exes
|
||||
|
||||
fun rank(folder: File): GameExePicker.Ranking {
|
||||
fun rank(folder: File, reuse: Boolean = false): GameExePicker.Ranking {
|
||||
val key = "${folder.path}|${folder.lastModified()}"
|
||||
val now = System.currentTimeMillis()
|
||||
ranked[key]?.takeIf { now - it.first < RANK_FRESH_MS }?.let { return it.second }
|
||||
if (reuse) kept?.rank(folder)?.let { return it }
|
||||
val guessed = GameExePicker.rank(folder) { !skipExe(it.name) }
|
||||
val declared = declaredExes(folder)
|
||||
val preferred = declared.map { it.path }.toSet()
|
||||
@@ -108,23 +115,27 @@ object AddedGames {
|
||||
)
|
||||
if (ranked.size > 256) ranked.clear()
|
||||
ranked[key] = now to result
|
||||
kept?.keepRank(folder, result)
|
||||
return result
|
||||
}
|
||||
|
||||
private val ranked = java.util.concurrent.ConcurrentHashMap<String, Pair<Long, GameExePicker.Ranking>>()
|
||||
private const val RANK_FRESH_MS = 60_000L
|
||||
@Volatile private var kept: AddedGamesCache? = null
|
||||
|
||||
private val identified = java.util.concurrent.ConcurrentHashMap<String, String>()
|
||||
|
||||
internal fun identifiedName(folder: File, exe: File): String? {
|
||||
val key = "${exe.path}|${exe.length()}|${exe.lastModified()}|${folder.lastModified()}"
|
||||
identified[key]?.let { return it.ifEmpty { null } }
|
||||
kept?.name(key)?.let { identified[key] = it; return it.ifEmpty { null } }
|
||||
val id = runCatching { GameIdentifier.identify(exe) }.getOrNull()
|
||||
val found = id?.name?.takeIf {
|
||||
id.nameSource in setOf(GameIdentifier.Source.STEAM_MANIFEST_ACF, GameIdentifier.Source.GOG_INFO, GameIdentifier.Source.PE_VERSION)
|
||||
}
|
||||
val name = found?.let { if (id?.nameSource == GameIdentifier.Source.PE_VERSION) betterSpelling(underscoresToSpaces(it), folder.name) else it }
|
||||
identified[key] = name.orEmpty()
|
||||
kept?.keepName(key, name.orEmpty())
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -166,25 +177,39 @@ object AddedGames {
|
||||
"launcher", "game", "engine", "system", "data", "app",
|
||||
)
|
||||
|
||||
internal fun groupGames(folder: File): List<File>? {
|
||||
internal fun groupGames(folder: File, reuse: Boolean = false): List<File>? {
|
||||
val files = folder.listFiles() ?: return null
|
||||
if (files.any { it.isFile && it.name.endsWith(".exe", ignoreCase = true) && !skipExe(it.name) }) return null
|
||||
if (declaredExes(folder).isNotEmpty()) return null
|
||||
val dirs = files.filter { it.isDirectory && !it.name.startsWith(".") && !isSteamLibraryFolder(it.name) }.sortedBy { it.name.lowercase() }
|
||||
if (dirs.any { it.name.lowercase() in GAME_PARTS || GameExePicker.auxiliaryDir(it) }) return null
|
||||
return dirs.filter { rank(it).exes.isNotEmpty() }.takeIf { it.size >= 2 }
|
||||
return dirs.filter { rank(it, reuse).exes.isNotEmpty() }.takeIf { it.size >= 2 }
|
||||
}
|
||||
|
||||
private fun expand(folder: File, entries: Set<String>): List<File> =
|
||||
if (folder.path in entries) listOf(folder) else groupGames(folder) ?: listOf(folder)
|
||||
private fun expand(folder: File, entries: Set<String>, reuse: Boolean = false): List<File> =
|
||||
if (folder.path in entries) listOf(folder) else groupGames(folder, reuse) ?: listOf(folder)
|
||||
|
||||
private fun entryFolders(context: Context): Set<String> = AddedExes.list(context).map { it.folder }.toSet()
|
||||
|
||||
/** The card's Steam libraries: the arm64 client's steamapps and the x86-64 client's own folder beside it. */
|
||||
private fun libraryDepotInstalls(context: Context): List<File> =
|
||||
SteamLibraries.list(context).flatMap { SteamLibraries.depotInstalls(File(it.path, "steamapps/common")) }
|
||||
|
||||
internal fun isSteamLibraryFolder(name: String): Boolean =
|
||||
name.equals("steamapps", ignoreCase = true) || name == com.droiddeck.launcher.session.GameStorage.X64_CARD_DIR
|
||||
|
||||
fun scan(context: Context): List<Game> {
|
||||
private val scanning = Any()
|
||||
|
||||
fun scan(context: Context): List<Game> = synchronized(scanning) {
|
||||
val file = File(context.filesDir, AddedGamesCache.FILE)
|
||||
val cache = kept?.takeIf { it.file == file } ?: AddedGamesCache(file).also { kept = it }
|
||||
try {
|
||||
scanAll(context)
|
||||
} finally {
|
||||
cache.save()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanAll(context: Context): List<Game> {
|
||||
val out = ArrayList<Game>()
|
||||
GamesFolderMigration.run(context)
|
||||
val folders = listOfNotNull(
|
||||
@@ -200,15 +225,16 @@ object AddedGames {
|
||||
if (folder.name.lowercase() in steamInstalls || isSteamLibraryFolder(folder.name)) continue
|
||||
if (folder.path in removed) continue
|
||||
if (folder.name == "Games" && StoreInstallRoot.roots(context).any { it.absolutePath == folder.absolutePath }) continue
|
||||
for (game in expand(folder, entries)) if (game.path !in removed) scanGame(context, game, out)
|
||||
for (game in expand(folder, entries, reuse = true)) if (game.path !in removed) scanGame(context, game, out, reuse = true)
|
||||
}
|
||||
}
|
||||
for (entry in AddedExes.list(context)) {
|
||||
val folder = File(entry.folder)
|
||||
if (folder.path in removed || !folder.isDirectory) continue
|
||||
scanGame(context, folder, out, single = entry)
|
||||
scanGame(context, folder, out, single = entry, reuse = true)
|
||||
}
|
||||
for (folder in StoreInstallRoot.gameFolders(context)) if (StoreInstallRoot.isFinished(folder)) scanGame(context, folder, out)
|
||||
for (folder in libraryDepotInstalls(context)) if (folder.path !in removed) scanGame(context, folder, out, reuse = true)
|
||||
for (folder in StoreInstallRoot.gameFolders(context)) if (StoreInstallRoot.isFinished(folder)) scanGame(context, folder, out, reuse = true)
|
||||
for (game in com.droiddeck.launcher.runtime.BattleNet.games(context)) {
|
||||
if (game.folder.path !in removed) com.droiddeck.launcher.runtime.BattleNet.shortcut(context, game)?.let(out::add)
|
||||
}
|
||||
@@ -254,6 +280,7 @@ object AddedGames {
|
||||
entries.map { File(it.folder) }.filter(::owns).maxByOrNull { canonical(it).length }?.let { return it }
|
||||
SessionPrefs.removedAddedGames(context).map { File(it) }.filter { owns(it) && groupGames(it) == null }.maxByOrNull { canonical(it).length }?.let { return it }
|
||||
StoreInstallRoot.gameFolders(context).firstOrNull(::holds)?.let { return it }
|
||||
libraryDepotInstalls(context).firstOrNull(::holds)?.let { return it }
|
||||
val removed = SessionPrefs.removedAddedGames(context)
|
||||
val dirs = listOfNotNull(
|
||||
GameStorage.effective(context)?.let { File(it.path, "steamapps/common") },
|
||||
@@ -315,7 +342,6 @@ object AddedGames {
|
||||
|
||||
fun addFolder(context: Context, dir: File, onGame: (Game) -> Unit = {}, onChecking: (String?) -> Unit = {}, showHidden: Boolean = false): FolderResult {
|
||||
val app = context.applicationContext
|
||||
// Binds are mounted at session start, so a new one reaches the session from the next start.
|
||||
if (guestPath(app, dir) == null) bindFolder(app, dir)
|
||||
val removed = SessionPrefs.removedAddedGames(app).toSet()
|
||||
val listed = listedFolders(app, removed)
|
||||
@@ -372,6 +398,7 @@ object AddedGames {
|
||||
}
|
||||
AddedExes.list(context).filter { it.folder !in removed }.forEach { out.add(canonical(File(it.folder))) }
|
||||
StoreInstallRoot.gameFolders(context).forEach { out.add(canonical(it)) }
|
||||
libraryDepotInstalls(context).filter { it.path !in removed }.forEach { out.add(canonical(it)) }
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -388,11 +415,11 @@ object AddedGames {
|
||||
|
||||
private val INSTALL_DIR = Regex("\"installdir\"\\s*\"([^\"]*)\"", RegexOption.IGNORE_CASE)
|
||||
|
||||
private fun scanGame(context: Context, folder: File, out: MutableList<Game>, single: AddedExes.Entry? = null) {
|
||||
private fun scanGame(context: Context, folder: File, out: MutableList<Game>, single: AddedExes.Entry? = null, reuse: Boolean = false) {
|
||||
run {
|
||||
val sidecar = StoreGameSidecar.read(folder)
|
||||
sidecar?.takeIf { it.id.isNotEmpty() }?.let { SessionPrefs.followStoreGame(context, "${it.store.id}:${it.id}", folder.path) { old -> !File(old).exists() } }
|
||||
val ranking = rank(folder)
|
||||
val ranking = rank(folder, reuse)
|
||||
val candidates = ranking.exes
|
||||
val picked = SessionPrefs.addedGameExe(context, folder.path)
|
||||
val chosen = picked.takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.isFile }
|
||||
@@ -416,7 +443,14 @@ object AddedGames {
|
||||
?.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 storeExe = sidecar?.exeFile(folder)
|
||||
val launched = adopted ?: chosen ?: storeExe?.takeIf { it.isFile }
|
||||
val storeOwnExe = launched != null && storeExe != null && canonical(launched) == canonical(storeExe)
|
||||
val sidecarDir = sidecar?.workdir?.takeIf { storeOwnExe }?.let { dir ->
|
||||
guestPath(context, if (dir.isEmpty()) folder else File(folder, dir))
|
||||
}
|
||||
val guestDir = SessionPrefs.addedGameStartIn(context, folder.path).takeIf { sidecar == null && it.isNotEmpty() }
|
||||
?: sidecarDir
|
||||
?: guestPath(context, (if (adopted != null) adopted else launch).parentFile ?: folder) ?: return
|
||||
val steamId = config?.let { steamRoute(it, appId) }
|
||||
out.add(Game(folder, name, exe, guestExe, guestDir, appId,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.util.AtomicFile
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
internal class AddedGamesCache(val file: File) {
|
||||
private class Rank(val modified: Long, val at: Long, val ranking: GameExePicker.Ranking)
|
||||
private class Name(val name: String, val at: Long)
|
||||
|
||||
private val ranks = ConcurrentHashMap<String, Rank>()
|
||||
private val names = ConcurrentHashMap<String, Name>()
|
||||
@Volatile private var changed = false
|
||||
|
||||
init {
|
||||
val json = runCatching { JSONObject(AtomicFile(file).readFully().toString(Charsets.UTF_8)) }.getOrNull()
|
||||
json?.optJSONObject("ranks")?.let { all ->
|
||||
for (path in all.keys()) runCatching {
|
||||
val o = all.getJSONObject(path)
|
||||
val exes = o.getJSONArray("exes")
|
||||
val scores = o.getJSONArray("scores")
|
||||
require(exes.length() > 0 && exes.length() == scores.length())
|
||||
ranks[path] = Rank(o.getLong("modified"), o.getLong("at"), GameExePicker.Ranking(
|
||||
List(exes.length()) { File(exes.getString(it)) }, List(scores.length()) { scores.getInt(it) }, o.optBoolean("uncertain"),
|
||||
))
|
||||
}
|
||||
}
|
||||
json?.optJSONObject("names")?.let { all ->
|
||||
for (key in all.keys()) runCatching { all.getJSONObject(key).let { names[key] = Name(it.getString("name"), it.getLong("at")) } }
|
||||
}
|
||||
}
|
||||
|
||||
fun rank(folder: File): GameExePicker.Ranking? {
|
||||
val kept = ranks[folder.path] ?: return null
|
||||
val age = System.currentTimeMillis() - kept.at
|
||||
val top = kept.ranking.exes.first()
|
||||
return kept.ranking.takeIf { kept.modified == folder.lastModified() && age in 0 until KEEP_MS && top.isFile }
|
||||
}
|
||||
|
||||
fun keepRank(folder: File, ranking: GameExePicker.Ranking) {
|
||||
if (ranking.exes.isEmpty()) {
|
||||
if (ranks.remove(folder.path) != null) changed = true
|
||||
return
|
||||
}
|
||||
ranks[folder.path] = Rank(folder.lastModified(), System.currentTimeMillis(), ranking)
|
||||
changed = true
|
||||
}
|
||||
|
||||
fun name(key: String): String? = names[key]?.takeIf { System.currentTimeMillis() - it.at in 0 until KEEP_MS }?.name
|
||||
|
||||
fun keepName(key: String, name: String) {
|
||||
names[key] = Name(name, System.currentTimeMillis())
|
||||
changed = true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun save() {
|
||||
if (!changed) return
|
||||
changed = false
|
||||
val oldest = System.currentTimeMillis() - KEEP_MS
|
||||
val keptRanks = JSONObject()
|
||||
ranks.forEach { (path, r) ->
|
||||
if (r.at >= oldest) keptRanks.put(path, JSONObject().put("modified", r.modified).put("at", r.at)
|
||||
.put("exes", JSONArray(r.ranking.exes.map { it.path })).put("scores", JSONArray(r.ranking.scores))
|
||||
.put("uncertain", r.ranking.uncertain))
|
||||
}
|
||||
val keptNames = JSONObject()
|
||||
names.forEach { (key, n) -> if (n.at >= oldest) keptNames.put(key, JSONObject().put("name", n.name).put("at", n.at)) }
|
||||
val atomic = AtomicFile(file)
|
||||
var out: java.io.FileOutputStream? = null
|
||||
try {
|
||||
out = atomic.startWrite()
|
||||
out.write(JSONObject().put("ranks", keptRanks).put("names", keptNames).toString().toByteArray())
|
||||
atomic.finishWrite(out)
|
||||
} catch (e: Exception) {
|
||||
out?.let(atomic::failWrite)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FILE = "added-games-scan.json"
|
||||
const val KEEP_MS = 7L * 24 * 3600 * 1000
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.droiddeck.launcher.frontend
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.util.AtomicFile
|
||||
import android.util.Log
|
||||
@@ -20,7 +22,11 @@ object AutoComponents {
|
||||
private const val TAG = "AutoComponents"
|
||||
|
||||
fun appKey(game: Library.SteamGame): String? =
|
||||
if (game.library == Library.ADDED) Integer.toUnsignedString(game.appId) else game.appId.takeIf { it > 0 }?.toString()
|
||||
when {
|
||||
game.source == com.droiddeck.launcher.stores.Store.BATTLENET.id -> game.profileKey
|
||||
game.library == Library.ADDED -> Integer.toUnsignedString(game.appId)
|
||||
else -> game.appId.takeIf { it > 0 }?.toString()
|
||||
}
|
||||
|
||||
fun autoPicks(findings: List<WinCompSources.Finding>, catalog: Map<String, WinComponents.Component>): Map<String, WinComponents.AutoReason> {
|
||||
val out = LinkedHashMap<String, WinComponents.AutoReason>()
|
||||
@@ -48,6 +54,8 @@ object AutoComponents {
|
||||
|
||||
val downloading = MutableStateFlow<Map<String, WinComponents.Progress>>(emptyMap())
|
||||
|
||||
val repairFailures = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||
|
||||
val pendingGames = MutableStateFlow<Set<String>>(emptySet())
|
||||
|
||||
val pendingComponents = MutableStateFlow<Set<String>>(emptySet())
|
||||
@@ -56,12 +64,12 @@ object AutoComponents {
|
||||
if (job.pics != null) return
|
||||
val flow = if (job.component != null) pendingComponents else pendingGames
|
||||
val id = job.component ?: job.game?.let { appKey(it) } ?: return
|
||||
flow.value = if (on) flow.value + id else flow.value - id
|
||||
flow.value = if (on || job.component != null && job.key in queuedKeys) flow.value + id else flow.value - id
|
||||
}
|
||||
|
||||
internal class Job(
|
||||
val game: Library.SteamGame?, val component: String?, val asked: Boolean, val seq: Long, val pics: List<Int>? = null,
|
||||
val inSession: Boolean = false,
|
||||
val inSession: Boolean = false, val revision: Int? = null,
|
||||
) {
|
||||
val key get() = when {
|
||||
pics != null -> "pics"
|
||||
@@ -79,29 +87,45 @@ object AutoComponents {
|
||||
@Volatile private var worker: Thread? = null
|
||||
private const val PAUSE_MS = 15_000L
|
||||
|
||||
private fun waitFor(game: Library.SteamGame, id: String) {
|
||||
val key = appKey(game) ?: return
|
||||
synchronized(waiting) { waiting.getOrPut(id) { ArrayList() }.let { if (it.none { g -> appKey(g) == key }) it.add(game) } }
|
||||
}
|
||||
|
||||
fun queueGame(context: Context, game: Library.SteamGame, asked: Boolean) {
|
||||
if (appKey(game) == null) return
|
||||
enqueue(context, Job(game, null, asked, seq.incrementAndGet()))
|
||||
}
|
||||
|
||||
private fun queueDownload(context: Context, id: String, asked: Boolean, inSession: Boolean = false) =
|
||||
enqueue(context, Job(null, id, asked, seq.incrementAndGet(), inSession = inSession))
|
||||
fun queueRepair(context: Context, id: String, gameKey: String?, revision: Int) {
|
||||
if (!WinComponents.validId(id)) return
|
||||
gameKey?.let { Pending.add(context, it, listOf(id), mapOf(id to revision)) }
|
||||
queueDownload(context, id, asked = true, inSession = gameKey != null,
|
||||
revision = maxOf(revision, Pending.revision(context, id) ?: 0))
|
||||
}
|
||||
|
||||
private fun queueDownload(context: Context, id: String, asked: Boolean, inSession: Boolean = false, revision: Int? = null) =
|
||||
enqueue(context, Job(null, id, asked, seq.incrementAndGet(), inSession = inSession, revision = revision))
|
||||
|
||||
private fun enqueue(context: Context, job: Job) {
|
||||
if (!queuedKeys.add(job.key)) {
|
||||
synchronized(jobs) {
|
||||
val added = synchronized(jobs) {
|
||||
if (!queuedKeys.add(job.key)) {
|
||||
val old = jobs.firstOrNull { it.key == job.key }
|
||||
if (old != null && ((job.asked && !old.asked) || (job.inSession && !old.inSession)) && jobs.remove(old)) {
|
||||
val revision = listOfNotNull(old?.revision, job.revision).maxOrNull()
|
||||
val pics = listOfNotNull(old?.pics, job.pics).flatten().distinct().takeIf { old?.pics != null || job.pics != null }
|
||||
if (old != null && ((job.asked && !old.asked) || (job.inSession && !old.inSession) || revision != old.revision || pics != old.pics) && jobs.remove(old)) {
|
||||
jobs.add(Job(old.game ?: job.game, old.component, old.asked || job.asked,
|
||||
if (job.asked && !old.asked) job.seq else old.seq, old.pics ?: job.pics, old.inSession || job.inSession))
|
||||
if (job.asked && !old.asked) job.seq else old.seq, pics,
|
||||
old.inSession || job.inSession, revision))
|
||||
}
|
||||
}
|
||||
if (runNow && !draining) drain(context)
|
||||
return
|
||||
false
|
||||
} else {
|
||||
jobs.add(job)
|
||||
mark(job, true)
|
||||
true
|
||||
}.also { (jobs as java.lang.Object).notifyAll() }
|
||||
}
|
||||
if (job.component != null) Log.i(TAG, "wincomp: queued ${job.component}")
|
||||
mark(job, true)
|
||||
jobs.add(job)
|
||||
if (added && job.component != null) Log.i(TAG, "wincomp: queued ${job.component}")
|
||||
if (runNow) { if (!draining) drain(context) } else ensureWorker(context.applicationContext)
|
||||
}
|
||||
|
||||
@@ -113,8 +137,15 @@ object AutoComponents {
|
||||
|
||||
private fun gameProcessRunning(): Boolean = com.droiddeck.launcher.session.SteamGameProcess.running()
|
||||
|
||||
private fun queuePending(context: Context, key: String) {
|
||||
Pending.ids(context, key).forEach { id ->
|
||||
queueDownload(context, id, asked = true, inSession = true, revision = Pending.revision(context, id))
|
||||
}
|
||||
}
|
||||
|
||||
fun queueInSession(context: Context, game: Library.SteamGame) {
|
||||
if (appKey(game) == null) return
|
||||
val key = appKey(game) ?: return
|
||||
queuePending(context, key)
|
||||
enqueue(context, Job(game, null, true, seq.incrementAndGet(), inSession = true))
|
||||
}
|
||||
|
||||
@@ -124,7 +155,6 @@ object AutoComponents {
|
||||
try {
|
||||
while (true) {
|
||||
val job = next(context) ?: return
|
||||
queuedKeys.remove(job.key)
|
||||
run(context, job)
|
||||
}
|
||||
} finally {
|
||||
@@ -135,6 +165,7 @@ object AutoComponents {
|
||||
private fun next(context: Context): Job? = synchronized(jobs) {
|
||||
val job = jobs.sortedWith(order).firstOrNull { !mustWait(context, it) } ?: return null
|
||||
jobs.remove(job)
|
||||
queuedKeys.remove(job.key)
|
||||
job
|
||||
}
|
||||
|
||||
@@ -148,13 +179,17 @@ object AutoComponents {
|
||||
worker = Thread({
|
||||
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND)
|
||||
while (true) {
|
||||
try { jobs.put(jobs.take()) } catch (e: InterruptedException) { return@Thread }
|
||||
val job = next(app)
|
||||
if (job == null) {
|
||||
try { Thread.sleep(PAUSE_MS) } catch (e: InterruptedException) { return@Thread }
|
||||
continue
|
||||
val job = try {
|
||||
synchronized(jobs) {
|
||||
next(app) ?: run {
|
||||
(jobs as java.lang.Object).wait(PAUSE_MS)
|
||||
next(app)
|
||||
}
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
return@Thread
|
||||
}
|
||||
queuedKeys.remove(job.key)
|
||||
if (job == null) continue
|
||||
WinComponents.background { run(app, job) }
|
||||
}
|
||||
}, "auto-components").apply { isDaemon = true; start() }
|
||||
@@ -164,18 +199,23 @@ object AutoComponents {
|
||||
try {
|
||||
when {
|
||||
job.pics != null -> SteamAppInfo.lookup(context, job.pics)
|
||||
job.component != null -> download(context, job.component, job.asked, job.inSession)
|
||||
job.component != null -> download(context, job.component, job.asked, job.inSession, job.revision)
|
||||
else -> job.game?.let { apply(context, it, job.asked, job.inSession) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "wincomp: ${job.key}: ${e.javaClass.simpleName}: ${e.message}")
|
||||
job.component?.let {
|
||||
repairFailures.value = repairFailures.value + (it to (e.message ?: "Install failed"))
|
||||
Pending.drop(context, it, job.revision)
|
||||
}
|
||||
} finally {
|
||||
mark(job, false)
|
||||
changes.value = changes.value + 1
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(context: Context, game: Library.SteamGame, asked: Boolean, inSession: Boolean = false) {
|
||||
private fun apply(context: Context, game: Library.SteamGame, asked: Boolean, inSession: Boolean = false,
|
||||
knownCatalog: Map<String, WinComponents.Component>? = null) {
|
||||
val key = appKey(game) ?: return
|
||||
val folder = game.gameFiles
|
||||
if (game.library == Library.ADDED && game.source == Library.ADDED && folder != null && SteamMatch.get(context, folder.path) == null) {
|
||||
@@ -183,9 +223,10 @@ object AutoComponents {
|
||||
}
|
||||
val store = com.droiddeck.launcher.stores.Store.byId(game.source)
|
||||
if (folder != null && (store == com.droiddeck.launcher.stores.Store.GOG || store == com.droiddeck.launcher.stores.Store.EPIC)) StoreListBackfill.fill(context, folder)
|
||||
if (asked && online(context)) steamAppOf(context, game)?.let { SteamAppInfo.lookup(context, listOf(it)) }
|
||||
if (knownCatalog == null && asked && online(context)) steamAppOf(context, game)?.let { SteamAppInfo.lookup(context, listOf(it)) }
|
||||
val findings = findings(context, game)
|
||||
val catalog = (catalogOf(context, false) ?: catalogOf(context, true))?.associateBy { it.name } ?: return
|
||||
val refreshed = if (knownCatalog == null && asked && online(context)) catalogOf(context, true) else null
|
||||
val catalog = knownCatalog ?: (refreshed ?: catalogOf(context, false) ?: catalogOf(context, true))?.associateBy { it.name } ?: return
|
||||
val wanted = autoPicks(findings, catalog)
|
||||
val user = WinComponents.selection(context, key).user
|
||||
val before = WinComponents.selection(context, key).auto
|
||||
@@ -196,22 +237,37 @@ object AutoComponents {
|
||||
if (user[id] == false) { kept[id] = reason; continue }
|
||||
if (id in installed) { kept[id] = reason; continue }
|
||||
if (id in before) kept[id] = reason
|
||||
synchronized(waiting) { waiting.getOrPut(id) { ArrayList() }.let { if (it.none { g -> appKey(g) == key }) it.add(game) } }
|
||||
queueDownload(context, id, asked, inSession)
|
||||
waitFor(game, id)
|
||||
missing.add(id)
|
||||
}
|
||||
if (inSession) Pending.set(context, key, (Pending.ids(context, key).filter { it !in installed && it !in wanted } + missing).distinct())
|
||||
if (before.keys != kept.keys) {
|
||||
WinComponents.setAuto(context, key, kept)
|
||||
Log.i(TAG, "${game.name}: on by itself ${kept.keys.joinToString().ifEmpty { "(none)" }}")
|
||||
}
|
||||
Pending.update(context, key, catalog.mapValues { it.value.revision }) { ids ->
|
||||
ids.filter { id -> user[id] != false && (id !in installed && id !in wanted || (Pending.revision(context, id) ?: 0) > (catalog[id]?.revision ?: 0)) } + missing
|
||||
}
|
||||
missing.forEach { id -> queueDownload(context, id, asked, inSession, catalog[id]?.revision) }
|
||||
changes.value = changes.value + 1
|
||||
}
|
||||
|
||||
private fun download(context: Context, id: String, asked: Boolean, inSession: Boolean = false) {
|
||||
val catalog = catalogOf(context, true)?.associateBy { it.name } ?: run { Pending.drop(context, id); return }
|
||||
private fun download(context: Context, id: String, asked: Boolean, inSession: Boolean = false, requestedRevision: Int? = null) {
|
||||
val catalog = catalogOf(context, true)?.associateBy { it.name } ?: run {
|
||||
repairFailures.value = repairFailures.value + (id to "Could not load the component catalog")
|
||||
return
|
||||
}
|
||||
val catalogComponent = catalog[id]
|
||||
val requiredRevision = maxOf(requestedRevision ?: 0, Pending.revision(context, id) ?: 0)
|
||||
if (catalogComponent != null && catalogComponent.revision < requiredRevision) {
|
||||
repairFailures.value = repairFailures.value + (id to "The component catalog is older than the requested revision")
|
||||
return
|
||||
}
|
||||
if (id !in installedOf(context)) {
|
||||
val c = catalog[id] ?: run { Pending.drop(context, id); return }
|
||||
val c = catalogComponent ?: run {
|
||||
repairFailures.value = repairFailures.value + (id to "Component is no longer available")
|
||||
Pending.drop(context, id, requestedRevision)
|
||||
return
|
||||
}
|
||||
val started = System.currentTimeMillis()
|
||||
downloading.value = downloading.value + (id to WinComponents.Progress(id, "", WinComponents.Phase.DOWNLOAD, -1))
|
||||
val problem = try {
|
||||
@@ -219,12 +275,28 @@ object AutoComponents {
|
||||
} finally {
|
||||
downloading.value = downloading.value - id
|
||||
}
|
||||
if (problem != null) { Log.w(TAG, "wincomp: $id did not install: $problem"); Pending.drop(context, id); return }
|
||||
if (problem != null) {
|
||||
repairFailures.value = repairFailures.value + (id to problem)
|
||||
Log.w(TAG, "wincomp: $id did not install: $problem")
|
||||
Pending.drop(context, id, maxOf(requestedRevision ?: 0, c.revision))
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "wincomp: $id installed in ${System.currentTimeMillis() - started}ms")
|
||||
}
|
||||
val games = synchronized(waiting) { waiting.remove(id).orEmpty() }
|
||||
games.forEach { apply(context, it, asked, inSession) }
|
||||
Pending.drop(context, id)
|
||||
var applied = true
|
||||
games.forEach { game ->
|
||||
try {
|
||||
apply(context, game, asked, inSession, knownCatalog = catalog)
|
||||
} catch (e: Exception) {
|
||||
applied = false
|
||||
appKey(game)?.let { key -> Pending.add(context, key, listOf(id), mapOf(id to (catalog[id]?.revision ?: requestedRevision ?: 0))) }
|
||||
waitFor(game, id)
|
||||
Log.w(TAG, "wincomp: $id installed, but ${game.name} was not updated: ${e.javaClass.simpleName}: ${e.message}")
|
||||
}
|
||||
}
|
||||
if (applied) Pending.drop(context, id, maxOf(requestedRevision ?: 0, catalog[id]?.revision ?: 0))
|
||||
repairFailures.value = repairFailures.value - id
|
||||
}
|
||||
|
||||
internal object Pending {
|
||||
@@ -233,24 +305,37 @@ object AutoComponents {
|
||||
@Synchronized fun read(context: Context): JSONObject =
|
||||
runCatching { JSONObject(file(context).readText()) }.getOrDefault(JSONObject()).let { it.optJSONObject("games") ?: JSONObject() }
|
||||
|
||||
@Synchronized fun set(context: Context, key: String, ids: List<String>) {
|
||||
@Synchronized fun set(context: Context, key: String, ids: List<String>, revisions: Map<String, Int> = emptyMap()) {
|
||||
val games = read(context)
|
||||
if (ids.isEmpty()) games.remove(key) else games.put(key, JSONArray(ids))
|
||||
write(context, games)
|
||||
write(context, games, revisions)
|
||||
}
|
||||
|
||||
@Synchronized fun ids(context: Context, key: String): List<String> =
|
||||
read(context).optJSONArray(key)?.let { a -> (0 until a.length()).map { a.optString(it) } }.orEmpty()
|
||||
|
||||
@Synchronized fun add(context: Context, key: String, ids: List<String>) {
|
||||
@Synchronized fun update(context: Context, key: String, revisions: Map<String, Int>, transform: (List<String>) -> List<String>): Boolean {
|
||||
val games = read(context)
|
||||
val current = games.optJSONArray(key)?.let { a -> (0 until a.length()).map { a.optString(it) } }.orEmpty()
|
||||
val ids = transform(current).distinct()
|
||||
if (ids.isEmpty()) games.remove(key) else games.put(key, JSONArray(ids))
|
||||
return write(context, games, revisions)
|
||||
}
|
||||
|
||||
@Synchronized fun revision(context: Context, id: String): Int? =
|
||||
runCatching { JSONObject(file(context).readText()).optJSONObject("revisions")?.optInt(id) }.getOrNull()
|
||||
|
||||
@Synchronized fun add(context: Context, key: String, ids: List<String>, revisions: Map<String, Int> = emptyMap()) {
|
||||
val games = read(context)
|
||||
val have = games.optJSONArray(key)?.let { a -> (0 until a.length()).map { a.optString(it) } }.orEmpty()
|
||||
games.put(key, JSONArray((have + ids).distinct()))
|
||||
write(context, games)
|
||||
write(context, games, revisions)
|
||||
}
|
||||
|
||||
@Synchronized fun drop(context: Context, id: String) {
|
||||
@Synchronized fun drop(context: Context, id: String, throughRevision: Int? = null) {
|
||||
val games = read(context)
|
||||
val required = runCatching { JSONObject(file(context).readText()).optJSONObject("revisions")?.optInt(id, 0) ?: 0 }.getOrDefault(0)
|
||||
if (throughRevision != null && required > throughRevision) return
|
||||
for (k in games.keys().asSequence().toList()) {
|
||||
val left = games.optJSONArray(k)?.let { a -> (0 until a.length()).map { a.optString(it) } }.orEmpty() - id
|
||||
if (left.isEmpty()) games.remove(k) else games.put(k, JSONArray(left))
|
||||
@@ -258,13 +343,29 @@ object AutoComponents {
|
||||
write(context, games)
|
||||
}
|
||||
|
||||
private fun write(context: Context, games: JSONObject) {
|
||||
private fun write(context: Context, games: JSONObject, required: Map<String, Int> = emptyMap()): Boolean {
|
||||
val f = file(context)
|
||||
runCatching {
|
||||
return runCatching {
|
||||
f.parentFile?.mkdirs()
|
||||
val tmp = File(f.path + ".tmp")
|
||||
tmp.writeText(JSONObject().put("games", games).toString())
|
||||
if (!tmp.renameTo(f)) { f.delete(); tmp.renameTo(f) }
|
||||
val before = runCatching { JSONObject(f.readText()).optJSONObject("revisions") }.getOrNull()
|
||||
val revisions = JSONObject()
|
||||
for (key in games.keys()) {
|
||||
val ids = games.getJSONArray(key)
|
||||
for (i in 0 until ids.length()) {
|
||||
val id = ids.getString(i)
|
||||
revisions.put(id, maxOf(before?.optInt(id, 0) ?: 0, required[id] ?: 0))
|
||||
}
|
||||
}
|
||||
tmp.writeText(JSONObject().put("games", games).put("revisions", revisions).toString())
|
||||
if (!tmp.renameTo(f)) {
|
||||
f.delete()
|
||||
if (!tmp.renameTo(f)) throw java.io.IOException("Could not replace pending repair file")
|
||||
}
|
||||
true
|
||||
}.getOrElse {
|
||||
Log.w(TAG, "wincomp: could not save pending repairs: ${it.javaClass.simpleName}: ${it.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,21 +430,76 @@ object AutoComponents {
|
||||
}
|
||||
|
||||
|
||||
fun beforeLaunch(context: Context, game: Library.SteamGame) = queueGame(context, game, asked = false)
|
||||
internal fun prepareLaunch(context: Context, game: Library.SteamGame): Boolean {
|
||||
val key = appKey(game) ?: return false
|
||||
val findings = findings(context, game)
|
||||
val cached = catalogOf(context, false)
|
||||
val current = if (online(context)) catalogOf(context, true) else null
|
||||
val catalog = (current ?: cached)?.associateBy { it.name } ?: run {
|
||||
val missing = WinComponents.selection(context, key).effective
|
||||
.filter { WinComponents.validId(it) && it !in installedOf(context) }
|
||||
val pendingSaved = missing.isEmpty() || Pending.update(context, key, emptyMap()) { ids -> (ids + missing).distinct() }
|
||||
val pending = Pending.ids(context, key)
|
||||
pending.forEach { waitFor(game, it) }
|
||||
queuePending(context, key)
|
||||
if (!pendingSaved) {
|
||||
missing.forEach { id ->
|
||||
waitFor(game, id)
|
||||
queueDownload(context, id, asked = true, inSession = true, revision = Pending.revision(context, id))
|
||||
repairFailures.value = repairFailures.value + (id to "Could not save pending repairs")
|
||||
}
|
||||
}
|
||||
return pendingSaved && pending.isEmpty()
|
||||
}
|
||||
val wanted = autoPicks(findings, catalog)
|
||||
val selection = WinComponents.selection(context, key)
|
||||
val user = selection.user
|
||||
val installed = installedOf(context)
|
||||
val roots = selection.effective + wanted.keys.filter { user[it] != false }
|
||||
val required = withParts(roots.filter { WinComponents.validId(it) }, catalog)
|
||||
val missing = required.filter { user[it] != false && it !in installed }
|
||||
val pendingSaved = Pending.update(context, key, catalog.mapValues { it.value.revision }) { ids ->
|
||||
ids.filter { id -> user[id] != false && (id !in installed && id !in wanted || (Pending.revision(context, id) ?: 0) > (catalog[id]?.revision ?: 0)) } + missing
|
||||
}
|
||||
missing.forEach { id ->
|
||||
waitFor(game, id)
|
||||
queueDownload(context, id, asked = true, inSession = true,
|
||||
revision = maxOf(catalog[id]?.revision ?: 0, Pending.revision(context, id) ?: 0))
|
||||
}
|
||||
Pending.ids(context, key).forEach { waitFor(game, it) }
|
||||
queuePending(context, key)
|
||||
if (!pendingSaved && missing.isNotEmpty()) {
|
||||
missing.forEach { id -> repairFailures.value = repairFailures.value + (id to "Could not save pending repairs") }
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun beforeLaunch(context: Context, game: Library.SteamGame, onBlocked: () -> Unit = {}, onReady: () -> Unit) {
|
||||
val app = context.applicationContext
|
||||
Thread({
|
||||
val ready = runCatching { prepareLaunch(app, game) }.getOrElse {
|
||||
Log.w(TAG, "${game.name}: launch repair discovery failed: ${it.javaClass.simpleName}: ${it.message}")
|
||||
false
|
||||
}
|
||||
Handler(Looper.getMainLooper()).post(if (ready) onReady else onBlocked)
|
||||
queueGame(app, game, asked = false)
|
||||
}, "auto-components-launch").apply { isDaemon = true; start() }
|
||||
}
|
||||
|
||||
fun restoreMissing(context: Context): List<String> {
|
||||
val selections = WinComponents.selections(context)
|
||||
if (selections.isEmpty()) return emptyList()
|
||||
val catalog = (catalogOf(context, false) ?: catalogOf(context, true))?.associateBy { it.name } ?: return emptyList()
|
||||
val catalog = (catalogOf(context, false) ?: catalogOf(context, true))?.associateBy { it.name }
|
||||
val installed = installedOf(context)
|
||||
val lostFor = selections.mapValues { (_, selection) ->
|
||||
val lostFor = if (catalog == null) emptyMap() else selections.mapValues { (_, selection) ->
|
||||
withParts(selection.effective.filter { WinComponents.validId(it) }, catalog).filter { it !in installed && it in catalog }
|
||||
}.filterValues { it.isNotEmpty() }
|
||||
if (lostFor.isEmpty()) return emptyList()
|
||||
val lost = lostFor.values.flatten().distinct()
|
||||
Log.i(TAG, "wincomp: reinstalling ${lost.joinToString()}, picked for games but missing from the runtime")
|
||||
lostFor.forEach { (key, ids) -> Pending.add(context, key, ids) }
|
||||
lost.forEach { queueDownload(context, it, asked = true, inSession = true) }
|
||||
if (lost.isNotEmpty()) {
|
||||
Log.i(TAG, "wincomp: reinstalling ${lost.joinToString()}, picked for games but missing from the runtime")
|
||||
lostFor.forEach { (key, ids) -> Pending.add(context, key, ids, catalog!!.mapValues { it.value.revision }) }
|
||||
}
|
||||
Pending.read(context).keys().asSequence().toList().forEach { queuePending(context, it) }
|
||||
return lost
|
||||
}
|
||||
|
||||
@@ -360,6 +516,7 @@ object AutoComponents {
|
||||
fun refreshLater(context: Context, game: Library.SteamGame) = queueGame(context, game, asked = true)
|
||||
|
||||
fun sweep(context: Context, games: List<Library.SteamGame>) {
|
||||
if (online(context) && !unfavourable(context)) catalogOf(context, true)
|
||||
val apps = games.mapNotNull { steamAppOf(context, it) }.distinct()
|
||||
if (apps.isNotEmpty() && SteamAppInfo.needLookup(context, apps).isNotEmpty()) enqueue(context, Job(null, null, false, seq.incrementAndGet(), pics = apps))
|
||||
games.forEach { queueGame(context, it, asked = false) }
|
||||
|
||||
@@ -54,21 +54,6 @@ object DependencyDetector {
|
||||
"d3dx9", "d3dx11", "xact", "xact_x64", "xna31", "xna40", "physx", "oalinst", "XLiveRedist",
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun detect(gameDir: File): List<Recommendation> {
|
||||
if (!gameDir.isDirectory) return emptyList()
|
||||
val hits = LinkedHashMap<String, Recommendation>()
|
||||
|
||||
@@ -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.SteamLibraries
|
||||
import com.droiddeck.launcher.session.SecondaryLibrary
|
||||
import java.io.File
|
||||
|
||||
@@ -22,6 +23,7 @@ object Library {
|
||||
SteamGame(appId, name, art, library, gameId, hero, lastPlayed, gameFiles, protonPrefix, icon, source, storeId, logo)
|
||||
|
||||
val gameIdString: String get() = java.lang.Long.toUnsignedString(gameId)
|
||||
val launchKey: String get() = if (storeId != null) "$source:$storeId" else gameIdString
|
||||
val profileKey: String get() = when {
|
||||
source == com.droiddeck.launcher.stores.Store.BATTLENET.id -> com.droiddeck.launcher.runtime.BattleNet.APP_ID.toString()
|
||||
library == ADDED -> java.lang.Integer.toUnsignedString(appId)
|
||||
@@ -87,13 +89,14 @@ object Library {
|
||||
|
||||
internal fun steamLibraries(context: Context, arch: String = SessionPrefs.activeSteamClientArch(context)): List<Pair<File, String>> {
|
||||
val root = SessionPrefs.steamRoot(context, arch)
|
||||
// The session gives the x86-64 client only its card folder; the games folders stay the arm64 client's.
|
||||
val card = GameStorage.effective(context)
|
||||
if (arch == "x86_64") return listOfNotNull(root to "internal", card?.let { File(it.path, GameStorage.X64_CARD_DIR) to it.label })
|
||||
val added = SteamLibraries.list(context).filter { it.host.isDirectory }
|
||||
if (arch == "x86_64") return listOfNotNull(root to "internal", card?.let { File(it.path, GameStorage.X64_CARD_DIR) to it.label }) +
|
||||
added.map { it.x64Host to it.label }
|
||||
return listOfNotNull(
|
||||
root to "internal",
|
||||
card?.let { File(it.path) to it.label },
|
||||
) + GameStorage.gamesFolderLibraries(context).map { it.host to it.label }
|
||||
) + added.map { it.host to it.label } + GameStorage.gamesFolderLibraries(context).map { it.host to it.label }
|
||||
}
|
||||
|
||||
fun protonPrefix(
|
||||
@@ -101,15 +104,21 @@ object Library {
|
||||
libraries: List<File> = steamLibraries(context).map { it.first },
|
||||
): File? {
|
||||
val ids = listOf(appId.toString(), java.lang.Integer.toString(appId.toInt())).distinct()
|
||||
return prefixRoots(context, libraries, preferredLibrary).asSequence()
|
||||
.flatMap { root -> ids.asSequence().map { id -> File(root, "steamapps/compatdata/$id/pfx") } }
|
||||
.firstOrNull { it.isDirectory }
|
||||
}
|
||||
|
||||
fun compatdataDirs(context: Context, libraries: List<File> = steamLibraries(context).map { it.first }): List<File> =
|
||||
prefixRoots(context, libraries).map { File(it, "steamapps/compatdata") }.filter { it.isDirectory }
|
||||
|
||||
private fun prefixRoots(context: Context, libraries: List<File>, preferredLibrary: File? = null): List<File> {
|
||||
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)
|
||||
return (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(
|
||||
@@ -172,7 +181,6 @@ object Library {
|
||||
}
|
||||
}
|
||||
|
||||
/** Apps the user hid (the "hidden" collection) or marked private in Steam. */
|
||||
internal fun concealedSteamApps(root: File): Set<Long> {
|
||||
val config = AddedGames.accountConfig(root) ?: return emptySet()
|
||||
val hidden = runCatching {
|
||||
@@ -213,7 +221,6 @@ object Library {
|
||||
return (steam + own.map { g -> addedGame(context, g, roots, played[g.appId] ?: 0L) }).distinctBy { it.gameId }
|
||||
}
|
||||
|
||||
/** Steam's LastPlayTime for each non-Steam shortcut, by its unsigned shortcut app id. */
|
||||
internal fun shortcutsLastPlayed(root: File): Map<Long, Long> = runCatching {
|
||||
val config = AddedGames.accountConfig(root) ?: return emptyMap()
|
||||
val bytes = File(config, "shortcuts.vdf").takeIf { it.isFile }?.readBytes() ?: return emptyMap()
|
||||
|
||||
@@ -10,7 +10,6 @@ object LibraryCache {
|
||||
private const val TAG = "LibraryCache"
|
||||
private fun file(context: Context) = File(context.filesDir, "library-cache.json")
|
||||
|
||||
/** The cached games and whether they are the x86-64 client's; a cache written as a bare list is arm64's. */
|
||||
@Synchronized
|
||||
fun snapshot(context: Context): Pair<List<Library.SteamGame>, Boolean> = runCatching {
|
||||
val f = file(context)
|
||||
@@ -36,7 +35,6 @@ object LibraryCache {
|
||||
|
||||
fun listedX64(context: Context): Boolean = snapshot(context).second
|
||||
|
||||
/** Without [x64] the list keeps the client already recorded. */
|
||||
@Synchronized
|
||||
fun save(context: Context, games: List<Library.SteamGame>, x64: Boolean? = null) {
|
||||
runCatching {
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.droiddeck.launcher.frontend
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
/** Where Steam pins a game's logo on its hero image and how big it draws it; the same JSON Steam keeps in `<appid>.json`. */
|
||||
object LogoPosition {
|
||||
enum class Pin(val steam: String) {
|
||||
UPPER_LEFT("UpperLeft"), UPPER_CENTER("UpperCenter"), CENTER("CenterCenter"), BOTTOM_LEFT("BottomLeft"), BOTTOM_CENTER("BottomCenter");
|
||||
@@ -17,7 +16,6 @@ object LogoPosition {
|
||||
const val MIN_PCT = 10
|
||||
const val MAX_PCT = 100
|
||||
|
||||
/** What Steam draws when a game has no custom position. */
|
||||
val DEFAULT = Value(Pin.BOTTOM_LEFT, 50, 50)
|
||||
|
||||
private const val VERSION = 1
|
||||
@@ -25,7 +23,6 @@ object LogoPosition {
|
||||
|
||||
fun sized(pin: Pin, size: Int): Value = size.coerceIn(MIN_PCT, MAX_PCT).let { Value(pin, it, it) }
|
||||
|
||||
/** `{"nVersion":1,"logoPosition":{...}}`, with whatever else [into] already held kept as it was. */
|
||||
fun toJson(value: Value, into: JSONObject = JSONObject()): JSONObject = into
|
||||
.put("nVersion", VERSION)
|
||||
.put(KEY, JSONObject().put("pinnedPosition", value.pin.steam).put("nWidthPct", value.widthPct).put("nHeightPct", value.heightPct))
|
||||
@@ -39,7 +36,6 @@ object LogoPosition {
|
||||
return Value(pin, inner.optInt("nWidthPct").coerceIn(MIN_PCT, MAX_PCT), inner.optInt("nHeightPct").coerceIn(MIN_PCT, MAX_PCT))
|
||||
}
|
||||
|
||||
/** [existing] without the logo position; null when nothing but the version number would be left. */
|
||||
fun without(existing: JSONObject): JSONObject? {
|
||||
existing.remove(KEY)
|
||||
val rest = existing.keys().asSequence().filter { it != "nVersion" }.toList()
|
||||
|
||||
@@ -7,11 +7,9 @@ import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
/** Steam keeps a game's custom art in `userdata/<account>/config/grid`; this is where DroidDeck's choices and Steam's meet. */
|
||||
object SteamGrid {
|
||||
private const val TAG = "SteamGrid"
|
||||
|
||||
/** Every change to a game's art, here or in a scan, takes this first so two of them never interleave. */
|
||||
internal val lock = Any()
|
||||
private const val MANIFEST = ".droiddeck-art.json"
|
||||
private val SUFFIX = mapOf(AddedGameArt.Slot.COVER to "p", AddedGameArt.Slot.WIDE to "", AddedGameArt.Slot.BACKGROUND to "_hero", AddedGameArt.Slot.LOGO to "_logo")
|
||||
@@ -36,7 +34,6 @@ object SteamGrid {
|
||||
|
||||
private fun signature(file: File?) = if (file == null) "" else "${file.length()}:${file.lastModified()}"
|
||||
|
||||
/** Each Steam client has its own grid folder, so what was last seen there is kept per client. */
|
||||
private fun sigName(context: Context, key: String) =
|
||||
if (SessionPrefs.activeSteamClientArch(context) == "x86_64") "grid-x86_64-$key" else "grid-$key"
|
||||
|
||||
@@ -45,10 +42,6 @@ object SteamGrid {
|
||||
private const val GRID_ID = ".droiddeck-grid"
|
||||
private const val UNKNOWN_GRID = "?"
|
||||
|
||||
/**
|
||||
* A random id kept in the grid folder, so a record made against another account's folder, or one wiped with the
|
||||
* runtime, reads as "not seen yet" instead of "removed in Steam". "?" never matches, so nothing is dropped.
|
||||
*/
|
||||
private fun gridId(grid: File): String = runCatching {
|
||||
val f = File(grid, GRID_ID)
|
||||
f.takeIf { it.isFile }?.readText()?.trim()?.takeIf { it.isNotEmpty() } ?: UUID.randomUUID().toString().also { id ->
|
||||
@@ -83,7 +76,6 @@ object SteamGrid {
|
||||
manifest.keys().asSequence().filter { name -> File(grid, name).let { it.isFile && it.length() == manifest.optLong(name, -1L) } }.toSet()
|
||||
}.getOrDefault(emptySet())
|
||||
|
||||
/** Puts a choice (or, with null, the lack of one) where Steam reads it, whether or not Steam is running. */
|
||||
fun write(context: Context, target: Target, slot: AddedGameArt.Slot, file: File?): Boolean = synchronized(lock) { place(context, target, slot, file) }
|
||||
|
||||
private fun place(context: Context, target: Target, slot: AddedGameArt.Slot, file: File?): Boolean {
|
||||
@@ -116,7 +108,6 @@ object SteamGrid {
|
||||
|
||||
private fun positionIn(file: File): LogoPosition.Value? = file.takeIf { it.isFile }?.let { runCatching { it.readText() }.getOrNull() }?.let(LogoPosition::parse)
|
||||
|
||||
/** The logo position chosen here (or taken over from Steam) for a game; null when Steam's own default applies. */
|
||||
fun position(target: Target): LogoPosition.Value? = positionIn(File(target.artDir, POSITION))
|
||||
|
||||
private fun positionRecorded(context: Context, grid: File, target: Target): String? =
|
||||
@@ -132,12 +123,10 @@ object SteamGrid {
|
||||
return runCatching { file.writeText(LogoPosition.toJson(value).toString()) }.isSuccess
|
||||
}
|
||||
|
||||
/** Sets (or with null, clears) a game's logo position here and in the client's grid folder, whether or not Steam is running. */
|
||||
fun writePosition(context: Context, target: Target, value: LogoPosition.Value?): Boolean = synchronized(lock) {
|
||||
placePosition(context, target, value) && storePosition(target, value)
|
||||
}
|
||||
|
||||
/** Steam keeps the position in `<id>.json`; anything else that file holds is left as it was. */
|
||||
private fun placePosition(context: Context, target: Target, value: LogoPosition.Value?): Boolean {
|
||||
val grid = dir(context) ?: return true
|
||||
val file = positionFile(grid, target.id)
|
||||
@@ -160,7 +149,6 @@ object SteamGrid {
|
||||
return true
|
||||
}
|
||||
|
||||
/** A `logoPosition` this version cannot read (a pin it does not know, say) is left alone rather than read as "removed". */
|
||||
private fun hasUnknownPosition(file: File): Boolean {
|
||||
val json = runCatching { JSONObject(file.readText()) }.getOrNull() ?: return false
|
||||
return json.has("logoPosition") && LogoPosition.parse(json) == null
|
||||
@@ -176,7 +164,6 @@ object SteamGrid {
|
||||
when {
|
||||
before == null && own != null && theirs == null -> placePosition(context, target, own)
|
||||
before == null || now != before -> {
|
||||
// Recorded only once the local side agrees, so a failed take-over is tried again on the next scan.
|
||||
if (theirs != null) {
|
||||
if (own == theirs || storePosition(target, theirs)) recordPosition(context, grid, target, now)
|
||||
} else {
|
||||
@@ -187,7 +174,6 @@ object SteamGrid {
|
||||
}
|
||||
}
|
||||
|
||||
/** Takes over what changed in Steam since the last look; returns true when a choice was added, replaced or dropped. */
|
||||
fun sync(context: Context, targets: List<Target>): Boolean = synchronized(lock) { reconcile(context, targets) }
|
||||
|
||||
private fun reconcile(context: Context, targets: List<Target>): Boolean {
|
||||
|
||||
@@ -70,15 +70,22 @@ object SteamGridDb {
|
||||
if (!tmp.renameTo(f)) { f.delete(); tmp.renameTo(f) }
|
||||
}
|
||||
|
||||
fun gameId(key: String, steamAppId: Int?, name: String): Int? {
|
||||
fun gameId(key: String, steamAppId: Int?, name: String): Int? = gameId(key, steamAppId, name, ::match)
|
||||
|
||||
fun exactGameId(key: String, steamAppId: Int?, name: String): Int? = gameId(key, steamAppId, name, ::exactMatch)
|
||||
|
||||
private fun gameId(key: String, steamAppId: Int?, name: String, pick: (String, List<Pair<Int, String>>) -> Int?): Int? {
|
||||
if (steamAppId != null) {
|
||||
json(key, "$API/games/steam/$steamAppId")?.optJSONObject("data")?.optInt("id")?.takeIf { it > 0 }?.let { return it }
|
||||
}
|
||||
val term = URLEncoder.encode(name, "UTF-8").replace("+", "%20")
|
||||
val results = json(key, "$API/search/autocomplete/$term")?.optJSONArray("data") ?: return null
|
||||
return match(name, (0 until results.length()).map { results.getJSONObject(it) }.map { it.optInt("id") to it.optString("name") })
|
||||
return pick(name, (0 until results.length()).map { results.getJSONObject(it) }.map { it.optInt("id") to it.optString("name") })
|
||||
}
|
||||
|
||||
internal fun exactMatch(name: String, results: List<Pair<Int, String>>): Int? =
|
||||
results.firstOrNull { (id, got) -> id > 0 && AddedGameArt.nameMatch(name, got) == 3 }?.first
|
||||
|
||||
internal fun match(name: String, results: List<Pair<Int, String>>): Int? =
|
||||
results.map { (id, got) -> id to AddedGameArt.nameMatch(name, got) }
|
||||
.filter { it.second > 0 && it.first > 0 }
|
||||
|
||||
Loaded 100 of 1090 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user