mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-09 01:59:35 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d51395ec7 |
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 @@
|
||||
ASEbrahim
|
||||
# GitHub PR openers trusted for automatic release-signed test builds.
|
||||
FX192
|
||||
K11MCH1
|
||||
kingokksa
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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:
|
||||
@@ -28,6 +31,7 @@ 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,11 +2,8 @@ name: Build gamescope (patched)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'tools/gamescope/build-in-arch.sh'
|
||||
- 'tools/gamescope/patches/**'
|
||||
- 'tools/gamescope/runtime-sonames.txt'
|
||||
- 'tools/gamescope/**'
|
||||
- '.github/workflows/build-gamescope.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -18,10 +15,6 @@ 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
|
||||
@@ -39,25 +32,11 @@ 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: ${{ steps.components-token.outputs.token }}
|
||||
GH_REPO: Droid-Deck/DroidDeck-Components
|
||||
TAG: ${{ inputs.tag }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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
|
||||
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
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
+7
-106
@@ -63,8 +63,6 @@ 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
|
||||
@@ -102,7 +100,6 @@ 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:
|
||||
@@ -136,18 +133,11 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
components-runner:
|
||||
needs: source
|
||||
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 }}
|
||||
|
||||
components:
|
||||
name: Prepare verified components
|
||||
needs: [source, components-runner]
|
||||
needs: source
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ${{ fromJSON(needs.components-runner.outputs.labels) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -168,7 +158,6 @@ 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:
|
||||
@@ -176,8 +165,6 @@ 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
|
||||
@@ -241,7 +228,6 @@ 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
|
||||
@@ -252,9 +238,6 @@ 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:
|
||||
@@ -282,37 +265,15 @@ 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
|
||||
@@ -325,18 +286,11 @@ 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, build-runner]
|
||||
needs: [source, components, xbox]
|
||||
name: Compile APK and Android tests
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ${{ fromJSON(needs.build-runner.outputs.labels) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
variants: ${{ steps.variants.outputs.list }}
|
||||
@@ -350,7 +304,6 @@ jobs:
|
||||
!/docs/releases/media/
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Check human authorship
|
||||
env:
|
||||
@@ -411,7 +364,6 @@ 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"
|
||||
@@ -562,33 +514,7 @@ 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: |
|
||||
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
|
||||
run: bash tools/packs/build.sh app/build/packs
|
||||
|
||||
- name: Note when the proot pack pin is behind this build
|
||||
run: |
|
||||
@@ -600,21 +526,6 @@ 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:
|
||||
@@ -622,7 +533,6 @@ 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
|
||||
@@ -756,7 +666,6 @@ 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:
|
||||
@@ -785,18 +694,11 @@ jobs:
|
||||
cd app/src/main/rust/stores
|
||||
cargo test --locked 2>&1 | tail -60
|
||||
|
||||
python-runner:
|
||||
needs: source
|
||||
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 }}
|
||||
|
||||
python-tests:
|
||||
name: Python and native regression tests
|
||||
needs: [source, python-runner]
|
||||
needs: source
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ${{ fromJSON(needs.python-runner.outputs.labels) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -817,7 +719,6 @@ 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:
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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,18 +47,10 @@ 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 grep -qxF "$tag" <<<"$existing"; then
|
||||
retry gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
|
||||
if gh release view "$tag" -R Droid-Deck/DroidDeck-CI >/dev/null 2>&1; then
|
||||
gh release delete "$tag" -R Droid-Deck/DroidDeck-CI --yes --cleanup-tag
|
||||
echo "Deleted $tag"
|
||||
found=1
|
||||
fi
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
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,5 +1,11 @@
|
||||
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:
|
||||
@@ -55,7 +61,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 powervr; do
|
||||
for name in proot directaudio tracking xbox comet butler; 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,8 +13,6 @@ 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/
|
||||
@@ -34,5 +32,4 @@ __pycache__/
|
||||
|
||||
.signing.env
|
||||
app/src/main/assets/directaudio/
|
||||
app/src/main/assets/hybris/
|
||||
/tools/vr/out/
|
||||
@@ -1,6 +0,0 @@
|
||||
[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
|
||||
@@ -1,4 +0,0 @@
|
||||
# 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,157 +5,82 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
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!
|
||||
DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big Picture on your Adreno handheld, with Windows games through Valve's ARM64 Proton.
|
||||
|
||||
<p align="center"><b>100% open source. No ads. No telemetry. No source withheld.</b></p>
|
||||
<p align="center"><a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
<a href="https://www.star-history.com/droid-deck/droiddeck"><img src="https://api.star-history.com/badge?repo=Droid-Deck/DroidDeck&type=rank&theme=dark" alt="Star History Rank" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.star-history.com/droid-deck/droiddeck"><img src="https://api.star-history.com/badge?repo=Droid-Deck/DroidDeck&type=rank&theme=dark" alt="Star History Rank"></a>
|
||||
<a href="https://trendshift.io/repositories/267852?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-267852" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/267852/daily?language=C" alt="Droid-Deck%2FDroidDeck | Trendshift" width="250" height="55"/></a>
|
||||
<a href="https://trendshift.io/repositories/267852?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-267852" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/267852/daily?language=Kotlin" alt="Droid-Deck%2FDroidDeck | Trendshift" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/267852"><img src="https://trendshift.io/api/badge/trendshift/repositories/267852/daily?language=C" alt="Droid-Deck%2FDroidDeck | Trendshift" width="250" height="55"></a>
|
||||
<a href="https://trendshift.io/repositories/267852"><img src="https://trendshift.io/api/badge/trendshift/repositories/267852/daily?language=Kotlin" alt="Droid-Deck%2FDroidDeck | Trendshift" width="250" height="55"></a>
|
||||
<a href="https://trendshift.io/repositories/267852?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-267852" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/267852/weekly?language=C" alt="Droid-Deck%2FDroidDeck | Trendshift" width="250" height="55"/></a>
|
||||
<a href="https://trendshift.io/repositories/267852?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-267852" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/267852/weekly?language=Kotlin" alt="Droid-Deck%2FDroidDeck | Trendshift" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><img src="docs/media/steam-to-desktop.gif" width="100%" alt="On a Retroid Pocket Duo: Play Steam from the DroidDeck launcher, open Pac-Man in Steam Big Picture and play it, exit the game, then Switch to Desktop from the Power menu"></p>
|
||||
<p align="center"><img src="docs/releases/media/0.2.0/launch-into-steam.gif" width="80%" alt="Tapping DroidDeck on the Android home screen and landing in Steam Big Picture"></p>
|
||||
|
||||
> DroidDeck has no website. Only download it from this repository's [Releases](https://github.com/Droid-Deck/DroidDeck/releases).
|
||||
> Note: DroidDeck does not have a stand-alone website. Do not click on any download links from websites claiming to be the DroidDeck team.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/releases/media/0.6.7/library.gif" width="49%" alt="The Library switching source chips, changing the card size and opening the Filter menu">
|
||||
<img src="docs/releases/media/0.3.0/in-game.gif" width="49%" alt="A Windows game running through Proton with the on-screen controls">
|
||||
<img src="docs/releases/media/0.4.0/multi-controller.gif" width="49%" alt="Two controllers playing split screen on one handheld">
|
||||
<img src="docs/releases/media/0.6.7/ubuntu-desktop-discover.jpg" width="49%" alt="The Kubuntu desktop with Discover open">
|
||||
</p>
|
||||
## Requirements and install
|
||||
|
||||
## Install
|
||||
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts. **Stores** (Setup › Stores › "Show Stores in the rail") signs in to GOG, Epic Games and Amazon Games and downloads games into the Games storage. Epic keeps its direct API install path. Opening Epic Stores prepares Epic’s official Linux launcher preview. When installed, Epic games launched from DroidDeck use that client, and games installed in the client are imported into DroidDeck’s library and shortcuts. The x86-64 client runs through FEX on ARM and DroidDeck retains its Proton hooks; this preview and ARM setup are outside Epic’s listed Linux support. The cog on a store’s chip row sets its opening tab and download speed tier.
|
||||
|
||||
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.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.
|
||||
On Android 14 and newer, before Steam launches, you must turn on **Disable child process restrictions** in Developer options. If this option is not available in developer settings (Android 12 and 13 devices), first launch of Steam will present a "Fix it for me" button, which will help automate the setup process.
|
||||
|
||||
Smart glasses setup is in the [support matrix](docs/smart-glasses-support.md). Release notes are in [docs/releases](docs/releases).
|
||||
## Physical button shortcuts
|
||||
|
||||
Help, Preview builds and device reports are on [Discord](https://discord.gg/JRGAvawjsm). For bugs, post in **#bug-reports** with the zip from **Share logs**.
|
||||
Open the launcher's **Controls** settings, **Setup → Controller → Button mapping**, or the session drawer's **Controller** page. Each menu action has one row showing **Custom** or **Default**. Select the current value beside **Steam menu**, **Open Quick Access Menu**, or **DroidDeck menu**, then press and release a physical button. The assignment saves automatically on release; confirmation is only required if the button already opens another menu. Assignments are stored per controller; each action has one assigned button per controller. A mapped button performs the shortcut instead of sending its normal input to the game. Steam shortcuts only apply in Steam sessions, and shortcuts are inactive while loading, suspended, or using the on-screen keyboard or other input dialogs.
|
||||
|
||||
Unassigned buttons keep their existing behavior, including the Back button's single/double-press setting and Select-to-Steam option. While the session drawer is open, A selects the focused item even if it has a shortcut assignment. Select an action's current value and use **Reset** beside an assignment to restore that behavior without changing touch layouts or other controller settings. **Close** exits capture without changing an assignment; it does not undo an assignment already applied. The touchscreen remains available for configuration and recovery.
|
||||
|
||||
Extra buttons such as M1/M2 work only if Android exposes distinct key events to the app. Android Home, volume and other reserved system keys cannot be assigned; making DroidDeck the default launcher retains its existing Home-to-Steam behavior. Firmware that intercepts a button or makes it duplicate another button cannot be overridden here. During capture, the detected device and button name are shown. If a controller disconnects before release, use **Try another button** to restart capture; **Reset** remains available for existing assignments. Capture and shortcut-action diagnostics, including key and scan codes, are included in session logs when capture runs inside a session. If a button is not detected, record its physical label, firmware/controller mode, and whether it leaves the app, then share the session logs.
|
||||
|
||||
## Smart glasses
|
||||
|
||||
Use **Setup → Smart glasses** for app-wide virtual-screen settings across DroidDeck menus, launchers, desktop sessions, and games. Supported VITURE, RayNeo, XREAL, and Rokid connections provide 3DoF; Luma Ultra adds optional experimental 6DoF and stereo-assisted hand/Joy-Con tracking. Model support and SDK requirements differ: see the [smart-glasses support matrix](docs/smart-glasses-support.md) for supported devices, controls, setup, and limitations.
|
||||
|
||||
## Community
|
||||
|
||||
Join the [DroidDeck Discord](https://discord.gg/JRGAvawjsm) for help, Preview builds, and device reports. Bug reports go in its **#bug-reports** forum; attach the zip from **Share logs** (Setup, or the session drawer) so the logs come with it.
|
||||
|
||||
## Build
|
||||
|
||||
Needs Docker, Java 17, the Android SDK and NDK, Rust via rustup, GitHub CLI, Python 3 and `zstd`.
|
||||
Run `tools/build_local.sh` with Docker, Java 17, the Android SDK/NDK, Rust through rustup, GitHub CLI, Python 3, and `zstd` installed. It prepares the same native libraries, pinned components and audio sinks as CI, then checks the APK's assets, JNI exports and dependencies. The APK is at `app/build/outputs/apk/release/app-release.apk`. Set `DROIDDECK_PA13_SOURCE_DIR` to an existing PulseAudio 13.0 source directory to skip downloading it. Set `DROIDDECK_SIGNING_ENV` to a signing environment file to install over a release build; relative keystore paths resolve beside that file. To install on an attached device, run `tools/deploy_local.sh`.
|
||||
|
||||
```bash
|
||||
tools/build_local.sh
|
||||
```
|
||||
After preparation, ordinary `./gradlew :app:assembleRelease` builds can reuse the verified inputs. If native sources, component pins or staged files change, packaging stops with instructions to rerun the helper. Generated audio stays under `app/build/`; the tracked base bundle is never modified. `./gradlew -PskipRust=true :app:testDebugUnitTest` needs no native preparation. For an APK without store engines, use `DROIDDECK_SKIP_RUST=1 tools/build_local.sh`; it omits cached engine libraries too.
|
||||
|
||||
The APK lands in `app/build/outputs/apk/release/`. Install it with `tools/deploy_local.sh`. After the first run, `./gradlew :app:assembleRelease` reuses the prepared inputs. Unit tests run with `./gradlew -PskipRust=true :app:testDebugUnitTest`, and the helper and preload suite with `bash tools/test_local.sh`.
|
||||
For the prepared Windows checkout, run `./tools/run_windows.ps1` (optionally with `-Serial <adb-serial>`), or select **DroidDeck XR** in Android Studio and run the **app** module on the connected phone. Machine-specific options live in the ignored `local.properties`: `droiddeck.python` selects a Python executable, `droiddeck.skipRust=true` omits the optional store download engines, and `droiddeck.debugApplicationIdSuffix=.xr` installs **DroidDeck XR Dev** alongside the signed release. The development app has its own runtime and Steam login. The local Java selection is in `.gradle/config.properties`.
|
||||
|
||||
On Windows, see `tools/run_windows.ps1` and `tools/prepare_windows_runtime.py`.
|
||||
`python tools/prepare_windows_runtime.py` restores this checkout's audited runtime inputs from the pinned CI APK, three locally rebuilt preload libraries, and the checksum-pinned Gamescope archive in `build/windows-setup/`. Start Docker Desktop's Linux engine first: preparation now rebuilds DroidDeck's ARM64 VR driver and Vulkan layer from current source and runs their native regression tests. The compiler image is built automatically on first use. Vulkan headers come from the NDK beside `--llvm-strip`, or from an explicit `--vk-include <NDK-sysroot-include>` directory.
|
||||
|
||||
To reuse that build, pass `--rebuilt-vr build/windows-setup/vr`. Preparation verifies its `vr-build.json` against the current Git revision, source files, Vulkan headers, output checksums, and ARM64 architecture; missing or stale builds fail instead of falling back to the donor's VR libraries. The script checks the actual donor CI merge tree, still rejects unrelated unaudited runtime changes, and records component provenance in `app/build/windows-runtime-provenance.json`. Preserve these ignored preparation files when rebuilding locally; `clean` removes the prepared assets and their verification manifest.
|
||||
|
||||
Run the helper and preload regression suite with `bash tools/test_local.sh`. On macOS it runs in Linux through Docker, matching the helpers' runtime and CI instead of compiling Linux preloads against macOS headers.
|
||||
|
||||
## Limits
|
||||
|
||||
Compatibility and performance vary by device. Desktop compositing is software rendered, and Firefox sandboxing is reduced under proot.
|
||||
Compatibility and performance vary by device; hardware validation is limited. Desktop compositing uses software rendering. Firefox sandboxing is reduced under proot. Logs are kept app-private (`files/logs/`, the last 30 sessions); **Share logs** packs the newest session, with the stores and tool logs, into one scrubbed zip.
|
||||
|
||||
## Credits and licence
|
||||
|
||||
DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. VITURE, RayNeo, XREAL, Rokid, REDMAGIC, Nintendo Switch and Joy-Con are trademarks of their owners. This project is not affiliated with any of them.
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. Battle.net support is [@kayakyakr](https://github.com/kayakyakr)'s: it runs Blizzard's own Battle.net client, downloaded from Blizzard and not included, in Proton, and shows the key art from each game's [Battle.net Shop](https://shop.battle.net) page; without the Steam client its games see DroidDeck's pad as an Xbox 360 controller through `PROTON_SPOOF_STEAMINPUT_VIDPID` in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/winebus.sys`. Its games install from Stores through Blizzard's own Agent and the local HTTP API it serves, driven the way [@barncastle](https://github.com/barncastle)'s [Battle.Net-Installer](https://github.com/barncastle/Battle.Net-Installer) drives it, with the requests described on [wowdev.wiki](https://wowdev.wiki/Agent); how other launchers find, install and start Blizzard games was read in [@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, without taking their code. The registry entries a GOG game's install script (`goggame-<id>.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. x86 AppImages packed with [sharun](https://github.com/VHSgunzo/sharun) by VHSgunzo (MIT) start because DroidDeck's own [FEX](https://github.com/FEX-Emu/FEX) build (MIT) puts the platform name under `AT_PLATFORM`, which sharun's [userland-execve](https://crates.io/crates/userland-execve) loader (MIT) requires; FEX filed it under `AT_BASE_PLATFORM`. The Steam client's 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` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. The in-game performance HUD is [@The412Banner](https://github.com/The412Banner)'s [FusionHUD](https://github.com/The412Banner/FusionHUD) (GPL-3.0 with an attribution term), Bannerlator's HUD; its frame rate, frame times and readings are taken from the same sources, and worked out the same way, as Valve's [MangoHud](https://github.com/flightlessmango/MangoHud) build (MIT) that runs Steam's performance overlay in a DroidDeck session, with the frame times shared by gamescope patch 0125; when gamescope sends no frames for two seconds, the HUD counts the frames DroidDeck's compositor shows instead, which stop at the screen's refresh rate. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc/<pid>/mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and isLine truncated
|
||||
|
||||
<details>
|
||||
<summary><b>Runtime, Wine and Proton</b></summary>
|
||||
XR glasses support builds on WinNative's VITURE glasses work. VITURE protocol details were checked against [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) by [@wheaney](https://github.com/wheaney), including the VITURE protocol header it ships, [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) by [@badicsalex](https://github.com/badicsalex) and the [go-macos/iokit VITURE package](https://pkg.go.dev/github.com/go-macos/iokit/viture); the optional VITURE SDK belongs to VITURE and is not included. RayNeo support 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 support references [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) by [@rohitsangwan01](https://github.com/rohitsangwan01) (MIT) and [xr-tools](https://github.com/0xcaff/xr-tools) by [@0xcaff](https://github.com/0xcaff). Rokid support follows the Rokid protocol notes in [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) and @badicsalex's [Void Computing write-ups](https://voidcomputing.hu/blog/worse-better-prettier/), and [Monado](https://gitlab.freedesktop.org/monado/monado)'s Rokid driver. The REDMAGIC glasses-display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube). VITURE, RayNeo, XREAL, Rokid and REDMAGIC are trademarks of their owners; this project is not affiliated with them.
|
||||
|
||||
- Runtime, shim, input and controller work build on WinNative and Bannerlator by maxjivi05.
|
||||
- mmap ring buffer controller input by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)).
|
||||
- [FEX](https://github.com/FEX-Emu/FEX) (MIT) runs x86 code. DroidDeck's build puts the platform name under `AT_PLATFORM` so [sharun](https://github.com/VHSgunzo/sharun) AppImages by VHSgunzo (MIT) start through its [userland-execve](https://crates.io/crates/userland-execve) loader (MIT). FEX's code disk cache was checked in its source.
|
||||
- x86 AppImages, and arm64 ones whose runtime can't unpack on Android, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence.
|
||||
- x86 programs linking OpenSSL 3 use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt). x86 Linux games get the `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes.
|
||||
- `WriteProcessMemory` hooks fall back to `/proc/<pid>/mem` the way [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` did before Wine 11.
|
||||
- Wine write watches use userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later). libblsession supplies the arm64 syscall number and answers `PAGEMAP_SCAN` from `/proc/self/pagemap`.
|
||||
- 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.
|
||||
- The arm64 client's sign in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest).
|
||||
Joy-Con VR controllers port the arm model of Google's [Google VR SDK for Unity](https://github.com/googlevr/gvr-unity-sdk) (Apache-2.0), adapt the hand skeleton of Valve's [OpenVR](https://github.com/ValveSoftware/openvr) `handskeletonsimulation` sample (BSD-3-Clause), which follows Moshi Turner's hand simulation in [Monado](https://gitlab.freedesktop.org/monado/monado), and use the Index controller offsets from Monado's `vive_poses.c` (BSL-1.0). Joy-Con behaviour was researched in Linux's [hid-nintendo](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-nintendo.c) driver by Daniel J. Ogorchock, [@dekuNukem](https://github.com/dekuNukem)'s [Nintendo_Switch_Reverse_Engineering](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering), [SDL](https://github.com/libsdl-org/SDL) and the Android Open Source Project, and checked against [ALVR](https://github.com/alvr-org/ALVR), [OpenComposite](https://gitlab.com/znixian/OpenOVR), [PSMoveSteamVRBridge](https://github.com/HipsterSloth/PSMoveSteamVRBridge), [@F8LEFT](https://github.com/F8LEFT)'s [JoyConVr](https://github.com/F8LEFT/JoyConVr), [@krnelpan1c](https://github.com/krnelpan1c)'s [Daydream-Controller-SteamVR](https://github.com/krnelpan1c/Daydream-Controller-SteamVR), [@yoyobuae](https://github.com/yoyobuae)'s [joyconvr](https://github.com/yoyobuae/joyconvr), [Moonlight](https://github.com/moonlight-stream/moonlight-android) and [Dolphin](https://github.com/dolphin-emu/dolphin); see [docs/third-party/joycon](docs/third-party/joycon/README.md). Nintendo Switch and Joy-Con are trademarks of Nintendo; this project is not affiliated with Nintendo.
|
||||
|
||||
</details>
|
||||
Camera hand tracking runs Google's [MediaPipe](https://github.com/google-ai-edge/mediapipe) Tasks Vision library with its Pose Landmarker (Lite) model, BlazePose GHUM 3D, and its Hand Landmarker model (all Apache-2.0), on frames from AndroidX [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) (Apache-2.0); MediaPipe's usage reports are switched off. The design was checked against [@F8LEFT](https://github.com/F8LEFT)'s [JoyConVr](https://github.com/F8LEFT/JoyConVr) camera app (MIT), and the short Joy-Con movements follow the accelerometer mode of the [Google VR SDK for Unity](https://github.com/googlevr/gvr-unity-sdk)'s arm model, Eric Foxlin's zero-velocity updates and Frosio, Pedersini and Borghese's accelerometer autocalibration, and the camera and Joy-Con are joined by a Kalman filter (R. E. Kalman, 1960). Bare camera hands take their gestures after [ALVR](https://github.com/alvr-org/ALVR)'s `hand_gestures.rs` (MIT) and Meta Quest's menu pinch, with distances checked on photos from the [HaGRID](https://github.com/hukenovs/hagrid) gesture set (CC BY-SA 4.0, not included), fit the palm with Horn's quaternion method (1987) and smooth it with the 1€ filter (Casiez, Roussel and Vogel, 2012); the white glove model follows Valve's OpenVR render model reference and Index controller poses, is meshed with surface nets (Gibson, 1998; Mikola Lysenko) and blends and shades it with Inigo Quilez's smooth minimum and distance-field ambient occlusion, porting his polygon distance (MIT); see [docs/third-party/handtracking](docs/third-party/handtracking/README.md).
|
||||
|
||||
<details>
|
||||
<summary><b>Stores</b></summary>
|
||||
The Store installs and runs apps with [Flatpak](https://github.com/flatpak/flatpak) (LGPL-2.1). Its sandbox goes through DroidDeck's own bubblewrap shim, which follows how Flatpak hands bwrap its environment. Chromium and Electron apps from Flathub start their browser processes through [zypak](https://github.com/refi64/zypak) by [@refi64](https://github.com/refi64) (BSD-3-Clause); its source was read to see that it always spawns them through Flatpak's spawn portal, which is why DroidDeck starts them directly without Chromium's sandbox instead.
|
||||
|
||||
- Epic Games and GOG build on [Phobos](https://github.com/phobos665)'s adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl). GOG install script registry entries are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) (GPL-3.0) reads them.
|
||||
- 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).
|
||||
Remote Play from a PC to the arm64 Steam client goes through a stand-in for the V4L2 decoder and the libdrm dumb buffers that Steam's `streaming_client` expects: each frame is decoded by Android's MediaCodec through the app, or by the [FFmpeg](https://ffmpeg.org) (LGPL-2.1-or-later) that ships in Steam's own runtime, into dma-heap buffers that its Vulkan renderer imports. The Qualcomm low-latency decoder settings follow [Moonlight](https://github.com/moonlight-stream/moonlight-android) (GPL-3.0).
|
||||
|
||||
</details>
|
||||
Steam's Browse buttons open a GTK 3 file chooser through PyGObject (LGPL-2.1-or-later). It runs under Steam's own `reaper` with Steam's app ID, which [gamescope](https://github.com/ValveSoftware/gamescope) (BSD-2-Clause) reads from the process tree (`GetAppIdFromReaper` in `src/Utils/Process.cpp`) to show it over Steam.
|
||||
|
||||
<details>
|
||||
<summary><b>Audio, video and HUD</b></summary>
|
||||
|
||||
- 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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Desktop</b></summary>
|
||||
|
||||
- The KDE pacman repository snapshots [Arch Linux ARM](https://archlinuxarm.org) and installs with [pacman](https://gitlab.archlinux.org/pacman/pacman) (GPL-2.0-or-later).
|
||||
- [KWin](https://invent.kde.org/plasma/kwin) (GPL-2.0-or-later) is patched to pass on the scroll data its `wayland_backend.cpp` leaves as a TODO.
|
||||
- Read without taking code: [Qt](https://code.qt.io/cgit/qt/qtbase.git)'s `qwaylandinputdevice.cpp` and `qguiapplication.cpp`, [Konsole](https://invent.kde.org/utilities/konsole)'s `TerminalDisplay.cpp`, and [Mesa](https://gitlab.freedesktop.org/mesa/mesa)'s `u_threaded_context.c` and `zink_resource.c`.
|
||||
- The Steam Keyboard window rule follows Valve's `steamdeck-kde-presets` with [Bazzite](https://github.com/ublue-os/bazzite)'s `0001-steam-virtual-keyboard.patch`.
|
||||
- Steam's Browse buttons open a GTK 3 file chooser via PyGObject (LGPL-2.1-or-later), shown over Steam by [gamescope](https://github.com/ValveSoftware/gamescope) (BSD-2-Clause) through `GetAppIdFromReaper`.
|
||||
- The AYN Odin 2 phantom second display was confirmed by [@SapphireRhodonite](https://github.com/SapphireRhodonite)'s [WatermelonDS 0.0.5](https://github.com/SapphireRhodonite/WatermelonDS/releases/tag/0.0.5) notes and [iiSU #363](https://github.com/iisu-network/iiSU/issues/363), without taking their code.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<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.
|
||||
- REDMAGIC display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>VR, Joy-Con and hand tracking</b></summary>
|
||||
|
||||
- Joy-Con VR ports the arm model from Google's [GVR SDK for Unity](https://github.com/googlevr/gvr-unity-sdk) (Apache-2.0) and adapts the hand skeleton from Valve's [OpenVR](https://github.com/ValveSoftware/openvr) `handskeletonsimulation` sample (BSD-3-Clause), which follows Moshi Turner's work in Monado. Index offsets come from Monado's `vive_poses.c` (BSL-1.0).
|
||||
- Joy-Con research: Linux's [hid-nintendo](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-nintendo.c) by Daniel J. Ogorchock, [@dekuNukem](https://github.com/dekuNukem)'s [Nintendo_Switch_Reverse_Engineering](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering), [SDL](https://github.com/libsdl-org/SDL) and AOSP. Checked against [ALVR](https://github.com/alvr-org/ALVR), [OpenComposite](https://gitlab.com/znixian/OpenOVR), [PSMoveSteamVRBridge](https://github.com/HipsterSloth/PSMoveSteamVRBridge), [@F8LEFT](https://github.com/F8LEFT)'s [JoyConVr](https://github.com/F8LEFT/JoyConVr), [@krnelpan1c](https://github.com/krnelpan1c)'s [Daydream-Controller-SteamVR](https://github.com/krnelpan1c/Daydream-Controller-SteamVR), [@yoyobuae](https://github.com/yoyobuae)'s [joyconvr](https://github.com/yoyobuae/joyconvr), [Moonlight](https://github.com/moonlight-stream/moonlight-android) and [Dolphin](https://github.com/dolphin-emu/dolphin). See [docs/third-party/joycon](docs/third-party/joycon/README.md).
|
||||
- Camera hand tracking runs Google's [MediaPipe](https://github.com/google-ai-edge/mediapipe) Pose Landmarker (Lite), BlazePose GHUM 3D and Hand Landmarker (Apache-2.0) on [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) frames (Apache-2.0), with usage reports off. Checked against JoyConVr's camera app (MIT).
|
||||
- Tracking math: the GVR arm model's accelerometer mode, Eric Foxlin's zero velocity updates, Frosio, Pedersini and Borghese's accelerometer autocalibration, a Kalman filter (Kalman, 1960), Horn's quaternion method (1987) and the 1€ filter (Casiez, Roussel and Vogel, 2012).
|
||||
- Gestures follow [ALVR](https://github.com/alvr-org/ALVR)'s `hand_gestures.rs` (MIT) and Meta Quest's menu pinch, tuned on [HaGRID](https://github.com/hukenovs/hagrid) photos (CC BY-SA 4.0, not included).
|
||||
- The glove model follows OpenVR's render model reference, is meshed with surface nets (Gibson, 1998; Mikola Lysenko), and uses Inigo Quilez's smooth minimum, distance field ambient occlusion and polygon distance (MIT). See [docs/third-party/handtracking](docs/third-party/handtracking/README.md).
|
||||
|
||||
</details>
|
||||
|
||||
## Star history
|
||||
|
||||
<a href="https://www.star-history.com/#Droid-Deck/DroidDeck&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Droid-Deck/DroidDeck&type=Date&theme=dark">
|
||||
<img alt="Star history chart" src="https://api.star-history.com/svg?repos=Droid-Deck/DroidDeck&type=Date" width="100%">
|
||||
</picture>
|
||||
</a>
|
||||
Black Ops III and Dark Souls Remastered start without [umu-protonfixes](https://github.com/Open-Wine-Components/umu-protonfixes) (BSD-2-Clause), whose fixes for them (`311210.py` sets ALSA audio, `570940.py` installs VC++ 2017) fail under ARM64 Proton.
|
||||
+14
-37
@@ -4,6 +4,7 @@ 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()) {
|
||||
@@ -93,10 +94,12 @@ 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', 'powervr'].collectEntries { name ->
|
||||
def addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'butler'].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]]
|
||||
}
|
||||
@@ -131,8 +134,8 @@ android {
|
||||
applicationId "com.droiddeck.launcher"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 28
|
||||
versionCode 17
|
||||
versionName "0.6.9"
|
||||
versionCode 16
|
||||
versionName "0.6.7"
|
||||
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
|
||||
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
|
||||
buildConfigField "int", "PR_NUMBER", "${prNumber}"
|
||||
@@ -205,7 +208,8 @@ android {
|
||||
}
|
||||
|
||||
androidResources {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~:<dir>droiddeck-xbox:<dir>droiddeck-comet:<dir>directaudio:<dir>hybris'
|
||||
// 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'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
@@ -215,6 +219,7 @@ 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']
|
||||
}
|
||||
}
|
||||
@@ -233,6 +238,7 @@ 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
|
||||
@@ -249,11 +255,6 @@ 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'))
|
||||
@@ -299,21 +300,10 @@ 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
|
||||
def allowStale = (project.findProperty('allowStaleRuntime')
|
||||
?: localBuildOptions.getProperty('droiddeck.allowStaleRuntime', 'false')).toString().toBoolean()
|
||||
commandLine([pythonExecutable, 'tools/release/runtime_inputs.py', 'verify'] + (allowStale ? ['--allow-stale-sources'] : []))
|
||||
commandLine pythonExecutable, 'tools/release/runtime_inputs.py', 'verify'
|
||||
}
|
||||
tasks.configureEach { task ->
|
||||
if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs)
|
||||
@@ -351,27 +341,14 @@ 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'
|
||||
implementation 'com.github.luben:zstd-jni:1.5.7-4@aar'
|
||||
implementation 'org.apache.commons:commons-compress:1.27.1'
|
||||
implementation 'org.tukaani:xz:1.10'
|
||||
// dx (Apache-2.0) turns the Samsung performance SDK a user imports into dex on the phone (SamsungPerf).
|
||||
implementation 'com.jakewharton.android.repackaged:dalvik-dx:16.0.1'
|
||||
implementation platform('androidx.compose:compose-bom:2024.02.00')
|
||||
implementation 'androidx.compose.ui:ui'
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
@@ -387,11 +364,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,21 +3,12 @@
|
||||
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" />
|
||||
@@ -69,19 +60,6 @@
|
||||
<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"
|
||||
@@ -91,14 +69,6 @@
|
||||
</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>
|
||||
@@ -135,7 +105,6 @@
|
||||
android:exported="true"
|
||||
android:screenOrientation="user"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden|navigation">
|
||||
<layout android:defaultWidth="1024dp" android:defaultHeight="640dp" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -195,6 +164,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".PinRequestActivity"
|
||||
android:exported="true"
|
||||
@@ -207,13 +177,13 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".sync.SyncthingWebActivity"
|
||||
android:name=".input.ControllerEditorActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/SessionTheme"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
|
||||
android:theme="@style/AppTheme" />
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
|
||||
|
||||
<activity
|
||||
android:name=".files.FileManagerActivity"
|
||||
@@ -314,11 +284,6 @@
|
||||
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" />
|
||||
@@ -329,10 +294,6 @@
|
||||
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,6 +8,7 @@ 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")
|
||||
@@ -36,14 +37,13 @@ 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,7 +62,6 @@ 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);
|
||||
@@ -92,7 +91,6 @@ 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,6 +78,7 @@ 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;
|
||||
@@ -390,6 +391,7 @@ 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) {
|
||||
@@ -1241,6 +1243,7 @@ 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";
|
||||
@@ -2670,6 +2673,9 @@ __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;
|
||||
@@ -2953,19 +2959,15 @@ 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 = 0;
|
||||
mode_t mode;
|
||||
int fd;
|
||||
bool hasMode;
|
||||
|
||||
va_start(va, flags);
|
||||
|
||||
hasMode = open_has_mode(flags);
|
||||
hasMode = flags & O_CREAT;
|
||||
|
||||
if (hasMode) {
|
||||
mode = va_arg(va, mode_t);
|
||||
@@ -3037,13 +3039,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 = 0;
|
||||
mode_t mode;
|
||||
int fd;
|
||||
bool hasMode;
|
||||
|
||||
va_start(va, flags);
|
||||
|
||||
hasMode = open_has_mode(flags);
|
||||
hasMode = flags & O_CREAT;
|
||||
|
||||
if (hasMode) {
|
||||
mode = va_arg(va, mode_t);
|
||||
@@ -3677,6 +3679,7 @@ 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;
|
||||
@@ -4255,7 +4258,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 open_has_mode(flags) ? va_arg(va, mode_t) : 0;
|
||||
return (flags & (O_CREAT | O_TMPFILE)) ? va_arg(va, mode_t) : 0;
|
||||
}
|
||||
|
||||
EXPORT int open64(const char *pathname, int flags, ...) {
|
||||
|
||||
@@ -267,6 +267,8 @@ 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,4 +1,5 @@
|
||||
// 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,4 +1,5 @@
|
||||
// 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,4 +1,5 @@
|
||||
#pragma once
|
||||
// DXVK's dxbc subset is zlib licensed; vendored under cpp/thirdparty/dxbc.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
// 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
|
||||
@@ -1,77 +0,0 @@
|
||||
#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
|
||||
@@ -1,159 +0,0 @@
|
||||
// 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
|
||||
@@ -1,27 +0,0 @@
|
||||
#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,6 +76,7 @@ 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;
|
||||
@@ -87,6 +88,7 @@ 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,6 +3,10 @@
|
||||
|
||||
#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 };
|
||||
@@ -41,6 +45,7 @@ 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;
|
||||
constexpr int kRemoteTouchId = 31;
|
||||
constexpr int kTouchWidth = 1920, kTouchHeight = 1080; // InputSpace.kt
|
||||
constexpr int kRemoteTouchId = 31; // the compositor's last touch slot (MAX_TOUCH_POINTS 32)
|
||||
|
||||
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,18 +1,20 @@
|
||||
#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;
|
||||
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;
|
||||
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
|
||||
default: return -4;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +30,7 @@ constexpr int from_native_mode(int native) {
|
||||
case 0x36: return 0x44;
|
||||
case 0x3a: return 0x42;
|
||||
case 0x3b: return 0x45;
|
||||
default: return -4;
|
||||
default: return -4; // Ultrawide is not stereo.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
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,6 +30,7 @@
|
||||
#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{};
|
||||
@@ -63,6 +64,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -232,6 +234,8 @@ 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,7 +11,6 @@ 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
|
||||
@@ -19,11 +18,9 @@ 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
|
||||
@@ -46,7 +43,6 @@ 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)
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/* 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,
|
||||
};
|
||||
@@ -1,341 +0,0 @@
|
||||
/* 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
|
||||
@@ -1,139 +0,0 @@
|
||||
<?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,9 +79,6 @@ 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; }
|
||||
@@ -106,7 +103,6 @@ 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,6 +34,7 @@ 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);
|
||||
@@ -185,13 +186,6 @@ 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,7 +27,6 @@ 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,3 +1,4 @@
|
||||
// 1115.1.0
|
||||
#pragma once
|
||||
const uint32_t cardboard_frag_code[] = {
|
||||
0x07230203,0x00010000,0x0008000b,0x000001ba,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
|
||||
@@ -154,16 +154,6 @@ 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));
|
||||
@@ -299,6 +289,7 @@ 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;
|
||||
@@ -359,62 +350,6 @@ 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);
|
||||
@@ -438,8 +373,7 @@ 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 (!buffer_release_blocked(pr->buffer) && (!earliest || pr->at_ns < earliest))
|
||||
earliest = pr->at_ns;
|
||||
if (!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);
|
||||
@@ -451,13 +385,8 @@ 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 && !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);
|
||||
if (pr->at_ns <= now) {
|
||||
wl_buffer_send_release(pr->buffer);
|
||||
perf_note_release(pr->since_ns);
|
||||
pending_release_free(pr);
|
||||
}
|
||||
@@ -468,12 +397,9 @@ 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) { send_buffer_release(buffer); perf_note_release(since_ns); return; }
|
||||
if (g_release_unpaced || interval <= 0 || g_release_timer_fd < 0) { wl_buffer_send_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);
|
||||
@@ -481,7 +407,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) { send_buffer_release(buffer); perf_note_release(since_ns); return; }
|
||||
if (!pr) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
|
||||
pr->buffer = buffer;
|
||||
pr->surface = s;
|
||||
pr->at_ns = at;
|
||||
@@ -520,12 +446,6 @@ 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++; }
|
||||
@@ -598,10 +518,7 @@ static void constraints_focus_entered(struct wl_resource *target, struct wl_clie
|
||||
|
||||
|
||||
void dmabuf_buffer_unref(struct dmabuf_buffer *b) {
|
||||
/* 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;
|
||||
if (!b || --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]);
|
||||
@@ -700,10 +617,9 @@ 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 (buffer_shown_elsewhere(s->dmabuf, s)) {
|
||||
} else if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) {
|
||||
if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) {
|
||||
if (paced) release_buffer(s, s->dmabuf, now_ns());
|
||||
else send_buffer_release(s->dmabuf);
|
||||
else wl_buffer_send_release(s->dmabuf);
|
||||
}
|
||||
s->dmabuf = NULL;
|
||||
}
|
||||
@@ -812,7 +728,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);
|
||||
}
|
||||
send_buffer_release(buffer);
|
||||
wl_buffer_send_release(buffer);
|
||||
s->buf_w = w;
|
||||
s->buf_h = h;
|
||||
s->buf_alpha = wl_shm_buffer_get_format(shm) == WL_SHM_FORMAT_ARGB8888;
|
||||
@@ -856,7 +772,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);
|
||||
send_buffer_release(buffer);
|
||||
wl_buffer_send_release(buffer);
|
||||
s->buf_w = 1;
|
||||
s->buf_h = 1;
|
||||
s->buf_alpha = bgra[3] != 0xff;
|
||||
@@ -959,9 +875,8 @@ 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 { send_buffer_release(buffer); perf_note_release(since_ns); }
|
||||
else { wl_buffer_send_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) {
|
||||
@@ -1181,7 +1096,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) { send_buffer_release(buffer); return; }
|
||||
if (px <= 0 || px > CURSOR_MAX_PX) { wl_buffer_send_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);
|
||||
@@ -1199,7 +1114,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) send_buffer_release(buffer);
|
||||
if (buffer) wl_buffer_send_release(buffer);
|
||||
}
|
||||
|
||||
static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
@@ -1252,12 +1167,11 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
cursor_publish_hidden();
|
||||
} else {
|
||||
cursor_publish_buffer(s, buffer);
|
||||
send_buffer_release(buffer);
|
||||
wl_buffer_send_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);
|
||||
@@ -1268,7 +1182,7 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
} else {
|
||||
drop_dmabuf(s, 1);
|
||||
s->has_content = 0;
|
||||
if (buffer) send_buffer_release(buffer);
|
||||
if (buffer) wl_buffer_send_release(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1822,8 +1736,6 @@ 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;
|
||||
@@ -1997,7 +1909,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 + CURSOR_MAX_DRAWS, 1))) return 0;
|
||||
if (dl->n <= 0 || !(hs->is_hdr = calloc((size_t)dl->n, 1))) return 0;
|
||||
for (int i = 0; i < dl->n; i++) {
|
||||
struct surface *s = surface_for_image(dl->d[i].img);
|
||||
if (!is_hdr_surface(s)) continue;
|
||||
@@ -2099,6 +2011,12 @@ 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;
|
||||
@@ -2118,6 +2036,7 @@ 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;
|
||||
@@ -2131,7 +2050,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);
|
||||
AHardwareBuffer_release(ahb); /* the slot keeps its own reference */
|
||||
} else {
|
||||
struct vkp_image *img = vkp_image_from_ahb(ahb);
|
||||
if (!img) {
|
||||
@@ -2143,7 +2062,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();
|
||||
vkp_wait_idle(); /* the GPU may still read the picture being replaced */
|
||||
if (g_stream_img == g_stream_slots[slot].img) g_stream_img = NULL;
|
||||
vkp_image_destroy(g_stream_slots[slot].img);
|
||||
}
|
||||
@@ -2189,6 +2108,7 @@ 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);
|
||||
@@ -2196,7 +2116,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; }
|
||||
if (dropped) { AHardwareBuffer_release(dropped); return; } /* a wake-up is already queued */
|
||||
struct input_msg m = { 20, 0, 0, 0, 0 };
|
||||
ssize_t n = write(g_input_pipe[1], &m, sizeof(m));
|
||||
(void)n;
|
||||
@@ -2265,28 +2185,6 @@ 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();
|
||||
@@ -2312,19 +2210,21 @@ 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 && !cursor_on &&
|
||||
!ahb_only) {
|
||||
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. */
|
||||
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 && !cursor_on) ? layer_candidate(&dl, w, h) : -1;
|
||||
int li = (g_zero_copy && !framegen && !cardboard) ? 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 && !cursor_on && blocked != g_zero_copy_paused) {
|
||||
if (g_zero_copy && 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");
|
||||
@@ -2337,9 +2237,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 = layer_only;
|
||||
if (li < 0) ls = ahb_layer_only_candidate(w, h);
|
||||
if (li < 0 && !ls) ls = hdr_unimported_fullscreen(&dl, w, h);
|
||||
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",
|
||||
@@ -2422,6 +2322,8 @@ 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();
|
||||
}
|
||||
@@ -2480,6 +2382,10 @@ 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 &&
|
||||
@@ -2503,7 +2409,6 @@ 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);
|
||||
@@ -2511,21 +2416,18 @@ 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);
|
||||
send_buffer_release(buffer);
|
||||
wl_buffer_send_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 = {
|
||||
@@ -2707,97 +2609,6 @@ 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) {
|
||||
@@ -2806,7 +2617,6 @@ 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) {
|
||||
@@ -2886,10 +2696,7 @@ 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;
|
||||
@@ -2948,7 +2755,6 @@ 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) {
|
||||
@@ -2972,8 +2778,6 @@ 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) {
|
||||
@@ -3159,7 +2963,6 @@ 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) {
|
||||
@@ -3182,6 +2985,10 @@ 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);
|
||||
@@ -3213,6 +3020,9 @@ 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];
|
||||
@@ -3632,7 +3442,6 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -3707,17 +3516,6 @@ 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; }
|
||||
@@ -3897,6 +3695,8 @@ 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();
|
||||
@@ -3917,10 +3717,6 @@ 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);
|
||||
@@ -3931,7 +3727,6 @@ 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,6 +61,3 @@ 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,6 +1,5 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "effects_chain.h"
|
||||
#include "blend_pass.h"
|
||||
#include "vk_loader.h"
|
||||
#include "vk_present.h"
|
||||
#include <pthread.h>
|
||||
@@ -147,16 +146,6 @@ 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;
|
||||
@@ -244,7 +233,6 @@ 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,9 +10,6 @@ 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,11 +374,16 @@ 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);
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
#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,6 +103,7 @@ 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;
|
||||
@@ -123,6 +124,7 @@ 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,6 +24,7 @@ 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,6 +8,7 @@
|
||||
|
||||
#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
|
||||
@@ -68,6 +69,7 @@ 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,7 +8,9 @@ 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,6 +3,7 @@
|
||||
#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);
|
||||
api.setZOrder(tx, g_base_sc, 0); /* GAME = 1, OVERLAY = 2 */
|
||||
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,26 +381,6 @@ 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 };
|
||||
@@ -436,54 +416,22 @@ 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;
|
||||
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)");
|
||||
if (!sniff_modifier(nh, &mod)) {
|
||||
if (tier < TIER_LINEAR) {
|
||||
AHardwareBuffer_release(ahb);
|
||||
l->alloc_tier = vkp_can_import_ahb() ? TIER_PLAIN : TIER_LINEAR;
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
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");
|
||||
}
|
||||
mod = MOD_LINEAR;
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
droiddeck_log("layer", "%s: import of a %s %dx%d pool buffer (stride %u px) into the compositor's Turnip failed",
|
||||
l->name, mod == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", w, h, got.stride);
|
||||
AHardwareBuffer_release(ahb);
|
||||
if (tier < TIER_LINEAR) { l->alloc_tier = tier + 1; continue; }
|
||||
g_alloc_failed = 1;
|
||||
@@ -494,11 +442,10 @@ 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), 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] ? ")" : "");
|
||||
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);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -867,7 +814,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,
|
||||
pool_kind(l->pool_modifier), l->pool_n);
|
||||
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", l->pool_n);
|
||||
}
|
||||
vkp_signal_first_frame();
|
||||
return 0;
|
||||
@@ -889,7 +836,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,
|
||||
pool_kind(l->pool_modifier), l->pool_n);
|
||||
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear", l->pool_n);
|
||||
}
|
||||
vkp_signal_first_frame();
|
||||
return 0;
|
||||
@@ -927,12 +874,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,
|
||||
pool_kind(l->pool_modifier));
|
||||
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear");
|
||||
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,
|
||||
pool_kind(l->pool_modifier),
|
||||
l->pool_modifier == MOD_QCOM_COMPRESSED ? "UBWC" : "linear",
|
||||
g_hdr_pool_fmt == AHB_RGB10A2 ? "10-bit" : "8-bit",
|
||||
color && color->dataspace == DROIDDECK_ADATASPACE_BT2020_PQ ? "BT2020_PQ" : "HDR",
|
||||
cov[0] ? ", " : "", cov);
|
||||
@@ -1026,7 +973,7 @@ void sc_layer_hide_overlay(void) {
|
||||
}
|
||||
|
||||
void sc_layer_window_gone(void) {
|
||||
base_hide(1);
|
||||
base_hide(1); /* the backdrop belongs to the old window, just like GAME and OVERLAY */
|
||||
if (api.state != 1) return;
|
||||
layers_init();
|
||||
for (int i = SC_LAYER_COUNT - 1; i >= 0; i--) retire_sc(&g_layers[i]);
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
#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(GetPhysicalDeviceProperties2)
|
||||
X(GetPhysicalDeviceImageFormatProperties2)
|
||||
|
||||
#define VK_DEVICE_FUNCS(X) \
|
||||
X(GetDeviceQueue) X(CreateSwapchainKHR) X(DestroySwapchainKHR) X(GetSwapchainImagesKHR) \
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,10 +1,7 @@
|
||||
#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);
|
||||
|
||||
@@ -24,9 +21,6 @@ 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
|
||||
@@ -39,9 +33,6 @@ 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;
|
||||
@@ -66,10 +57,6 @@ 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,7 +30,6 @@ 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);
|
||||
@@ -216,24 +215,6 @@ 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) {
|
||||
@@ -311,13 +292,6 @@ 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) {
|
||||
@@ -716,6 +690,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -742,6 +717,7 @@ 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);
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
/* 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,7 +3,6 @@ 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() {
|
||||
@@ -17,10 +16,9 @@ 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
|
||||
@@ -33,11 +31,12 @@ 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" || process == "$packageName:bottom_desktop") return
|
||||
process == "$packageName:shortcuts_sync") return
|
||||
CrashHandler.install(this)
|
||||
com.droiddeck.launcher.core.SteamIntegration.syncShortcutsListing(this)
|
||||
com.droiddeck.launcher.session.QuestExperimental.initialize(this)
|
||||
@@ -57,12 +56,11 @@ 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,11 +5,8 @@ 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)
|
||||
@@ -23,10 +20,6 @@ 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) {
|
||||
@@ -36,14 +29,7 @@ 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.autoDxvkHeld(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) {
|
||||
@@ -56,35 +42,6 @@ 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,12 +14,10 @@ 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
|
||||
@@ -44,8 +42,7 @@ 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("")
|
||||
var gpu: GpuInfo = GpuInfo.detect()
|
||||
private set
|
||||
val gpu: GpuInfo = GpuInfo.detect()
|
||||
var mode by mutableStateOf(SessionPrefs.GPU_DRIVERS_AUTO)
|
||||
var pairRows by mutableStateOf<List<PairRow>>(emptyList())
|
||||
var pairBusy by mutableStateOf<String?>(null)
|
||||
@@ -68,23 +65,20 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
}
|
||||
|
||||
fun state() = GpuDriversState(
|
||||
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)),
|
||||
gpuName = gpu.displayName(activity), gpuFamily = gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity),
|
||||
supported = gpu.support == GpuInfo.Support.TESTED, unsupported = gpu.support == GpuInfo.Support.UNSUPPORTED,
|
||||
auto = mode == SessionPrefs.GPU_DRIVERS_AUTO, pairs = pairRows, busy = pairBusy, percent = pairPercent,
|
||||
autoStatus = autoStatus, releaseStatus = releaseStatus.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, powerVr = gpu.family == GpuInfo.Family.POWERVR,
|
||||
systemPowerVr = SystemVulkanDriver.usesDefault(linuxSelected),
|
||||
activeBundle = activeBundle,
|
||||
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)
|
||||
@@ -129,14 +123,8 @@ 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 (restartBlocked()) return
|
||||
if (SessionState.running) 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"))
|
||||
@@ -154,7 +142,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(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)
|
||||
if (id.isEmpty()) DriverRow("", activity.getString(R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false)
|
||||
else bundleRow(id, linux = true) ?: DriverRow(
|
||||
id, lm.getDriverName(id),
|
||||
listOfNotNull(
|
||||
@@ -189,7 +177,6 @@ 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)
|
||||
@@ -206,28 +193,17 @@ 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 nad = DriverPairs.isNonAdreno(p.key)
|
||||
val bundle = p.bundle?.let { if (nad) installedBundleAnyBuild(it) else installedBundle(it) }
|
||||
val bundle = p.bundle?.let { installedBundle(it) }
|
||||
val displayId = if (p.bundle != null) bundle?.androidId else p.display?.let { TurnipReleases.installedId(activity, it, td::isInstalled) }
|
||||
val linuxId = if (p.bundle != null) bundle?.linuxId else p.linux?.let { TurnipReleases.installedId(activity, it, lm::isInstalled) }
|
||||
val mb = p.assets.sumOf { it.size } / 1_048_576.0
|
||||
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 = version, update = if (update) builtOn.takeIf { it.isNotEmpty() }?.let { activity.getString(R.string.drivers_built_on, it) } ?: p.version else null,
|
||||
key = p.key, name = p.name, version = p.version,
|
||||
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.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)),
|
||||
active = displayId != null && linuxId != null &&
|
||||
SessionPrefs.androidDriver(activity) == displayId && SessionPrefs.linuxDriver(activity) == linuxId,
|
||||
)
|
||||
}.sortedByDescending { it.recommended }
|
||||
}
|
||||
@@ -261,9 +237,7 @@ 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() } ||
|
||||
// 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) })
|
||||
cached.assets.none { it.pair.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun ensureAuto(force: Boolean) {
|
||||
@@ -273,33 +247,9 @@ 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
|
||||
}
|
||||
@@ -313,31 +263,8 @@ 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, com.droiddeck.launcher.core.DeviceSupport.vkBridge(activity)).lowercase())
|
||||
autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText(activity).lowercase())
|
||||
return
|
||||
}
|
||||
releaseChecking = true
|
||||
@@ -348,7 +275,6 @@ 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))
|
||||
@@ -358,37 +284,10 @@ 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)
|
||||
@@ -397,53 +296,6 @@ 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
|
||||
@@ -452,15 +304,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
installPair(pair, auto = false)
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean) {
|
||||
if (!pair.complete) return
|
||||
pairBusy = pair.key
|
||||
pairPercent = 0
|
||||
@@ -470,7 +314,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 ->
|
||||
(if (force) null else installedIdOf(asset))
|
||||
installedIdOf(asset)
|
||||
?: installAsset(asset) { pct -> ui.post { pairPercent = (i * 100 + pct) / zips.size } }.also { downloaded = true }
|
||||
}
|
||||
if (pair.bundle != null) DriverBundle.get(activity, ids[0])!!.let { listOf(it.androidId, it.linuxId) } else ids
|
||||
@@ -478,39 +322,23 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
ui.post {
|
||||
pairBusy = null
|
||||
pairPercent = -1
|
||||
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
|
||||
}
|
||||
result.onSuccess { (displayId, linuxId) ->
|
||||
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
|
||||
// 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 })
|
||||
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId))
|
||||
if (changed) AutoDriverTrial.begin(
|
||||
activity, AutoDriverTrial.release(pair), "${pair.name} ${pair.version}", displayId, linuxId,
|
||||
trial?.previousDisplay ?: previousDisplay, trial?.previousLinux ?: previousLinux,
|
||||
@@ -538,9 +366,6 @@ 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)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -24,9 +24,6 @@ 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,
|
||||
@@ -57,24 +54,6 @@ 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)
|
||||
@@ -168,62 +147,8 @@ object HomeApp {
|
||||
.getShortcutIconDrawable(info, context.resources.displayMetrics.densityDpi)?.toBitmap(px, px)
|
||||
}.getOrNull()
|
||||
|
||||
fun trackRecents(app: android.app.Application) {
|
||||
app.registerActivityLifecycleCallbacks(object : android.app.Application.ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(activity: android.app.Activity, savedInstanceState: android.os.Bundle?) = Unit
|
||||
override fun onActivityStarted(activity: android.app.Activity) = Unit
|
||||
override fun onActivityResumed(activity: android.app.Activity) = syncRecents(activity)
|
||||
override fun onActivityPaused(activity: android.app.Activity) { if (activity is FrontEndActivity) syncRecents(activity) }
|
||||
override fun onActivityStopped(activity: android.app.Activity) = Unit
|
||||
override fun onActivitySaveInstanceState(activity: android.app.Activity, outState: android.os.Bundle) = Unit
|
||||
override fun onActivityDestroyed(activity: android.app.Activity) = Unit
|
||||
})
|
||||
}
|
||||
|
||||
private fun syncRecents(activity: android.app.Activity) =
|
||||
keepOutOfRecents(activity, activity is FrontEndActivity && !SessionState.running && isDefault(activity))
|
||||
|
||||
internal enum class RecentsStep { HIDE, SHOW, FORGET, NONE }
|
||||
|
||||
internal fun recentsStep(hidden: Boolean, owned: Boolean, excluded: Boolean): RecentsStep = when {
|
||||
hidden && !excluded -> RecentsStep.HIDE
|
||||
!hidden && owned && excluded -> RecentsStep.SHOW
|
||||
!hidden && owned -> RecentsStep.FORGET
|
||||
else -> RecentsStep.NONE
|
||||
}
|
||||
|
||||
fun keepOutOfRecents(activity: android.app.Activity, hidden: Boolean) {
|
||||
val prefs = activity.getSharedPreferences("home-recents", Context.MODE_PRIVATE)
|
||||
val owned = prefs.getStringSet("tasks", null).orEmpty()
|
||||
val id = activity.taskId.toString()
|
||||
if (!hidden && id !in owned) return
|
||||
runCatching {
|
||||
@Suppress("DEPRECATION")
|
||||
fun idOf(task: android.app.ActivityManager.AppTask) =
|
||||
if (Build.VERSION.SDK_INT >= 29) task.taskInfo.taskId else task.taskInfo.persistentId
|
||||
val tasks = activity.getSystemService(android.app.ActivityManager::class.java).appTasks
|
||||
val task = tasks.firstOrNull { idOf(it) == activity.taskId } ?: return
|
||||
val excluded = task.taskInfo.baseIntent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS != 0
|
||||
val step = recentsStep(hidden, id in owned, excluded)
|
||||
if (step == RecentsStep.NONE) return
|
||||
if (step != RecentsStep.FORGET) task.setExcludeFromRecents(step == RecentsStep.HIDE)
|
||||
val live = tasks.map { idOf(it).toString() }.toSet()
|
||||
val next = owned.filter { it in live && it != id }.toMutableSet()
|
||||
if (step == RecentsStep.HIDE) next += id
|
||||
prefs.edit().putStringSet("tasks", next).apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun startShortcut(context: Context, info: ShortcutInfo) {
|
||||
val task = generateSequence(context) { (it as? android.content.ContextWrapper)?.baseContext }
|
||||
.filterIsInstance<android.app.Activity>().firstOrNull()?.taskId
|
||||
task?.let { com.droiddeck.launcher.ui.DualScreenPanel.yieldTo(it, true) }
|
||||
try {
|
||||
context.getSystemService(LauncherApps::class.java).startShortcut(info, null, null)
|
||||
} catch (e: Exception) {
|
||||
task?.let { com.droiddeck.launcher.ui.DualScreenPanel.yieldTo(it, false) }
|
||||
throw e
|
||||
}
|
||||
context.getSystemService(LauncherApps::class.java).startShortcut(info, null, null)
|
||||
}
|
||||
|
||||
fun openAppInfo(context: Context, app: LaunchableApp) {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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,6 +197,7 @@ 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,7 +62,6 @@ 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,6 +71,7 @@ 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,15 +18,12 @@ 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
|
||||
|
||||
@@ -96,7 +93,6 @@ 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'")
|
||||
@@ -201,46 +197,9 @@ 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 = logDir(context)
|
||||
val dir = SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: SessionPaths.sessionFolders(context).lastOrNull()
|
||||
val session = JSONObject()
|
||||
.put("id", SessionState.sessionId ?: dir?.name ?: JSONObject.NULL)
|
||||
.put("phase", SessionState.phase.name)
|
||||
@@ -392,7 +351,6 @@ 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,6 +18,7 @@ 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()
|
||||
@@ -159,7 +160,7 @@ object AgentEnv {
|
||||
}
|
||||
|
||||
private fun pathOf(lines: List<String>) =
|
||||
lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: LinuxRuntime.GUEST_PATH
|
||||
lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: BASE_PATH
|
||||
|
||||
private fun bin(context: Context): File {
|
||||
val root = LinuxRuntime.rootDir(context).canonicalFile.toPath()
|
||||
|
||||
@@ -26,10 +26,6 @@ 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
|
||||
@@ -47,7 +43,7 @@ object AgentGuest {
|
||||
observer?.stopWatching()
|
||||
observer = null
|
||||
lastFocusKey = null
|
||||
main.post { steamKeyboard = false; desktopTextField = false; gameTextField = false; gameProgram = null }
|
||||
main.post { steamKeyboard = false; desktopTextField = false }
|
||||
val dir = dir(context)
|
||||
dir.deleteRecursively()
|
||||
dir.mkdirs()
|
||||
@@ -63,7 +59,6 @@ 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) }
|
||||
}
|
||||
@@ -76,7 +71,7 @@ object AgentGuest {
|
||||
synchronized(lock) {
|
||||
observer?.stopWatching()
|
||||
observer = null
|
||||
main.post { steamKeyboard = false; desktopTextField = false; gameTextField = false; gameProgram = null }
|
||||
main.post { steamKeyboard = false; desktopTextField = false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,16 +129,6 @@ 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,6 +1,5 @@
|
||||
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
|
||||
@@ -144,10 +143,9 @@ 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)
|
||||
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 } }
|
||||
bridge.applyTouch { if (right) { it.rightX = x; it.rightY = y } else { it.leftX = x; it.leftY = y } }
|
||||
hold(request, 300)
|
||||
bridge.applyTouch(stickWrite) { if (right) { it.rightX = 0f; it.rightY = 0f } else { it.leftX = 0f; it.leftY = 0f } }
|
||||
bridge.applyTouch { 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,6 +118,7 @@ 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,46 +130,22 @@ public class PulseAudioComponent extends SessionPart {
|
||||
|
||||
private void ensureClientSink(java.io.PrintWriter out) {
|
||||
try {
|
||||
// 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;
|
||||
long deadline = System.currentTimeMillis() + 15000L;
|
||||
String sinks = null;
|
||||
boolean socketSeen = false;
|
||||
while (System.currentTimeMillis() < deadline && pid > 1) {
|
||||
socketSeen |= socket().exists();
|
||||
if (socketSeen && (sinks = pactl("list", "short", "sinks")) != null) break;
|
||||
if (socket().exists() && (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 30 s; no fallback attempted";
|
||||
line = "== client sink: the daemon did not answer in 15 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 {
|
||||
// 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";
|
||||
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";
|
||||
Log.w(TAG, line);
|
||||
}
|
||||
Log.i(TAG, line);
|
||||
@@ -179,14 +155,6 @@ 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,10 +13,15 @@ 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
|
||||
@@ -37,11 +42,13 @@ 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()
|
||||
@@ -58,6 +65,7 @@ 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) {
|
||||
@@ -72,6 +80,7 @@ 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
|
||||
@@ -95,7 +104,7 @@ internal class AddonPack(
|
||||
}
|
||||
val root = root(context).apply { mkdirs() }
|
||||
val archive = File(root, "$sha256.tar.zst")
|
||||
val downloaded = bundled(context, archive) || urls.any { url ->
|
||||
val downloaded = urls.any { url ->
|
||||
Downloader.downloadFile(url, archive, true, sha256) { p -> mutableState.value = State.Downloading(p.coerceIn(0f, 1f)) }
|
||||
.also { if (!it) archive.delete() }
|
||||
}
|
||||
@@ -117,16 +126,6 @@ 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"
|
||||
@@ -149,8 +148,5 @@ 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,8 +10,6 @@ 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
|
||||
@@ -44,6 +42,7 @@ 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"
|
||||
@@ -112,7 +111,6 @@ 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)
|
||||
@@ -189,11 +187,7 @@ 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", 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"
|
||||
})
|
||||
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")
|
||||
|
||||
h("Display")
|
||||
k("Session output", SessionState.outputSize?.let { "${it.first}x${it.second}" })
|
||||
@@ -215,7 +209,7 @@ object DeviceReport {
|
||||
|
||||
h("Drivers")
|
||||
val gpu = com.droiddeck.launcher.gpu.GpuInfo.detect()
|
||||
k("GPU", "${gpu.name} · ${gpu.family.label(gpu.name)} · ${gpu.supportText(SessionPrefs.vkBridge(context))}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "")
|
||||
k("GPU", "${gpu.name} · ${gpu.family.label} · ${gpu.supportText}" + 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")
|
||||
@@ -228,25 +222,19 @@ object DeviceReport {
|
||||
com.droiddeck.launcher.gpu.AutoDriverTrial.bad(context))?.let { " (recommended pair: $it)" } ?: ""))
|
||||
val turnip = TurnipDriver(context)
|
||||
val androidChoice = SessionPrefs.androidDriver(context)
|
||||
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("Display driver (chosen)", if (androidChoice.isEmpty()) "Auto -> ${turnip.autoId()}" else androidChoice)
|
||||
k(" name / version", "${turnip.displayName(if (androidChoice.isEmpty()) turnip.autoId() else androidChoice)} ${turnip.driverVersion(if (androidChoice.isEmpty()) turnip.autoId() else androidChoice)}".trim())
|
||||
k(" imported available", turnip.enumerateImported().ifEmpty { listOf("none") }.joinToString(", "))
|
||||
val lm = LinuxVulkanDriverManager(context)
|
||||
run {
|
||||
val id = SessionPrefs.linuxDriver(context)
|
||||
val label = if (id.isEmpty()) DeviceInfo.builtinDriverLabel(SystemVulkanDriver.isPowerVr(), runtime = true)
|
||||
val label = if (id.isEmpty()) "runtime default (the Turnip built into the runtime)"
|
||||
else "${lm.getDriverName(id)} ${lm.getDriverVersion(id)}".trim() +
|
||||
lm.getMinGlibc(id).let { if (it.isEmpty()) "" else " (glibc $it+)" } +
|
||||
if (lm.isInstalled(id)) "" else if (SystemVulkanDriver.isPowerVr()) " [MISSING - select an installed driver or Auto]"
|
||||
else " [MISSING - falls back to the runtime's own]"
|
||||
if (lm.isInstalled(id)) "" 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,43 +3,11 @@ 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 {
|
||||
private val adrenoGpu by lazy {
|
||||
fun adreno(): Boolean =
|
||||
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,9 +126,11 @@ 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)
|
||||
@@ -182,6 +184,7 @@ 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 | IllegalStateException ignored) { }
|
||||
try { active.close(); } catch (IOException ignored) { }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ 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));
|
||||
@@ -102,6 +103,7 @@ 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;
|
||||
}
|
||||
@@ -111,11 +113,13 @@ 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) {
|
||||
@@ -124,11 +128,13 @@ 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);
|
||||
@@ -143,6 +149,7 @@ 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) {
|
||||
@@ -185,6 +192,7 @@ 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);
|
||||
@@ -205,6 +213,7 @@ 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;
|
||||
@@ -230,7 +239,7 @@ public final class Downloader {
|
||||
try (FileOutputStream out = new FileOutputStream(marker)) {
|
||||
p.store(out, "DroidDeck partial download validator");
|
||||
} catch (IOException e) {
|
||||
marker.delete();
|
||||
marker.delete(); // No valid metadata means no future resume.
|
||||
Log.w(TAG, "cannot persist download validator", e);
|
||||
}
|
||||
}
|
||||
@@ -243,6 +252,7 @@ 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;
|
||||
}
|
||||
@@ -276,6 +286,7 @@ 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;
|
||||
}
|
||||
@@ -322,12 +333,14 @@ 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);
|
||||
@@ -377,6 +390,7 @@ 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,6 +30,8 @@ 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,6 +5,7 @@ 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 ->
|
||||
@@ -18,5 +19,6 @@ 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,9 +12,6 @@ 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,6 +11,7 @@ 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()
|
||||
}
|
||||
@@ -131,81 +132,43 @@ object LogRedactor {
|
||||
@Volatile
|
||||
private var accounts: List<Learned> = emptyList()
|
||||
|
||||
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 {
|
||||
fun learnAccounts(vararg loginUsers: java.io.File) {
|
||||
val names = LinkedHashSet<String>()
|
||||
val kv = Regex("\"(AccountName|PersonaName)\"\\s+\"([^\"]*)\"")
|
||||
var unread = false
|
||||
for (file in loginUsers) {
|
||||
if (!file.isFile) continue
|
||||
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 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 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)
|
||||
}
|
||||
accounts = if (unread) (learned + accounts).distinctBy { it.regex.pattern }.take(MAX_LEARNED_ACCOUNTS) else learned
|
||||
return !unread
|
||||
// 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
|
||||
}
|
||||
|
||||
fun learnFromRuntime(root: java.io.File): Boolean {
|
||||
val addressesComplete = learnOwnAddresses(java.io.File(root, "etc/droiddeck-net"))
|
||||
val accountsComplete = learnAccounts(
|
||||
fun learnFromRuntime(root: java.io.File) {
|
||||
learnOwnAddresses(java.io.File(root, "etc/droiddeck-net"))
|
||||
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): Boolean {
|
||||
fun learnOwnAddresses(linkFile: java.io.File) {
|
||||
val found = ArrayList<Learned>()
|
||||
try {
|
||||
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
|
||||
if (linkFile.isFile) linkFile.forEachLine { line ->
|
||||
val addr = line.trim().takeIf { it.startsWith("addr ") }?.split(Regex("\\s+"))?.getOrNull(1) ?: return@forEachLine
|
||||
when (kind(addr)) {
|
||||
"public IPv6" -> {
|
||||
val groups = expand6(addr)?.take(4) ?: return@forEach
|
||||
val groups = expand6(addr)?.take(4) ?: return@forEachLine
|
||||
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_.]+)?"),
|
||||
@@ -216,10 +179,9 @@ object LogRedactor {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
return
|
||||
}
|
||||
own = found
|
||||
return true
|
||||
}
|
||||
|
||||
fun kind(address: String): String? {
|
||||
@@ -266,6 +228,7 @@ 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 {
|
||||
@@ -278,6 +241,8 @@ 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,6 +52,8 @@ 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,6 +45,8 @@ 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 {
|
||||
@@ -88,6 +90,7 @@ 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)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.droiddeck.launcher.files
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
|
||||
object AllFilesAccess {
|
||||
fun missing(sdk: Int = Build.VERSION.SDK_INT, granted: () -> Boolean = { Environment.isExternalStorageManager() }): Boolean =
|
||||
sdk >= Build.VERSION_CODES.R && !granted()
|
||||
|
||||
fun open(context: Context) {
|
||||
val app = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + context.packageName))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
runCatching { context.startActivity(app) }.recoverCatching {
|
||||
context.startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,3 +311,87 @@ 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -498,25 +498,6 @@ fun FileManagerScreen(
|
||||
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
val drives = remember(storageTick) { StorageRoots.list(context) }
|
||||
var accessDrive by remember { mutableStateOf<String?>(null) }
|
||||
fun unreadable(drive: StorageRoot) {
|
||||
if (AllFilesAccess.missing()) accessDrive = drive.label
|
||||
else Toast.makeText(context, context.getString(R.string.fm_drive_not_readable, drive.label), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
accessDrive?.let { label ->
|
||||
OutlinedAlertDialog(
|
||||
onDismissRequest = { accessDrive = null },
|
||||
title = { Text(stringResource(R.string.fm_drive_access_title, label)) },
|
||||
text = { Text(stringResource(R.string.fm_drive_access_text)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
accessDrive = null
|
||||
AllFilesAccess.open(context)
|
||||
}, modifier = Modifier.focusGlide(CircleShape)) { Text(stringResource(R.string.upd_open_settings)) }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { accessDrive = null }, modifier = Modifier.focusGlide(CircleShape)) { Text(stringResource(R.string.common_cancel)) } },
|
||||
)
|
||||
}
|
||||
val linuxRoot = remember(storageTick) {
|
||||
com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context).takeIf { File(it, "usr").isDirectory }
|
||||
}
|
||||
@@ -766,7 +747,11 @@ fun FileManagerScreen(
|
||||
if (drive.readable) {
|
||||
openDrive(drive.dir)
|
||||
} else {
|
||||
unreadable(drive)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.fm_drive_not_readable, drive.label),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusGlide(),
|
||||
@@ -1087,7 +1072,7 @@ fun FileManagerScreen(
|
||||
add(locItem(stringResource(R.string.fm_drive_internal), Icons.Filled.Smartphone, File("/storage/emulated/0")))
|
||||
drives.filter { it.removable }.forEach { d ->
|
||||
add(RailItem(d.label, Icons.Filled.SdStorage, !showFavorites && currentRoot.absolutePath == d.dir.absolutePath) {
|
||||
showFavorites = false; if (d.readable) openDrive(d.dir) else unreadable(d)
|
||||
showFavorites = false; if (d.readable) openDrive(d.dir)
|
||||
})
|
||||
}
|
||||
linuxRoot?.let { add(locItem(stringResource(R.string.fm_drive_linux), Icons.Filled.Terminal, it)) }
|
||||
|
||||
@@ -57,6 +57,7 @@ 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,6 +63,7 @@ object StorageRoots {
|
||||
}
|
||||
return true
|
||||
}
|
||||
// vold-managed locations only hold removable volumes, whatever the fstype is.
|
||||
return path.startsWith("/storage/") || path.startsWith("/mnt/media_rw/")
|
||||
}
|
||||
|
||||
@@ -90,8 +91,9 @@ object StorageRoots {
|
||||
.filter { present(it.state) }
|
||||
.forEachIndexed { index, volume ->
|
||||
val uuid = volume.uuid?.takeIf { it.isNotBlank() }
|
||||
val directory = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) volume.directory else null
|
||||
val dirName = directory?.name?.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 descSlug = volume.getDescription(context)?.trim()
|
||||
?.takeIf { it.isNotBlank() && !it.equals("android", ignoreCase = true) }
|
||||
?.replace(Regex("[^A-Za-z0-9._-]+"), "-")
|
||||
@@ -99,11 +101,15 @@ object StorageRoots {
|
||||
entry.removable = !volume.isPrimary && volume.isRemovable
|
||||
if (entry.label == null) entry.label = labelFor(context, volume, uuid ?: dirName ?: descSlug)
|
||||
|
||||
directory?.let { entry.candidates += it }
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
volume.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 }
|
||||
}
|
||||
@@ -116,6 +122,8 @@ 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,8 +243,6 @@ 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(
|
||||
@@ -253,8 +251,6 @@ 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)
|
||||
@@ -291,20 +287,14 @@ object AddedGameArt {
|
||||
var sgdbDown = false
|
||||
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
|
||||
if (again.isEmpty()) File(cache, REFETCH).delete()
|
||||
val full = Slot.COVER in start
|
||||
val allowed = if (full) Slot.values().toSet() else EXTRAS + again
|
||||
val allowed = if (full) Slot.values().toSet() else EXTRAS
|
||||
var need = start intersect allowed
|
||||
if (need.isEmpty() || (need == setOf(Slot.ICON) && sgdbKey.isEmpty())) continue
|
||||
val cache = cacheDir(context, game)
|
||||
val tried = File(cache, EXTRAS_TRIED)
|
||||
if (!full && again.isEmpty() && tried.isFile && tried.lastModified() >= keyChangedAt && System.currentTimeMillis() - tried.lastModified() < RETRY_AFTER_MS) continue
|
||||
if (!full && tried.isFile && tried.lastModified() >= keyChangedAt && System.currentTimeMillis() - tried.lastModified() < RETRY_AFTER_MS) continue
|
||||
cache.mkdirs()
|
||||
if (Slot.LOGO in need && game.source != Library.ADDED && runCatching { sources.storeLogo(game) }.getOrDefault(false)) {
|
||||
changed = true
|
||||
@@ -325,7 +315,7 @@ object AddedGameArt {
|
||||
if (need.isNotEmpty() && sgdbKey.isNotEmpty()) {
|
||||
if (sgdbDown) asked = false else try {
|
||||
val gridId = remembered(File(cache, "sgdb-id"), keyChangedAt) {
|
||||
(if (battleNetTitle != null) sources.sgdbExactGame else sources.sgdbGame)(sgdbKey, match.appId, game.name)?.toString()
|
||||
sources.sgdbGame(sgdbKey, match.appId, game.name)?.toString()
|
||||
}?.toIntOrNull()
|
||||
if (gridId != null && sources.sgdb(sgdbKey, gridId, cache, need)) changed = true
|
||||
} catch (e: Exception) {
|
||||
@@ -334,57 +324,11 @@ object AddedGameArt {
|
||||
asked = false
|
||||
}
|
||||
}
|
||||
val left = missing(context, game)
|
||||
if (again.isNotEmpty()) keepRefetch(cache, again intersect left)
|
||||
if ((left intersect EXTRAS).isEmpty()) tried.delete() else if (asked) tried.writeText("")
|
||||
if ((missing(context, game) intersect EXTRAS).isEmpty()) tried.delete() else if (asked) tried.writeText("")
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
internal const val REFETCH = "refetch"
|
||||
|
||||
private fun refetch(cache: File): Set<Slot> =
|
||||
runCatching { File(cache, REFETCH).readText() }.getOrDefault("").split(',').mapNotNull { key -> Slot.values().firstOrNull { it.key == key } }.toSet()
|
||||
|
||||
private fun keepRefetch(cache: File, slots: Set<Slot>) {
|
||||
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 }
|
||||
val legacy = runCatching { File(cache, "steam-appid").readText().trim().toInt() }.getOrNull()?.takeIf { it > 0 }
|
||||
val before = recorded?.appId ?: legacy ?: return false
|
||||
val identity = runCatching { GameIdentifier.identify(game.exe) }.getOrNull()
|
||||
val now = identity?.appId
|
||||
val fromFiles = recorded?.certainty == SteamMatch.Certainty.FILES || before == GameIdentifier.SPACEWAR
|
||||
if (now == before || (now == null && !fromFiles)) return false
|
||||
SteamMatch.put(context, game.folder.path,
|
||||
if (identity != null && now != null) SteamMatch.Match(now, SteamMatch.Certainty.FILES, identity.source.name.lowercase(), System.currentTimeMillis())
|
||||
else SteamMatch.Match(null, SteamMatch.Certainty.NONE, "", 0))
|
||||
val dropped = Slot.values().filter { File(cache, SteamGridDb.cacheName(it)).delete() }.toSet()
|
||||
Slot.values().forEach { File(cache, SteamGridDb.cacheName(it) + ".src").delete() }
|
||||
listOf("sgdb-id", "steam-appid", EXTRAS_TRIED).forEach { File(cache, it).delete() }
|
||||
keepRefetch(cache, refetch(cache) + dropped)
|
||||
Log.i(TAG, "${game.name}: its files no longer name Steam app $before; its art is fetched again")
|
||||
return dropped.isNotEmpty()
|
||||
}
|
||||
|
||||
internal fun steamMatch(context: Context, game: AddedGames.Game, search: (String) -> Int?): SteamMatch.Match {
|
||||
if (game.source == Store.BATTLENET.id) {
|
||||
val id = game.storeId?.let(BattleNet::title)?.steam
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -70,10 +69,6 @@ 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}/")
|
||||
}
|
||||
@@ -88,7 +83,6 @@ 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
|
||||
@@ -99,11 +93,10 @@ object AddedGames {
|
||||
|
||||
fun candidates(folder: File): List<File> = rank(folder).exes
|
||||
|
||||
fun rank(folder: File, reuse: Boolean = false): GameExePicker.Ranking {
|
||||
fun rank(folder: File): 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()
|
||||
@@ -115,27 +108,23 @@ 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
|
||||
}
|
||||
|
||||
@@ -177,39 +166,25 @@ object AddedGames {
|
||||
"launcher", "game", "engine", "system", "data", "app",
|
||||
)
|
||||
|
||||
internal fun groupGames(folder: File, reuse: Boolean = false): List<File>? {
|
||||
internal fun groupGames(folder: File): 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, reuse).exes.isNotEmpty() }.takeIf { it.size >= 2 }
|
||||
return dirs.filter { rank(it).exes.isNotEmpty() }.takeIf { it.size >= 2 }
|
||||
}
|
||||
|
||||
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 expand(folder: File, entries: Set<String>): List<File> =
|
||||
if (folder.path in entries) listOf(folder) else groupGames(folder) ?: listOf(folder)
|
||||
|
||||
private fun entryFolders(context: Context): Set<String> = AddedExes.list(context).map { it.folder }.toSet()
|
||||
|
||||
private fun libraryDepotInstalls(context: Context): List<File> =
|
||||
SteamLibraries.list(context).flatMap { SteamLibraries.depotInstalls(File(it.path, "steamapps/common")) }
|
||||
|
||||
/** The card's Steam libraries: the arm64 client's steamapps and the x86-64 client's own folder beside it. */
|
||||
internal fun isSteamLibraryFolder(name: String): Boolean =
|
||||
name.equals("steamapps", ignoreCase = true) || name == com.droiddeck.launcher.session.GameStorage.X64_CARD_DIR
|
||||
|
||||
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> {
|
||||
fun scan(context: Context): List<Game> {
|
||||
val out = ArrayList<Game>()
|
||||
GamesFolderMigration.run(context)
|
||||
val folders = listOfNotNull(
|
||||
@@ -225,16 +200,15 @@ 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, reuse = true)) if (game.path !in removed) scanGame(context, game, out, reuse = true)
|
||||
for (game in expand(folder, entries)) if (game.path !in removed) scanGame(context, game, out)
|
||||
}
|
||||
}
|
||||
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, reuse = true)
|
||||
scanGame(context, folder, out, single = entry)
|
||||
}
|
||||
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 (folder in StoreInstallRoot.gameFolders(context)) if (StoreInstallRoot.isFinished(folder)) scanGame(context, folder, out)
|
||||
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)
|
||||
}
|
||||
@@ -280,7 +254,6 @@ 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") },
|
||||
@@ -342,6 +315,7 @@ 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)
|
||||
@@ -398,7 +372,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -415,11 +388,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, reuse: Boolean = false) {
|
||||
private fun scanGame(context: Context, folder: File, out: MutableList<Game>, single: AddedExes.Entry? = null) {
|
||||
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, reuse)
|
||||
val ranking = rank(folder)
|
||||
val candidates = ranking.exes
|
||||
val picked = SessionPrefs.addedGameExe(context, folder.path)
|
||||
val chosen = picked.takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.isFile }
|
||||
@@ -443,14 +416,7 @@ 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,
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
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,8 +3,6 @@ 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
|
||||
@@ -22,11 +20,7 @@ object AutoComponents {
|
||||
private const val TAG = "AutoComponents"
|
||||
|
||||
fun appKey(game: Library.SteamGame): String? =
|
||||
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()
|
||||
}
|
||||
if (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>()
|
||||
@@ -54,8 +48,6 @@ 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())
|
||||
@@ -64,12 +56,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 || job.component != null && job.key in queuedKeys) flow.value + id else flow.value - id
|
||||
flow.value = if (on) 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 revision: Int? = null,
|
||||
val inSession: Boolean = false,
|
||||
) {
|
||||
val key get() = when {
|
||||
pics != null -> "pics"
|
||||
@@ -87,45 +79,29 @@ 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()))
|
||||
}
|
||||
|
||||
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 queueDownload(context: Context, id: String, asked: Boolean, inSession: Boolean = false) =
|
||||
enqueue(context, Job(null, id, asked, seq.incrementAndGet(), inSession = inSession))
|
||||
|
||||
private fun enqueue(context: Context, job: Job) {
|
||||
val added = synchronized(jobs) {
|
||||
if (!queuedKeys.add(job.key)) {
|
||||
if (!queuedKeys.add(job.key)) {
|
||||
synchronized(jobs) {
|
||||
val old = jobs.firstOrNull { it.key == job.key }
|
||||
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)) {
|
||||
if (old != null && ((job.asked && !old.asked) || (job.inSession && !old.inSession)) && 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, pics,
|
||||
old.inSession || job.inSession, revision))
|
||||
if (job.asked && !old.asked) job.seq else old.seq, old.pics ?: job.pics, old.inSession || job.inSession))
|
||||
}
|
||||
false
|
||||
} else {
|
||||
jobs.add(job)
|
||||
mark(job, true)
|
||||
true
|
||||
}.also { (jobs as java.lang.Object).notifyAll() }
|
||||
}
|
||||
if (runNow && !draining) drain(context)
|
||||
return
|
||||
}
|
||||
if (added && job.component != null) Log.i(TAG, "wincomp: queued ${job.component}")
|
||||
if (job.component != null) Log.i(TAG, "wincomp: queued ${job.component}")
|
||||
mark(job, true)
|
||||
jobs.add(job)
|
||||
if (runNow) { if (!draining) drain(context) } else ensureWorker(context.applicationContext)
|
||||
}
|
||||
|
||||
@@ -137,15 +113,8 @@ 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) {
|
||||
val key = appKey(game) ?: return
|
||||
queuePending(context, key)
|
||||
if (appKey(game) == null) return
|
||||
enqueue(context, Job(game, null, true, seq.incrementAndGet(), inSession = true))
|
||||
}
|
||||
|
||||
@@ -155,6 +124,7 @@ object AutoComponents {
|
||||
try {
|
||||
while (true) {
|
||||
val job = next(context) ?: return
|
||||
queuedKeys.remove(job.key)
|
||||
run(context, job)
|
||||
}
|
||||
} finally {
|
||||
@@ -165,7 +135,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -179,17 +148,13 @@ object AutoComponents {
|
||||
worker = Thread({
|
||||
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND)
|
||||
while (true) {
|
||||
val job = try {
|
||||
synchronized(jobs) {
|
||||
next(app) ?: run {
|
||||
(jobs as java.lang.Object).wait(PAUSE_MS)
|
||||
next(app)
|
||||
}
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
return@Thread
|
||||
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
|
||||
}
|
||||
if (job == null) continue
|
||||
queuedKeys.remove(job.key)
|
||||
WinComponents.background { run(app, job) }
|
||||
}
|
||||
}, "auto-components").apply { isDaemon = true; start() }
|
||||
@@ -199,23 +164,18 @@ object AutoComponents {
|
||||
try {
|
||||
when {
|
||||
job.pics != null -> SteamAppInfo.lookup(context, job.pics)
|
||||
job.component != null -> download(context, job.component, job.asked, job.inSession, job.revision)
|
||||
job.component != null -> download(context, job.component, job.asked, job.inSession)
|
||||
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,
|
||||
knownCatalog: Map<String, WinComponents.Component>? = null) {
|
||||
private fun apply(context: Context, game: Library.SteamGame, asked: Boolean, inSession: Boolean = false) {
|
||||
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) {
|
||||
@@ -223,10 +183,9 @@ 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 (knownCatalog == null && asked && online(context)) steamAppOf(context, game)?.let { SteamAppInfo.lookup(context, listOf(it)) }
|
||||
if (asked && online(context)) steamAppOf(context, game)?.let { SteamAppInfo.lookup(context, listOf(it)) }
|
||||
val findings = findings(context, game)
|
||||
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 catalog = (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
|
||||
@@ -237,37 +196,22 @@ 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
|
||||
waitFor(game, id)
|
||||
synchronized(waiting) { waiting.getOrPut(id) { ArrayList() }.let { if (it.none { g -> appKey(g) == key }) it.add(game) } }
|
||||
queueDownload(context, id, asked, inSession)
|
||||
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, 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
|
||||
}
|
||||
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 }
|
||||
if (id !in installedOf(context)) {
|
||||
val c = catalogComponent ?: run {
|
||||
repairFailures.value = repairFailures.value + (id to "Component is no longer available")
|
||||
Pending.drop(context, id, requestedRevision)
|
||||
return
|
||||
}
|
||||
val c = catalog[id] ?: run { Pending.drop(context, id); return }
|
||||
val started = System.currentTimeMillis()
|
||||
downloading.value = downloading.value + (id to WinComponents.Progress(id, "", WinComponents.Phase.DOWNLOAD, -1))
|
||||
val problem = try {
|
||||
@@ -275,28 +219,12 @@ object AutoComponents {
|
||||
} finally {
|
||||
downloading.value = downloading.value - id
|
||||
}
|
||||
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
|
||||
}
|
||||
if (problem != null) { Log.w(TAG, "wincomp: $id did not install: $problem"); Pending.drop(context, id); return }
|
||||
Log.i(TAG, "wincomp: $id installed in ${System.currentTimeMillis() - started}ms")
|
||||
}
|
||||
val games = synchronized(waiting) { waiting.remove(id).orEmpty() }
|
||||
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
|
||||
games.forEach { apply(context, it, asked, inSession) }
|
||||
Pending.drop(context, id)
|
||||
}
|
||||
|
||||
internal object Pending {
|
||||
@@ -305,37 +233,24 @@ 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>, revisions: Map<String, Int> = emptyMap()) {
|
||||
@Synchronized fun set(context: Context, key: String, ids: List<String>) {
|
||||
val games = read(context)
|
||||
if (ids.isEmpty()) games.remove(key) else games.put(key, JSONArray(ids))
|
||||
write(context, games, revisions)
|
||||
write(context, games)
|
||||
}
|
||||
|
||||
@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 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()) {
|
||||
@Synchronized fun add(context: Context, key: String, ids: List<String>) {
|
||||
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, revisions)
|
||||
write(context, games)
|
||||
}
|
||||
|
||||
@Synchronized fun drop(context: Context, id: String, throughRevision: Int? = null) {
|
||||
@Synchronized fun drop(context: Context, id: String) {
|
||||
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))
|
||||
@@ -343,29 +258,13 @@ object AutoComponents {
|
||||
write(context, games)
|
||||
}
|
||||
|
||||
private fun write(context: Context, games: JSONObject, required: Map<String, Int> = emptyMap()): Boolean {
|
||||
private fun write(context: Context, games: JSONObject) {
|
||||
val f = file(context)
|
||||
return runCatching {
|
||||
runCatching {
|
||||
f.parentFile?.mkdirs()
|
||||
val tmp = File(f.path + ".tmp")
|
||||
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
|
||||
tmp.writeText(JSONObject().put("games", games).toString())
|
||||
if (!tmp.renameTo(f)) { f.delete(); tmp.renameTo(f) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -430,76 +329,21 @@ object AutoComponents {
|
||||
}
|
||||
|
||||
|
||||
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 beforeLaunch(context: Context, game: Library.SteamGame) = queueGame(context, game, asked = false)
|
||||
|
||||
fun restoreMissing(context: Context): List<String> {
|
||||
val selections = WinComponents.selections(context)
|
||||
val catalog = (catalogOf(context, false) ?: catalogOf(context, true))?.associateBy { it.name }
|
||||
if (selections.isEmpty()) return emptyList()
|
||||
val catalog = (catalogOf(context, false) ?: catalogOf(context, true))?.associateBy { it.name } ?: return emptyList()
|
||||
val installed = installedOf(context)
|
||||
val lostFor = if (catalog == null) emptyMap() else selections.mapValues { (_, selection) ->
|
||||
val lostFor = 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()
|
||||
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) }
|
||||
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) }
|
||||
return lost
|
||||
}
|
||||
|
||||
@@ -516,7 +360,6 @@ 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,6 +54,21 @@ 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,7 +5,6 @@ import java.io.File
|
||||
object GameIdentifier {
|
||||
|
||||
private const val MAX_PARENT_DEPTH = 6
|
||||
internal const val SPACEWAR = 480
|
||||
|
||||
enum class Source { STEAM_APPID_TXT, STEAM_MANIFEST_ACF, STEAM_EMU_INI, GOG_INFO, PE_VERSION, FILENAME, NONE }
|
||||
enum class Confidence { HIGH, MEDIUM, LOW }
|
||||
@@ -93,7 +92,7 @@ object GameIdentifier {
|
||||
for (candidate in arrayOf(File(dir, "steam_appid.txt"), File(dir, "steam_settings/steam_appid.txt"))) {
|
||||
val id = readSmallTextFile(candidate)?.removePrefix("\uFEFF")?.trim()
|
||||
?.takeWhile { it.isDigit() }?.toIntOrNull()
|
||||
if (id != null && id > 0 && id != SPACEWAR) return id
|
||||
if (id != null && id > 0) return id
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -112,7 +111,7 @@ object GameIdentifier {
|
||||
for (f in candidates) {
|
||||
val text = readSmallTextFile(f) ?: continue
|
||||
val id = re.find(text)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
if (id != null && id > 0 && id != SPACEWAR) return id
|
||||
if (id != null && id > 0) return id
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -134,8 +133,7 @@ object GameIdentifier {
|
||||
val match = manifests.firstOrNull { m ->
|
||||
val text = readSmallTextFile(m) ?: return@firstOrNull false
|
||||
val installdir = vdfValue(text, "installdir")
|
||||
gameFolder != null && installdir != null && installdir.equals(gameFolder, ignoreCase = true) &&
|
||||
vdfValue(text, "appid")?.toIntOrNull() != SPACEWAR
|
||||
gameFolder != null && installdir != null && installdir.equals(gameFolder, ignoreCase = true)
|
||||
} ?: return null
|
||||
|
||||
val text = readSmallTextFile(match) ?: return null
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
|
||||
@@ -23,7 +22,6 @@ 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)
|
||||
@@ -89,14 +87,13 @@ 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)
|
||||
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 }
|
||||
if (arch == "x86_64") return listOfNotNull(root to "internal", card?.let { File(it.path, GameStorage.X64_CARD_DIR) to it.label })
|
||||
return listOfNotNull(
|
||||
root to "internal",
|
||||
card?.let { File(it.path) to it.label },
|
||||
) + added.map { it.host to it.label } + GameStorage.gamesFolderLibraries(context).map { it.host to it.label }
|
||||
) + GameStorage.gamesFolderLibraries(context).map { it.host to it.label }
|
||||
}
|
||||
|
||||
fun protonPrefix(
|
||||
@@ -104,21 +101,15 @@ 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
|
||||
return (listOfNotNull(preferredLibrary) + libraries).map(::prefixRoot)
|
||||
val roots = (listOfNotNull(preferredLibrary) + libraries).map(::prefixRoot)
|
||||
.distinctBy { runCatching { it.canonicalPath }.getOrDefault(it.absolutePath) }
|
||||
return roots.asSequence()
|
||||
.flatMap { root -> ids.asSequence().map { id -> File(root, "steamapps/compatdata/$id/pfx") } }
|
||||
.firstOrNull { it.isDirectory }
|
||||
}
|
||||
|
||||
fun steamGames(
|
||||
@@ -181,6 +172,7 @@ 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 {
|
||||
@@ -221,6 +213,7 @@ 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,6 +10,7 @@ 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)
|
||||
@@ -35,6 +36,7 @@ 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 {
|
||||
|
||||
Loaded 100 of 1142 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user