Compare commits

..
1820 changed files with 13327 additions and 183968 deletions

No files matched your search

+1 -1
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
ASEbrahim
FX192
K11MCH1
kingokksa
ovurrsl
+5 -9
View File
@@ -60,12 +60,11 @@ jobs:
- name: Build
run: |
set -euo pipefail
sudo apt-get update -qq && sudo apt-get install -y -qq zstd xz-utils gpgv gcc-aarch64-linux-gnu >/dev/null
KDE_KWIN_OVERRIDE="$PWD/out/kwin" DESKTOP_KDE_RELEASE_TAG='${{ inputs.tag }}' tools/desktop-kde/build.sh out
sudo apt-get update -qq && sudo apt-get install -y -qq zstd xz-utils gpgv >/dev/null
KDE_KWIN_OVERRIDE="$PWD/out/kwin" tools/desktop-kde/build.sh out
cp out/kwin/desktop-kde.kwin.txt out/
echo "size $(stat -c %s out/desktop-kde.tar.zst) sha256 $(cat out/desktop-kde.sha256)"
echo "language packs $(ls out/desktop-kde-i18n-*.tar.zst | wc -l)"
echo "pacman repository $(ls out/repo | wc -l) files, $(du -sm out/repo | cut -f1) MiB"
- uses: actions/upload-artifact@v4
with:
name: desktop-kde
@@ -78,7 +77,6 @@ jobs:
out/desktop-kde.sha256
out/desktop-kde.i18n.txt
out/desktop-kde-i18n-*.tar.zst
out/repo/
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
@@ -91,10 +89,8 @@ jobs:
echo "::error::$TAG is already published in DroidDeck-Components, and published tags are never replaced: use a new tag"
exit 1
fi
prerelease=false
case $TAG in *-rc*) prerelease=true ;; esac
gh release create "$TAG" --latest=false --prerelease="$prerelease" --title "$TAG" \
gh release create "$TAG" --latest=false --prerelease=false --title "$TAG" \
--notes "KDE Plasma desktop package for DroidDeck's Linux runtime ($GITHUB_REPOSITORY tools/desktop-kde, $GITHUB_SHA). size $(stat -c %s out/desktop-kde.tar.zst), sha256 $(cat out/desktop-kde.sha256). desktop-kde.packages.txt lists the signature-checked Arch Linux ARM packages it was built from." \
out/desktop-kde.tar.zst out/desktop-kde.sha256 out/desktop-kde.packages.txt out/desktop-kde.kwin.txt \
out/desktop-kde.i18n.txt out/desktop-kde-i18n-*.tar.zst out/repo/*
gh release edit "$TAG" --latest=false --prerelease="$prerelease"
out/desktop-kde.i18n.txt out/desktop-kde-i18n-*.tar.zst
gh release edit "$TAG" --latest=false --prerelease=false
+4
View File
@@ -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' }}
+4 -25
View File
@@ -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,95 +0,0 @@
name: Build Linux runtime (Ubuntu)
on:
push:
paths:
- 'tools/ubuntu/**'
- '.github/workflows/build-ubuntu-runtime.yml'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish as (e.g. ubuntu-r1); empty = artifact only'
required: false
default: ''
permissions:
contents: read
jobs:
debs:
runs-on: ubuntu-24.04-arm
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build Mesa with KGSL and the patched KWin
run: |
set -euo pipefail
docker run --rm -v "$PWD:/work" -w /work ubuntu:26.04 bash tools/ubuntu/build-debs.sh /work/out/debs
sudo chown -R "$(id -u):$(id -g)" out
- uses: actions/upload-artifact@v4
with:
name: ubuntu-runtime-debs
retention-days: 7
compression-level: 0
path: out/debs/
rootfs:
needs: debs
runs-on: ubuntu-24.04-arm
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: ubuntu-runtime-debs
path: out/debs
- 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: Refuse a published tag
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ steps.components-token.outputs.token }}
run: |
if gh release view '${{ inputs.tag }}' -R Droid-Deck/DroidDeck-Components >/dev/null 2>&1; then
echo "::error::${{ inputs.tag }} is already published, and published tags are never replaced: use a new tag"
exit 1
fi
- name: Build
run: |
set -euo pipefail
sudo apt-get update -qq && sudo apt-get install -y -qq zstd gpgv ubuntu-keyring >/dev/null
tag='${{ inputs.tag }}'
tools/ubuntu/build.sh out/debs out "${tag:-ubuntu-dev}"
- uses: actions/upload-artifact@v4
with:
name: ubuntu-runtime
retention-days: 7
compression-level: 0
path: |
out/*.tar.zst
out/*.sha256
out/*.packages.txt
- name: Publish
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
env:
GH_TOKEN: ${{ steps.components-token.outputs.token }}
GH_REPO: Droid-Deck/DroidDeck-Components
run: |
set -euo pipefail
TAG='${{ inputs.tag }}'
prerelease=false
case $TAG in *-rc*) prerelease=true ;; esac
gh release create "$TAG" --latest=false --prerelease="$prerelease" --title "$TAG" \
--notes "Ubuntu 26.04 LTS arm64 runtime for DroidDeck ($GITHUB_REPOSITORY tools/ubuntu, $GITHUB_SHA): size $(stat -c %s "out/$TAG.tar.zst"), sha256 $(cat "out/$TAG.sha256"). $TAG-desktop.tar.zst is the Kubuntu Plasma desktop built on top of it, holding only what the runtime does not already have: size $(stat -c %s "out/$TAG-desktop.tar.zst"), sha256 $(cat "out/$TAG-desktop.sha256"). The .packages.txt files list every installed package and version." \
"out/$TAG.tar.zst" "out/$TAG.sha256" "out/$TAG.packages.txt" \
"out/$TAG-desktop.tar.zst" "out/$TAG-desktop.sha256" "out/$TAG-desktop.packages.txt" out/debs/*.deb
gh release edit "$TAG" --latest=false --prerelease="$prerelease"
-31
View File
@@ -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
+29 -206
View File
@@ -8,8 +8,6 @@ on:
- '**/*.md'
- 'LICENSE'
- '.gitignore'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, ready_for_review ]
@@ -17,8 +15,6 @@ on:
- '**/*.md'
- 'LICENSE'
- '.gitignore'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
workflow_dispatch:
inputs:
rebuild_preloads:
@@ -32,8 +28,8 @@ permissions:
pull-requests: write
concurrency:
group: build-${{ github.event.pull_request.number || (github.event_name == 'push' && 'main') || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
group: build-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
source:
@@ -49,10 +45,6 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Pin and merge the APK source
id: merge
@@ -63,8 +55,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
@@ -88,10 +78,6 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
@@ -102,7 +88,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,28 +121,17 @@ 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
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
@@ -168,7 +142,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 +149,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 +212,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 +222,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,42 +249,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: Cache graphics driver inputs
uses: actions/cache@v4
with:
path: app/build/quest-inputs
key: quest-inputs-${{ hashFiles('tools/quest/build-driver.py') }}
- name: Prepare graphics driver variants
run: python3 tools/quest/build-driver.py "$ANDROID_HOME/ndk/$NDK_VERSION"
- name: Record and pack prepared components
@@ -330,18 +270,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,12 +283,7 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
submodules: recursive
- name: Check human authorship
env:
@@ -416,7 +344,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"
@@ -477,7 +404,7 @@ jobs:
- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
cache-cleanup: ${{ github.event_name == 'pull_request' && 'never' || 'on-success' }}
cache-cleanup: on-success
- name: Resolve the pinned Android NDK
run: bash tools/ci/resolve-ndk.sh
@@ -486,7 +413,6 @@ jobs:
id: build-label
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BUILD_REF: ${{ github.ref_name }}
BUILD_SHA: ${{ github.sha }}
@@ -505,7 +431,6 @@ jobs:
echo "label=$label" >> "$GITHUB_OUTPUT"
echo "DROIDDECK_SOURCE_COMMIT=${PR_HEAD_SHA:-$BUILD_SHA}" >> "$GITHUB_ENV"
echo "DROIDDECK_PR_NUMBER=${PR_NUMBER:-0}" >> "$GITHUB_ENV"
echo "DROIDDECK_PR_TITLE=${PR_TITLE//$'\n'/ }" >> "$GITHUB_ENV"
echo "Build: \`$label\`" >> "$GITHUB_STEP_SUMMARY"
- name: Rust toolchain for the store engines (aarch64-linux-android)
@@ -525,7 +450,7 @@ jobs:
~/.cargo/git/db
~/.cargo/bin/cargo-ndk
app/src/main/rust/stores/target
key: stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-${{ hashFiles('app/src/main/rust/stores/src/**', 'app/src/main/rust/stores/Cargo.toml', 'app/src/main/rust/stores/build.rs', 'app/src/main/rust/stores/.cargo/**', 'app/build.gradle') }}
key: stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-${{ hashFiles('app/src/main/rust/stores/amazon/**', 'tools/stores/**', 'app/src/main/rust/stores/src/**', 'app/src/main/rust/stores/Cargo.toml', 'app/src/main/rust/stores/build.rs', 'app/src/main/rust/stores/.cargo/**', 'app/build.gradle') }}
restore-keys: |
stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-
stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-
@@ -563,64 +488,11 @@ jobs:
- name: Cache checksum-pinned pack inputs
uses: actions/cache@v4
with:
path: |
~/.cache/droiddeck-pack-inputs
~/.cache/droiddeck-build
key: pack-inputs-v2-${{ hashFiles('tools/packs/build.sh', 'tools/packs/fetch-verified.sh', 'tools/packs/tracking/release.env', 'tools/powervr/release.json') }}
restore-keys: pack-inputs-v2-
path: ~/.cache/droiddeck-pack-inputs
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
- name: Note when the proot pack pin is behind this build
run: |
set -euo pipefail
# shellcheck disable=SC1091
source tools/packs/proot/release.env
sha=$(sha256sum app/build/packs/proot.tar.zst | cut -d' ' -f1)
if [ "$sha" != "$PROOT_PACK_SHA256" ]; then
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
run: bash tools/packs/build.sh app/build/packs
- name: Build
id: build
@@ -629,7 +501,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
@@ -645,6 +516,20 @@ jobs:
git diff --exit-code -- app/src/main/rust/stores/Cargo.lock \
|| { echo "::error::Cargo.lock changed during the build; commit the regenerated lockfile (uploaded as stores-cargo-lock)"; exit 1; }
- name: Store engines host unit tests
timeout-minutes: 10
continue-on-error: true
run: |
cd app/src/main/rust/stores
cargo test --locked 2>&1 | tail -60
- name: Amazon engine host tests
timeout-minutes: 10
run: |
set -euo pipefail
export CARGO_TARGET_DIR="$PWD/app/src/main/rust/stores/target"
cargo test --manifest-path app/src/main/rust/stores/amazon/Cargo.toml --locked
- name: Upload the regenerated Cargo.lock
if: failure()
uses: actions/upload-artifact@v4
@@ -732,66 +617,9 @@ jobs:
UNIT_TEST_OUTCOME: ${{ steps.unit-tests.outcome }}
TEST_DIAGNOSTICS_URL: ${{ steps.unit-test-diagnostics.outputs.artifact-url }}
APK_OUTCOME: ${{ steps.apk-upload.outcome }}
SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
run: bash tools/release/report-build.sh
store-tests:
name: Store engines host tests
needs: source
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 15
continue-on-error: true
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
env:
BUILD_BASE_SHA: ${{ needs.source.outputs.base-sha }}
run: |
set -euo pipefail
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:
XBOX_SOURCE_TREE: ${{ needs.source.outputs.tree-sha }}
run: test "$(git rev-parse 'HEAD^{tree}')" = "$XBOX_SOURCE_TREE"
- name: Rust toolchain
id: rust
run: echo "rustc=$(rustc -V | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache cargo registry and the host test target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
app/src/main/rust/stores/target
key: stores-host-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-${{ hashFiles('app/src/main/rust/stores/src/**', 'app/src/main/rust/stores/Cargo.toml', 'app/src/main/rust/stores/build.rs') }}
restore-keys: |
stores-host-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-
stores-host-v1-${{ runner.os }}-${{ runner.arch }}-${{ steps.rust.outputs.rustc }}-
- name: Store engines host unit tests
timeout-minutes: 10
continue-on-error: true
run: |
set -o pipefail
cd app/src/main/rust/stores
cargo test --locked 2>&1 | tail -60
python-tests:
name: Python and native regression tests
needs: source
@@ -803,10 +631,6 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
sparse-checkout: |
/*
!/docs/releases/media/
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
@@ -817,7 +641,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:
@@ -856,12 +679,12 @@ jobs:
timeout 10 /tmp/test-presenter-shutdown
- name: Steam import and game environment tests
run: bash tools/ci/run-python-tests.sh
run: python3 -m unittest discover -s tools/tests -p 'test_*.py'
checks:
name: Compile and test APK
needs: [build, python-tests]
if: ${{ !cancelled() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
-60
View File
@@ -1,60 +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 || steps.fork.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"
- name: Pick the fork runner
id: fork
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
labels='"ubuntu-24.04"'
gist=https://api.github.com/gists/0c7ed7ad6d6493de84f5123689c64ae6
body=$(curl -fsS --max-time 15 -H "Authorization: Bearer $GH_TOKEN" "$gist" 2>/dev/null || curl -fsS --max-time 15 "$gist" 2>/dev/null || echo '{}')
IDLE_AT=$(jq -r 'select(.owner.login == "maxjivi05") | .files["idle-at"].content // "0"' <<< "$body" 2>/dev/null || echo 0)
echo "Heartbeat: ${IDLE_AT:-none}"
age=-1
if [[ "${IDLE_AT:-0}" =~ ^[0-9]+$ ]] && [ "${IDLE_AT:-0}" -gt 0 ]; then
age=$(( $(date +%s) - IDLE_AT ))
fi
if [ "$age" -ge 0 ] && [ "$age" -lt 180 ]; then
labels='"droiddeck-fork"'
fi
echo "labels=$labels" >> "$GITHUB_OUTPUT"
echo "Compile runner: \`$labels\`" >> "$GITHUB_STEP_SUMMARY"
+2 -10
View File
@@ -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
+31 -103
View File
@@ -8,10 +8,6 @@ on:
description: Open pull request number
required: true
type: string
automatic_build_run_id:
description: Completed build run for automatic publishing
required: false
type: string
pull_request_target:
types: [labeled]
branches: [main]
@@ -31,47 +27,16 @@ concurrency:
queue: max
jobs:
dispatch-fork-builds:
if: >-
github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name != github.repository
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: write
pull-requests: read
steps:
- name: Find fork PRs for the completed build
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
RUN_SHA: ${{ github.event.workflow_run.head_sha }}
RUN_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
RUN_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
set -euo pipefail
owner=${RUN_REPO%%/*}
gh api --paginate --slurp -X GET "repos/$GITHUB_REPOSITORY/pulls" \
-f state=open -f base=main -f "head=$owner:$RUN_BRANCH" -f per_page=100 |
jq -r --arg sha "$RUN_SHA" --arg repo "$RUN_REPO" --arg branch "$RUN_BRANCH" '
.[][] | select(.head.sha == $sha and .head.repo.full_name == $repo and .head.ref == $branch) | .number' |
while IFS= read -r number; do
gh workflow run pr-release-publish.yml -R "$GITHUB_REPOSITORY" --ref main \
-f "pr_number=$number" -f "automatic_build_run_id=$RUN_ID"
done
authorize:
if: >-
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request_target' &&
(github.event.action != 'labeled' || github.event.label.name == 'publish-test')) ||
(github.event_name == 'pull_request_target' && github.event.label.name == 'publish-test') ||
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name == github.repository &&
github.event.workflow_run.pull_requests[0] != null)
runs-on: ubuntu-24.04
timeout-minutes: 150
timeout-minutes: 45
outputs:
pr_number: ${{ steps.gate.outputs.pr_number }}
run_id: ${{ steps.gate.outputs.run_id }}
@@ -85,7 +50,6 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: The packages a signed build ships as
id: variants
@@ -97,14 +61,22 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
REQUESTER: ${{ github.actor }}
PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number || inputs.pr_number }}
ACTION: ${{ github.event.action }}
BUILT_SHA: ${{ github.event.workflow_run.head_sha }}
AUTO_RUN: ${{ inputs.automatic_build_run_id }}
run: |
set -euo pipefail
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number '$PR_NUMBER'"; exit 1; }
skip() { echo "::notice::Not publishing PR #$PR_NUMBER: $1"; echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
if [ "$EVENT_NAME" = pull_request_target ] && [ "$ACTION" = labeled ]; then
if [ "$EVENT_NAME" = workflow_run ]; then
echo "automatic=true" >> "$GITHUB_OUTPUT"
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
[ "$(jq -r .state <<<"$pr")" = open ] || skip "it is not open"
[ "$(jq -r .draft <<<"$pr")" = false ] || skip "it is a draft"
[ "$(jq -r .base.ref <<<"$pr")" = main ] || skip "it does not target main"
[ "$(jq -r .head.repo.full_name <<<"$pr")" = "$GITHUB_REPOSITORY" ] || skip "it comes from a fork"
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$pr" >/dev/null && skip "it is labelled no-test-build"
[ "$(jq -r .head.sha <<<"$pr")" = "$BUILT_SHA" ] || skip "a newer push is building"
fi
if [ "$EVENT_NAME" = pull_request_target ]; then
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$REQUESTER/permission" --jq .permission)
case "$permission" in
admin|maintain|write) ;;
@@ -113,59 +85,29 @@ jobs:
fi
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
jq -e '.state == "open" and .draft == false and .base.ref == "main"' <<<"$pr" >/dev/null ||
skip "it must be open, ready for review, and target main"
if [ "$EVENT_NAME" = workflow_run ]; then
[ "$(jq -r .head.repo.full_name <<<"$pr")" = "$GITHUB_REPOSITORY" ] || skip "it comes from a fork"
[ "$(jq -r .head.sha <<<"$pr")" = "$BUILT_SHA" ] || skip "a newer push is building"
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$pr" >/dev/null && skip "it is labelled no-test-build"
echo "automatic=true" >> "$GITHUB_OUTPUT"
elif [ "$EVENT_NAME" = workflow_dispatch ] && [ -n "$AUTO_RUN" ]; then
[[ "$AUTO_RUN" =~ ^[1-9][0-9]*$ ]] || { echo '::error::Invalid automatic build run ID'; exit 1; }
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$pr" >/dev/null && skip "it is labelled no-test-build"
git fetch --no-tags origin main
opener=$(jq -r .user.login <<<"$pr")
git show origin/main:.github/trusted-test-builds.txt | grep -Fixq -- "$opener" || skip "PR opener @$opener is not trusted for automatic signing"
echo "automatic=true" >> "$GITHUB_OUTPUT"
fi
{ echo "::error::PR #$PR_NUMBER must be open, ready for review, and target main"; exit 1; }
head=$(jq -r '.head.sha' <<<"$pr")
base=$(jq -r '.base.sha' <<<"$pr")
run_id=
for attempt in $(seq 1 240); do
latest=$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/build.yml/runs?event=pull_request&head_sha=$head&per_page=100" |
jq -c --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" '
[.workflow_runs[] | select(.head_sha == $head and .head_repository.full_name == $repo and .head_branch == $ref)] |
sort_by([.created_at, .run_attempt]) | last // empty')
if [[ -z "$latest" && "$attempt" == 1 ]]; then
git fetch --no-tags origin main
git fetch --no-tags origin "refs/pull/$PR_NUMBER/head"
git diff --no-renames --name-only -z "$base...FETCH_HEAD" > "$RUNNER_TEMP/pr-changed-paths"
excluded=true
while IFS= read -r -d '' path; do
case "$path" in
*.md|LICENSE|.gitignore) ;;
*) excluded=false; break ;;
esac
done < "$RUNNER_TEMP/pr-changed-paths"
[ "$excluded" = false ] || skip "its changes do not trigger the APK build"
fi
for attempt in $(seq 1 60); do
latest=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \
--event pull_request --commit "$head" \
--json databaseId,headSha,status,conclusion,createdAt,attempt -L 100 |
jq -c --arg head "$head" '[.[] | select(.headSha == $head)] |
sort_by([.createdAt, .attempt]) | last // empty')
if [[ -n "$latest" && $(jq -r .status <<<"$latest") == completed ]]; then
conclusion=$(jq -r .conclusion <<<"$latest")
if [[ "$conclusion" == success ]]; then
run_id=$(jq -r .id <<<"$latest")
if [ -n "$AUTO_RUN" ] && [ "$run_id" != "$AUTO_RUN" ]; then
skip "a newer build exists"
fi
break
fi
[[ "$conclusion" == action_required ]] || { echo "::error::The latest PR build is not green ($conclusion)"; exit 1; }
[[ "$conclusion" == success ]] || { echo "::error::The latest PR build is not green ($conclusion)"; exit 1; }
run_id=$(jq -r .databaseId <<<"$latest")
break
fi
status=not-started
[[ -z "$latest" ]] || status=$(jq -r .status <<<"$latest")
echo "Waiting for the PR build of ${head:0:7} to finish ($status)"
sleep 30
done
[[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 120 minutes'; exit 1; }
[[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; }
run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id")
jq -e --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" '
.event == "pull_request" and .head_sha == $head and .conclusion == "success" and
@@ -195,13 +137,6 @@ jobs:
.head.sha == $head and .base.sha == $base
' <<<"$current" >/dev/null ||
{ echo "::error::PR #$PR_NUMBER moved on while its build was checked; publish again"; exit 1; }
if [ "$EVENT_NAME" = workflow_run ] || { [ "$EVENT_NAME" = workflow_dispatch ] && [ -n "$AUTO_RUN" ]; }; then
jq -e 'any(.labels[]; .name == "no-test-build")' <<<"$current" >/dev/null && skip "it is labelled no-test-build"
if [ "$EVENT_NAME" = workflow_dispatch ]; then
git fetch --no-tags origin main
git show origin/main:.github/trusted-test-builds.txt | grep -Fixq -- "$(jq -r .user.login <<<"$current")" || skip "the PR opener is no longer trusted"
fi
fi
{
echo "pr_number=$PR_NUMBER"
echo "run_id=$run_id"
@@ -336,24 +271,17 @@ jobs:
echo "release=$release" >> "$GITHUB_OUTPUT"
echo "apk=$GITHUB_SERVER_URL/$repo/releases/download/$tag/$apk" >> "$GITHUB_OUTPUT"
- name: Whether the PR opener is trusted
id: trusted-opener
- name: Whether the PR's branch is in this repo
id: same-repo
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
run: |
pr=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
opener=$(jq -r .user.login <<<"$pr")
git fetch --no-tags origin main
if [ "$(jq -r .head.repo.full_name <<<"$pr")" = "$GITHUB_REPOSITORY" ] ||
git show origin/main:.github/trusted-test-builds.txt | grep -Fixq -- "$opener"; then
echo "trusted=true" >> "$GITHUB_OUTPUT"
else
echo "trusted=false" >> "$GITHUB_OUTPUT"
fi
head_repo=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .head.repo.full_name)
echo "same=$([ "$head_repo" = "$GITHUB_REPOSITORY" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
- name: Clear the publish request label
if: ${{ github.event_name == 'pull_request_target' && github.event.action == 'labeled' && steps.publish.outcome == 'success' }}
if: ${{ github.event_name == 'pull_request_target' && steps.publish.outcome == 'success' }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ needs.authorize.outputs.pr_number }}
@@ -367,12 +295,12 @@ jobs:
HEAD_SHA: ${{ needs.authorize.outputs.head_sha }}
RELEASE: ${{ steps.publish.outputs.release }}
APK: ${{ steps.publish.outputs.apk }}
TRUSTED_OPENER: ${{ steps.trusted-opener.outputs.trusted }}
SAME_REPO: ${{ steps.same-repo.outputs.same }}
run: |
set -euo pipefail
short=${HEAD_SHA:0:7}
mark='<!-- droiddeck-pr-release -->'
if [ "$TRUSTED_OPENER" = true ]; then
if [ "$SAME_REPO" = true ]; then
NEXT='Each green build of a new push is signed and published here on its own. Label the PR `no-test-build` to stop that.'
else
NEXT='Pushes to this PR do not re-sign automatically. After a new green build, a maintainer can add the `publish-test` label again.'
-57
View File
@@ -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"
+7 -1
View File
@@ -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 amazon; 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)
+1 -3
View File
@@ -13,10 +13,9 @@ 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/rust/amazon/target/
app/src/main/assets/droiddeck-esync/
.kotlin/
@@ -34,5 +33,4 @@ __pycache__/
.signing.env
app/src/main/assets/directaudio/
app/src/main/assets/hybris/
/tools/vr/out/
-6
View File
@@ -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
-4
View File
@@ -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
+3 -3
View File
@@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
DroidDeck brings the SteamOS experience to Android
Copyright (C) 2026 DroidDeck contributors
Pluvia is an unofficial Steam client app for Android
Copyright (C) 2024 Oxters Wyzgowski
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
DroidDeck Copyright (C) 2026 DroidDeck contributors
Pluvia Copyright (C) 2024 Oxters Wyzgowski
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
+25 -129
View File
@@ -5,158 +5,54 @@
</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 proprietary components. 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>
</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>
<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>
</p>
> Note: DroidDeck does not have a stand-alone website. Do not click on any download links from websites claiming to be the DroidDeck team.
<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>
</p>
## Requirements and install
<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>
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.
> DroidDeck has no website. Only download it from this repository's [Releases](https://github.com/Droid-Deck/DroidDeck/releases).
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.
<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>
## Smart glasses
## Install
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.
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.
## Community
Smart glasses setup is in the [support matrix](docs/smart-glasses-support.md). Release notes are in [docs/releases](docs/releases).
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**.
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. 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. 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. 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 is already on through Proton. The proot fast path picks the guest name of a host file bound under several names in the order [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later) sorts its bindings, so links and `/proc/self/fd` read back as they do through proot. The session's [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) opens its sockets with the per-socket access settings documented in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions through its `access.rules`. Wine write watches use the userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later): libblsession gives it the arm64 syscall number and answers its `PAGEMAP_SCAN` from `/proc/self/pagemap` where Android refuses the ioctl. Wine on arm64 sets a thread's full context by raising `SIGUSR2` from `syscall_dispatcher_return_slowpath` in [Wine](https://gitlab.winehq.org/wine/wine)'s `dlls/ntdll/unix/signal_arm64.c` (LGPL-2.1-or-later); libblsession runs that handler in place on a frame laid out as the [Linux](https://github.com/torvalds/linux) kernel's `arch/arm64/kernel/signal.c` builds it and returns through `rt_sigreturn`, so proot does not stop the thread for it. Wine's process list reads each process's memory counters from `/proc/<pid>/status` in `fill_vm_counters` ([Wine](https://gitlab.winehq.org/wine/wine) `dlls/ntdll/unix/process.c`, LGPL-2.1-or-later), so libblsession gives those reads the kernel's file instead of a copy with `TracerPid` cleared. droiddeck-ntsync is checked against the object, wait and error semantics documented for the [Linux](https://github.com/torvalds/linux) ntsync driver in `Documentation/userspace-api/ntsync.rst` (GPL-2.0). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
<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.
- Zink on PowerVR keeps colour textures in colour with Icenowy Zheng's upstream [Mesa](https://gitlab.freedesktop.org/mesa/mesa) fix d55418dae7 (MIT), carried as a runtime patch.
- Games and AppImage, Flatpak and Snap programs such as emulators use [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's per render pass memory choice. The Steam client and Chromium-based apps keep `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>
- 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).
</details>
<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>
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.
+11 -62
View File
@@ -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()) {
@@ -77,10 +78,6 @@ def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"')
def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull()
?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: ''
def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' }
def prTitle = providers.environmentVariable('DROIDDECK_PR_TITLE').getOrNull() ?: ''
def buildBranch = providers.environmentVariable('GITHUB_HEAD_REF').getOrNull()?.trim()
?: gitValue(['branch', '--show-current']) ?: ''
def escapeJava = { String value -> value.replace('\\', '\\\\').replace('"', '\\"').replace('\n', ' ').replace('\r', ' ') }
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
def commitTime = (gitValue(['show', '-s', '--format=%ct', '--end-of-options', sourceCommit]) ?: '0').with { it.isLong() ? it : '0' }
@@ -90,16 +87,10 @@ def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
def trackingPack = rootProject.file('tools/packs/tracking/release.env').readLines()
.findAll { it.contains('=') && !it.startsWith('#') }
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
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', 'amazon'].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")
if (name == 'proot' && prootPackPin.PROOT_PACK_SHA256) {
return [(name): [sha256: prootPackPin.PROOT_PACK_SHA256, size: (prootPackPin.PROOT_PACK_SIZE ?: '0') as long]]
}
return [(name): [sha256: '', size: 0L]]
}
def digest = java.security.MessageDigest.getInstance('SHA-256')
@@ -131,13 +122,11 @@ android {
applicationId "com.droiddeck.launcher"
minSdkVersion 26
targetSdkVersion 28
versionCode 18
versionName "0.7.0"
versionCode 15
versionName "0.6.0"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
buildConfigField "String", "PR_TITLE", "\"${escapeJava(prTitle)}\""
buildConfigField "String", "BUILD_BRANCH", "\"${escapeJava(buildBranch)}\""
buildConfigField "boolean", "CI_BUILD", "${ciBuild}"
buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\""
buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L"
@@ -205,7 +194,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 +205,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,8 +224,8 @@ 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
if (logTarget != null) systemProperty 'robolectric.logging', logTarget
}
@@ -249,11 +240,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'))
@@ -276,7 +262,6 @@ def runtimeAssetsDir = layout.buildDirectory.dir('runtimeAssets')
def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
dependsOn(stageSessionScripts)
from(layout.projectDirectory.dir('src/main/assets')) { exclude 'pulseaudio.tzst' }
from(rootProject.file('tools/linuxfs/innoextract'))
from(layout.buildDirectory.dir('prepared-assets'))
into(runtimeAssetsDir)
}
@@ -292,28 +277,10 @@ if (uvcDir) {
}
tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) }
def stageGogNotices = tasks.register('stageGogNotices', Copy) {
from(layout.projectDirectory.dir('src/main/rust/stores/vendor/xdelta3')) { include 'LICENSE', 'README.droiddeck.md' }
into(layout.buildDirectory.dir('gogNotices/licenses/xdelta3'))
}
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)
@@ -327,10 +294,9 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
description = 'Cross-compiles the store download engines (libdroiddeckstores.so) with cargo-ndk.'
onlyIf { !skipRust }
inputs.dir(rustCrateDir.dir('src'))
inputs.dir(rustCrateDir.dir('vendor'))
inputs.file(rustCrateDir.file('build.rs'))
inputs.files(rustCrateDir.file('Cargo.toml'), rustCrateDir.file('Cargo.lock'))
inputs.property('ndkVersion', android.ndkVersion)
inputs.property('storeFeatures', 'gog,epic')
if (!skipRust) inputs.property('rustToolchain', providers.exec {
environment 'PATH', "${System.getProperty('user.home')}/.cargo/bin${File.pathSeparator}${System.getenv('PATH')}"
commandLine 'rustup', 'run', 'stable', 'rustc', '-Vv'
@@ -346,26 +312,11 @@ def buildRustStores = tasks.register('buildRustStores', Exec) {
}.standardOutput.asText.get().trim()
commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26',
'--output-dir', rustJniLibsDir.get().asFile.absolutePath,
'build', '--release', '--locked'
'build', '--release', '--locked', '--no-default-features', '--features', 'gog,epic'
}
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'
@@ -387,11 +338,9 @@ dependencies {
implementation 'androidx.camera:camera-camera2:1.4.2'
implementation 'androidx.camera:camera-lifecycle:1.4.2'
implementation 'androidx.webkit:webkit:1.12.1'
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'
+13 -145
View File
@@ -2,25 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -37,10 +18,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.CAMERA" />
@@ -56,7 +34,6 @@
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="false"
android:allowAudioPlaybackCapture="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
@@ -69,42 +46,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"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</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>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<meta-data android:name="com.oculus.supportedDevices" android:value="quest2|quest3|quest3s" />
<activity android:name=".QuestXrActivity" android:exported="true" android:launchMode="singleTask"
android:permission="android.permission.DUMP"
@@ -119,23 +60,11 @@
</intent-filter>
</activity>
<activity
android:name=".PairingGuideActivity"
android:exported="false"
android:launchMode="singleTask"
android:taskAffinity="${applicationId}.pairingguide"
android:excludeFromRecents="true"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden|navigation"
android:theme="@style/AppTheme" />
<activity
android:name=".FrontEndActivity"
android:exported="true"
android:screenOrientation="user"
android:screenOrientation="sensorLandscape"
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" />
@@ -186,7 +115,7 @@
android:excludeFromRecents="true"
android:noHistory="true"
android:taskAffinity="${applicationId}.home"
android:screenOrientation="user"
android:screenOrientation="sensorLandscape"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -195,37 +124,26 @@
</intent-filter>
</activity>
<activity
android:name=".PinRequestActivity"
android:exported="true"
android:excludeFromRecents="true"
android:taskAffinity="${applicationId}.pin"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
<intent-filter>
<action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT" />
<action android:name="android.content.pm.action.CONFIRM_PIN_APPWIDGET" />
</intent-filter>
</activity>
<activity
android:name=".sync.SyncthingWebActivity"
android:name=".input.ControllerEditorActivity"
android:exported="false"
android:theme="@style/SessionTheme"
android:screenOrientation="sensorLandscape"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".files.FileManagerActivity"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
<activity
android:name=".files.FileManagerActivity"
android:exported="false"
android:screenOrientation="user"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
<activity
android:name=".files.FilePickerActivity"
android:exported="false"
android:screenOrientation="user"
android:screenOrientation="sensorLandscape"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
@@ -233,31 +151,10 @@
android:process=":xboxlogin" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
<activity android:name=".stores.xbox.XboxCloudPlayerActivity" android:exported="false"
android:screenOrientation="sensorLandscape" android:hardwareAccelerated="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|density|navigation|uiMode"
android:theme="@style/AppTheme" />
<service android:name=".stores.xbox.XboxCloudTokenService" android:exported="false" />
<activity android:name=".stores.xbox.XboxNativePlayerActivity" android:exported="false"
android:process=":xcloudnative" android:screenOrientation="sensorLandscape" android:hardwareAccelerated="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|density|navigation|uiMode"
android:theme="@style/AppTheme" />
<activity android:name=".stores.xbox.XboxCloudActivity" android:exported="false"
android:process=":xboxcloud" android:launchMode="singleTop" android:screenOrientation="sensorLandscape"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|density|navigation|uiMode"
android:theme="@style/AppTheme" />
<activity android:name=".stores.xbox.XboxCloudSettingsActivity" android:exported="false"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
<activity
android:name=".stores.StoreLoginActivity"
android:exported="false"
android:screenOrientation="user"
android:screenOrientation="sensorLandscape"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
android:theme="@style/AppTheme" />
@@ -280,26 +177,6 @@
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".ui.DualScreenActivity"
android:exported="false"
android:excludeFromRecents="true"
android:taskAffinity="${applicationId}.dualscreen"
android:theme="@style/SessionTheme"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".ui.SessionTypingActivity"
android:exported="false"
android:excludeFromRecents="true"
android:noHistory="true"
android:taskAffinity="${applicationId}.typing"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
android:resizeableActivity="true"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".gpu.SteamVrRestartActivity"
android:exported="false"
@@ -314,11 +191,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 +201,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"
@@ -3,52 +3,31 @@
"lxqt-r1": "c2bb860b3b1688287451e2495be5db313ec07f8c8af501dfac2768a9f417bc86",
"desktop-kde-r1": "55747e75d0ec055ab7ea1e92a757f3be227cc3205bb9101f5b53cadef6318579",
"desktop-kde-r2": "9ba832677b498654136c69d8c8330434d17e4d53cd099acac9bde3ba429b35bf",
"emulators-r1": "a1f4806a9663d28f424b4cab1277e0ede80a92e69de5bc063cdc8b45af220164",
"desktop-kde-r3": "8b7142fcec8d86143ae39fc14ffc86907d30d9a5b5a45f49cb1b55109d6fc4eb"
"emulators-r1": "a1f4806a9663d28f424b4cab1277e0ede80a92e69de5bc063cdc8b45af220164"
},
"desktopApplications": {
"7zip-26.05-1-aarch64.pkg.tar.xz": "17569bafbc5bd1a044ca1db164b96cb15857a22a3575f36679de46e693ae285e",
"ark-26.08.2-1-aarch64.pkg.tar.xz": "b93cc90c4fc8a6cb0247fe8fb0f9de1c2074f5173fa2735d3d1959c9162c08f2",
"breeze-6.8.1-1-aarch64.pkg.tar.xz": "70a2ff3afd41602f184814af5c6e298ff84ef7cb7e5880ca7cb4f6fb28521511",
"breeze-gtk-6.8.1-1-any.pkg.tar.xz": "e92b9acf3155db22d4a288a43e8d95c20c72b8d8cffd028dd6800eb01a304bf0",
"breeze-icons-6.31.0-1-aarch64.pkg.tar.xz": "b252ef02395621b5891d72c5f7fccd1a1ead1ce770886e9fced4bf21e1130096",
"discover-6.8.1-1-aarch64.pkg.tar.xz": "64cfca615e7c18530c684325eb36a5c45e18c63b31f651335474f53b0178958c",
"dolphin-26.08.2-1-aarch64.pkg.tar.xz": "4b5f7a9212c88d47146b374742f67f5620ab128ad893d945721bb43501066b70",
"elisa-26.08.2-1-aarch64.pkg.tar.xz": "fd2df84a2905b59fba0ab1b7fadafe567c5a1dfd44d53ae4cd4996f683b83ba2",
"filelight-26.08.2-1-aarch64.pkg.tar.xz": "17f39435104baf0743ab508bdde6e8bd1b3417b025b67ca8f343032d2fdef3e6",
"firefox-158.0.1-1-aarch64.pkg.tar.xz": "4940e14c05522bfee8df901e1cfe81f56036c82e8f3d95f9f7a2a12e6b38d232",
"flatpak-kcm-6.8.1-1-aarch64.pkg.tar.xz": "3145601002fb87d61307949a3011a8f27a7b1e9851fc59835460f934f8e00098",
"git-2.56.0-1-aarch64.pkg.tar.xz": "b152a20edb215e98cada4b2dfbd02a9e925bda0b2de6ac1441e9e60b6698d3b5",
"gwenview-26.08.2-1-aarch64.pkg.tar.xz": "02aae3973ec0563022c89e312ce7696d05d33514101ae2a01df3275717929de2",
"htop-3.5.3-1-aarch64.pkg.tar.xz": "a58899bd2596d55575493509b3a5aa6f53075cb3d88bb6c712088c57068d9eb7",
"kate-26.08.2-1-aarch64.pkg.tar.xz": "89c21ad8e62d20961b13f8fac8d9f4d358b1a3feebed243f6412e4ad996d5e9e",
"kcalc-26.08.2-1-aarch64.pkg.tar.xz": "79b3303ccc5ae956e97313439456243e59f0b79a8166ca91606a25d1c9e41029",
"kde-cli-tools-6.8.1-1-aarch64.pkg.tar.xz": "b052e9c25e7d6896be67846696f14c9e00430c0445f358ee64cf2a8ea16aa2eb",
"kde-gtk-config-6.8.1-1-aarch64.pkg.tar.xz": "3095da034ac5fe8594165e3396f61f083b0c011cd96807146ddfb53690118608",
"kfind-26.08.2-1-aarch64.pkg.tar.xz": "b4108cc3bd9efd62970379aadb3cb22e99dd1f54873ff432392edd0a833bb393",
"kinfocenter-6.8.1-1-aarch64.pkg.tar.xz": "758882ef13a166205acab0e6bb96ce756d638d91e501db400cb579b9e1331d31",
"konsole-26.08.2-1-aarch64.pkg.tar.xz": "0cf27f69d1792a094d4a935f578c82f8151b2d359198d9cd32bfdbd8dec544e4",
"kscreen-6.8.1-1-aarch64.pkg.tar.xz": "72c948dfdebfef305623527de27274baf1a2328c87e71033fd852e386d9fdf5c",
"kwin-6.8.1-2-aarch64.pkg.tar.xz": "726c98a3f8682b487c2b313633edbd80aa8b20ff657a6b63bbb1b73454f6e0c4",
"less-1:710-1-aarch64.pkg.tar.xz": "8e1d4cbecf6d2ae734cfc1c5495367c0a9245f92dfd2bc03d4d0ab14f8b26ff2",
"man-db-2.13.1-2-aarch64.pkg.tar.xz": "67515751f848717e1f6d3470f57ad2ea6900ee07618fd2726b6fcaa695687e80",
"man-pages-6.19-1-any.pkg.tar.xz": "69df38f1a1aa35240d239ff82a2e067950ac394f23b8a6c7307580ccd7e95f07",
"ark-26.08.1-1-aarch64.pkg.tar.xz": "93e2c2b75ff20cd29b4f312ed1979ef6f27255b93232b93bfcdb31239903c1d4",
"breeze-6.7.5-1-aarch64.pkg.tar.xz": "1d7f4eb9d8d234caaea23621909f5233d19462c2f058afeeb54bed7b81bd03bd",
"breeze-gtk-6.7.5-1-any.pkg.tar.xz": "a0f59bc4fc426879c8f8a97f6dcc707fb7d1b52913da5eb7e9db83777b6957b7",
"breeze-icons-6.30.0-1-aarch64.pkg.tar.xz": "4bae4689032501156687a227024e56554820747644953c67fc8d2e2b6b0b045a",
"dolphin-26.08.1-1-aarch64.pkg.tar.xz": "fef738ab8132d3eba1156b98c116699ce0a5efcb4cb462efbacdcf4c47f79a23",
"firefox-157.0.1-1-aarch64.pkg.tar.xz": "d142fdb8a339a65eca7cea6a2bba5b29202386166abd9d59851d6757c9eb2163",
"gwenview-26.08.1-1-aarch64.pkg.tar.xz": "a9dc7d5ab73fa677905d5a57248b1b9b404b6b16e862958cc7443d387f684a47",
"kate-26.08.1-1-aarch64.pkg.tar.xz": "acf50710042746f243a3b219a1ae0f788e338f3dd9d75093e8bb671a618b83b6",
"kde-cli-tools-6.7.5-1-aarch64.pkg.tar.xz": "af466398a912a994a97fadfe46aff401d88a90d7fdce7e843dc4218098fa38ac",
"kde-gtk-config-6.7.5-1-aarch64.pkg.tar.xz": "ba6cb4cb01dbb3f2c247f2f6973ede3b64c0849f89340217b51dfbf11cb8cb5c",
"kinfocenter-6.7.5-1-aarch64.pkg.tar.xz": "f3d5416db96b50f9a5b8a284d82ae21d834a8cce646010abdd7087491c399b0b",
"konsole-26.08.1-1-aarch64.pkg.tar.xz": "1ecf87c0d11d620ebe4aeafb02788e0d5f4fe9019ad9c5d9f1011c43237dd5ec",
"kscreen-6.7.5-1-aarch64.pkg.tar.xz": "8fbaa6fedf93edeba021a5416b551663654919b27c2ee237d696ec0b0ce7d5b7",
"kwin-6.7.5-1-aarch64.pkg.tar.xz": "a7535710c74b90359f80306134b2b7581132cbac508dc84b7e07ef275cc8c3fe",
"mpv-1:0.41.0-6-aarch64.pkg.tar.xz": "6c89d00736820210a3c7f32af4cf95d155191d463f9df8bd22137c45f30a554a",
"okular-26.08.2-1-aarch64.pkg.tar.xz": "aef87cf74730d2345092184d7e9b14a0068ba4030de58564aa770506175cbf06",
"plasma-desktop-6.8.1-1-aarch64.pkg.tar.xz": "4b24662a2059b21ca98b5b35b72216f22c9e316057247ea018bf6318b07cfdb6",
"plasma-integration-6.8.1-1-aarch64.pkg.tar.xz": "514f7d2a96370814b9374d7c52e1873257af6e18190d130e5a8d3d6d1fffdc54",
"plasma-pa-6.8.1-1-aarch64.pkg.tar.xz": "13dff66d30072744c7870bd1a294ae96f0fee086a1533b8052d7dfc038d57c45",
"plasma-systemmonitor-6.8.1-1-aarch64.pkg.tar.xz": "551b7c1f24cd5df64de4f5ba199bc71c00e2924c16a8241b21c020584794e10d",
"plasma-workspace-6.8.1-1-aarch64.pkg.tar.xz": "4c713990aeab118d50865c818f9689ae7637dc6556e4a0e2fec5e0b348cdcd29",
"spectacle-1:6.8.1-1-aarch64.pkg.tar.xz": "0d697803bd0135701688be3869d4d99eec6b8b43ca761610da73f44cb77bfa5c",
"systemsettings-6.8.1-1-aarch64.pkg.tar.xz": "db528089fa328d67dadb50be3b822858238785d8c659a9ef95389b993efcb234",
"unrar-1:7.3.1-1-aarch64.pkg.tar.xz": "9883e19b1f1adfa430285191b1f96db4cc208608baebd955f3fe9fee92cd3bdd",
"unzip-6.0-23-aarch64.pkg.tar.xz": "edc91c7e4e400f20345439744262d1ceded104c78d940659a6b9876076ef33fc",
"wget-1.25.0-6-aarch64.pkg.tar.xz": "e2bf0067cca93877c737c7d48bc36fff32e184c7874829429b9473c10a12dd39",
"wl-clipboard-1:2.3.0-1-aarch64.pkg.tar.xz": "723a63b140e00085a533ebb1ec36fa9305b5e6cbd8fb80c0521fbeff77a72982",
"xclip-0.13-6-aarch64.pkg.tar.xz": "772ec9336b679b0ef277d1ef0b69b74777467239e16ec845af00c07fa849d166",
"xdg-desktop-portal-kde-6.8.1-1-aarch64.pkg.tar.xz": "8098c0ef0454335bb7c806049d66977c512657346222606737d72a7ae1de3146",
"zip-3.0-14-aarch64.pkg.tar.xz": "9887f177544021809d065d0298514e98f76b55f9120119f748f5a28c8bb51994"
"plasma-desktop-6.7.5-1-aarch64.pkg.tar.xz": "b266dd42cea965b5ab1cc1ae600877e9d3bc83ec642c61fe1669c4457e641a55",
"plasma-integration-6.7.5-1-aarch64.pkg.tar.xz": "daa96da14d9a4d12c0d9c0341e1be6836a9a437c469d61da5e9844d51e2606cf",
"plasma-pa-6.7.5-1-aarch64.pkg.tar.xz": "02dbefe9d3e0069b2bb37cac2a6be18b8f0a1887c87704a0716e7a9268eadcd8",
"plasma-workspace-6.7.5-1-aarch64.pkg.tar.xz": "3a1455857afaa9ccf342ab6167f0f15794ee9d248b0925398ea6f62f12730a86",
"spectacle-1:6.7.5-1-aarch64.pkg.tar.xz": "0c9837412ea6119edef18bcb0a8fceb74634cb35be0ca78b3e1dd1ff2443866e",
"systemsettings-6.7.5-1-aarch64.pkg.tar.xz": "b02b6a726857d5125c92d1ec95379216c7a3bc1f187a3b46e88c0d056b29ce70",
"xdg-desktop-portal-kde-6.7.5-1-aarch64.pkg.tar.xz": "2c57d17d34d577023eb9cd7b0fca1ba9163549afe60450f6271aa148af099ef2"
},
"legacyApplications": {
"featherpad-1.6.3-1-aarch64.pkg.tar.xz": "fcac55215815c0b712453f9bcc09abfe4b518e36025a111a7a7598ef04913f3d",
@@ -1,3 +1,2 @@
desktop-kde-r1
desktop-kde-r2
desktop-kde-r3
-18
View File
@@ -1,18 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<style>
html, body { margin: 0; height: 100%; background: #000; overflow: hidden; }
#player { position: absolute; inset: 0; }
#status { position: absolute; left: 0; right: 0; bottom: 12%; text-align: center; color: #ddd;
font: 16px sans-serif; text-shadow: 0 0 4px #000; pointer-events: none; }
</style>
</head>
<body>
<div id="player"></div>
<div id="status"></div>
<script src="player.js"></script>
</body>
</html>
-187
View File
@@ -1,187 +0,0 @@
/* DroidDeck Xbox Cloud Gaming player. Includes Greenlight's player (c) UnknownSKL (Jim Kroon), MIT. */
(()=>{var _=class{_player;_dataChannel;constructor(e){this._player=e,this._dataChannel=this._player._peerConnection.createDataChannel(this.getChannelName(),this.getChannelConfig()),this._dataChannel.onopen=this.onOpen.bind(this),this._dataChannel.onmessage=this.onMessage.bind(this),this._dataChannel.onclosing=this.onClosing.bind(this),this._dataChannel.onclose=this.onClose.bind(this),this._dataChannel.onerror=this.onError.bind(this)}getChannelName(){return"channel"}getChannelConfig(){return{}}onOpen(e){console.log(this.getChannelName(),"Opening channel:",e)}onMessage(e){console.log(this.getChannelName(),"Message channel:",e)}onClosing(e){console.log(this.getChannelName(),"Closing channel:",e)}onClose(e){console.log(this.getChannelName(),"Closed channel:",e)}onError(e){console.log(this.getChannelName(),"Error channel:",e)}send(e){if(this._dataChannel.readyState!=="open"){console.log("["+this.getChannelName()+"] Tried to send data while channel is not open. readyState is:",this._dataChannel.readyState);return}return typeof e=="string"&&(e=new TextEncoder().encode(e)),this._dataChannel.send(e)}getPlayer(){return this._player}destroy(){}};var S=class extends _{_micPermissions="prompt";_micStream;constructor(e){super(e),navigator.permissions.query({name:"microphone"}).then(t=>{this._micPermissions=t.state,t.onchange=()=>{this._micPermissions=t.state}})}getChannelName(){return"chat"}getChannelConfig(){return{ordered:!0,protocol:"chatV1"}}destroy(){}};var v=class r{_player;_index;_physicalGamepadId=-1;_rumbleInterval;_isFocused=!0;_gamepadOverride={buttons:{}};_options={enable_keyboard:!1,enable_gamepad:!0,enable_vibration:!0,keyboard_mapping:{A:"Enter",B:"Backspace",X:"x",Y:"y",DPadUp:"ArrowUp",DPadDown:"ArrowDown",DPadLeft:"ArrowLeft",DPadRight:"ArrowRight",LeftShoulder:"[",RightShoulder:"]",LeftThumb:"l",RightThumb:"r",LeftTrigger:"-",RightTrigger:"=",Menu:"m",View:"v",Nexus:"n"},gamepad_mapping:{A:"0",B:"1",X:"2",Y:"3",DPadUp:"12",DPadDown:"13",DPadLeft:"14",DPadRight:"15",LeftShoulder:"4",RightShoulder:"5",LeftThumb:"10",RightThumb:"11",LeftTrigger:"6",RightTrigger:"7",Menu:"9",View:"8",Nexus:"16"},gamepad_axes_mapping:{LeftThumbXAxis:"0",LeftThumbYAxis:"1",RightThumbXAxis:"2",RightThumbYAxis:"3"},gamepad_deadzone:.08,gamepad_force_capture:!1};_listener={keyDown:this.onKeyDown.bind(this),keyUp:this.onKeyUp.bind(this),gamepadConnected:this.onGamepadConnected.bind(this),gamepadDisconnected:this.onGamepadDisconnected.bind(this),windowFocus:this.onWindowFocus.bind(this),windowBlur:this.onWindowBlur.bind(this)};constructor(e,t={}){this._index=e,this._options={...this._options,...t}}attach(e){this._player=e,this._player._channels.control.sendGamepadState(this._index,!0,this),this._options.enable_keyboard===!0&&(window.addEventListener("keydown",this._listener.keyDown),window.addEventListener("keyup",this._listener.keyUp)),this._options.enable_gamepad===!0&&(window.addEventListener("gamepadconnected",this._listener.gamepadConnected),window.addEventListener("gamepaddisconnected",this._listener.gamepadDisconnected)),this._options.enable_gamepad===!0&&this._options.gamepad_force_capture===!1&&(window.addEventListener("blur",this._listener.windowBlur),window.addEventListener("focus",this._listener.windowFocus)),this.detectActiveGamepad()}detach(){if(this._player===void 0){console.log("[Gamepad] Player is not attached. this._player is:",this._player);return}this._player._channels.control.sendGamepadState(this._index,!1),this._physicalGamepadId=-1,this._rumbleInterval!==void 0&&clearInterval(this._rumbleInterval),this._options.enable_keyboard===!0&&(window.removeEventListener("keydown",this._listener.keyDown),window.removeEventListener("keyup",this._listener.keyUp)),this._options.enable_gamepad===!0&&(window.removeEventListener("gamepadconnected",this._listener.gamepadConnected),window.removeEventListener("gamepaddisconnected",this._listener.gamepadDisconnected)),this._options.enable_gamepad===!0&&this._options.gamepad_force_capture===!1&&(window.removeEventListener("blur",this._listener.windowBlur),window.removeEventListener("focus",this._listener.windowFocus))}sendButtonState(e,t){if(this._player===void 0){console.log("[Gamepad] Player is not attached. this._player is:",this._player);return}if(!(e in this.getDefaultFamepadFrame())){console.log("[Gamepad] Invalid button:",e);return}let n=this.getDefaultFamepadFrame();n.GamepadIndex=this._index,this._gamepadOverride.buttons[e]=t}onKeyDown(e){if(this._isFocused!==!1)for(let t in this._options.keyboard_mapping)this._options.keyboard_mapping[t]===e.key&&(this._gamepadOverride.buttons[t]=1,e.preventDefault())}onKeyUp(e){if(this._isFocused!==!1)for(let t in this._options.keyboard_mapping)this._options.keyboard_mapping[t]===e.key&&(this._gamepadOverride.buttons[t]=0,e.preventDefault())}onWindowFocus(){this._isFocused=!0}onWindowBlur(){}getDefaultFamepadFrame(){return{GamepadIndex:0,Nexus:0,Menu:0,View:0,A:0,B:0,X:0,Y:0,DPadUp:0,DPadDown:0,DPadLeft:0,DPadRight:0,LeftShoulder:0,Line truncated
`+t.ContentText)?this.completeTransaction(e.id,{Result:0}):this.completeTransaction(e.id,{Result:1})}};var M=class extends _{_handshakeCompleted=!1;getChannelName(){return"message"}getChannelConfig(){return{protocol:"messageV1",ordered:!0}}onOpen(){console.log("[MessageChannel] Sending handshake...");let e=JSON.stringify({type:"Handshake",version:"messageV1",id:"be0bfc6d-1e83-4c8a-90ed-fa8601c5a179",cv:"0"});this.send(e)}onMessage(e){let t=JSON.parse(e.data);switch(t.type){case"HandshakeAck":console.log("[MessageChannel] Handshake completed"),this._handshakeCompleted=!0,this.getPlayer()._channels.control.sendAuthorization(),this.getPlayer()._channels.input.start(),this.sendConfig();break;case"TransactionStart":case"Message":new D(this,t);break;default:console.log("[MessageChannel] Unhandled message type:",t.type,t)}}sendConfig(){let e=[],t=[0,2,0],n=JSON.stringify(this.generateMessage("/streaming/systemUi/configuration",{version:t,systemUis:e}));this.send(n);let i=JSON.stringify(this.generateMessage("/streaming/properties/clientappinstallidchanged",{clientAppInstallId:"c97d7ee0-73b2-4239-bf1d-9d805a338429"}));this.send(i);let a=JSON.stringify(this.generateMessage("/streaming/characteristics/orientationchanged",{orientation:0}));this.send(a);let s=JSON.stringify(this.generateMessage("/streaming/characteristics/touchinputenabledchanged",{touchInputEnabled:!1}));this.send(s);let o=JSON.stringify(this.generateMessage("/streaming/characteristics/clientdevicecapabilities",{}));this.send(o);let d=JSON.stringify(this.generateMessage("/streaming/characteristics/dimensionschanged",{horizontal:1920,vertical:1080,preferredWidth:1920,preferredHeight:1080,safeAreaLeft:0,safeAreaTop:0,safeAreaRight:1920,safeAreaBottom:1080,supportsCustomResolution:!0}));this.send(d)}generateMessage(e,t){return{type:"Message",content:JSON.stringify(t),id:Q(),target:e,cv:""}}destroy(){}};var E=class{_address;_ipv4Address;_ipv4Port;constructor(e){this._address=e;let t=this._address.split(":"),n=t[6]+t[7],i=t[5],a=~parseInt(n.substring(0,2),16)&255,s=~parseInt(n.substring(2,4),16)&255,o=~parseInt(n.substring(4,6),16)&255,d=~parseInt(n.substring(6,8),16)&255,u=~parseInt(i.substring(0,4),16)&65535;this._ipv4Address=a+"."+s+"."+o+"."+d,this._ipv4Port=u}getIpv4Address(){return this._ipv4Address}getIpv4Port(){return this._ipv4Port}};var R=class{_peerConnection;_iceCandidates=[];constructor(e){this._peerConnection=e._peerConnection,this._peerConnection.addEventListener("icecandidate",t=>{t.candidate&&this._iceCandidates.push(t.candidate)})}getCandidates(){return this._iceCandidates}setRemoteCandidates(e){e.length===0&&console.warn("Error: No candidates received!");for(let t in e){if(e[t].candidate==="a=end-of-candidates")continue;let n=e[t].candidate.split(" ");if(n.length>4&&n[4].substr(0,4)==="2001"){let i=new E(n[4]);this._peerConnection.addIceCandidate({candidate:"a=candidate:10 1 UDP 1 "+i.getIpv4Address()+" 9002 typ host ",sdpMLineIndex:e[t].sdpMLineIndex,sdpMid:e[t].sdpMid}),this._peerConnection.addIceCandidate({candidate:"a=candidate:11 1 UDP 1 "+i.getIpv4Address()+" "+i.getIpv4Port()+" typ host ",sdpMLineIndex:e[t].sdpMLineIndex,sdpMid:e[t].sdpMid})}this._peerConnection.addIceCandidate({candidate:e[t].candidate,sdpMid:e[t].sdpMid,sdpMLineIndex:e[t].sdpMLineIndex})}}};var I=class{_player;_peerConnection;constructor(e){this._player=e,this._peerConnection=e._peerConnection}getAvailableCodecs(){let e=RTCRtpReceiver.getCapabilities("video")?.codecs;if(e===void 0)return[];let t=[];for(let n in e)t.includes(e[n].mimeType)||t.push(e[n].mimeType);return t}setLocalSDP(e){return console.log("[SDP] setLocalSDP() Setting local SDP:",e),e.sdp=e.sdp?.replace("useinbandfec=1","useinbandfec=1; stereo=1"),e}setRemoteSDP(e){return console.log("[SDP] setRemoteSDP() Setting remote SDP:",e),e}getDefaultCodecPreferences(){let e=RTCRtpReceiver.getCapabilities("video")?.codecs;if(e===void 0)return[];let t=[],n=[],i=[],a=[];for(let o in e)e[o].mimeType.includes("H264")?e[o].sdpFmtpLine?.includes("profile-level-id=4d")?t.push(e[o]):e[o].sdpFmtpLine?.includes("profile-level-id=42e")?n.push(e[o]):e[o].sdpFmtpLine?.includes("profile-level-id=420")&&i.push(e[o]):(e[o].mimeType.includes("ulpfec")||e[o].mimeType.includes("flexfec")||e[o].mimeType.includes("VP9")||e[o].mimeType.includes("VP8"))&&a.push(e[o]);return[...t,...n,...i,...a]}_setBitrate(e,t,n){let i=e.split(`
`),a=-1;for(let o=0;o<i.length;o++)if(i[o].indexOf("m="+t)===0){a=o;break}if(a===-1)return console.debug("Could not find the m line for",t),e;for(a++;i[a].indexOf("i=")===0||i[a].indexOf("c=")===0;)a++;if(i[a].indexOf("b")===0)return i[a]="b=AS:"+n,i.join(`
`);let s=i.slice(0,a);return s.push("b=AS:"+n),s=s.concat(i.slice(a,i.length)),s.join(`
`)}};var P=class{_player;_peerConnection;_videoCodec="";_audioCodec="";_videoWidth=0;_videoHeight=0;_videoFps=0;_rtt=0;_remoteHost="";_remotePort=0;_remoteIsLocal=!1;_remoteIsIpv6=!1;_activeRemoteCandidate="";constructor(e){this._player=e,this._peerConnection=e._peerConnection,setInterval(this.loop.bind(this),1e3)}loop(){this._peerConnection.getStats().then(e=>{e.forEach(t=>{t.type==="inbound-rtp"?this.readInboundRtp(t):t.type==="candidate-pair"&&t.packetsReceived!==void 0&&t.packetsReceived>0?this.readCandidatePair(t):t.type==="remote-candidate"&&this.readRemoteCandidate(t)})})}readInboundRtp(e){e.kind==="video"&&e.codecId&&e.packetsReceived!==void 0&&e.packetsReceived>0?(e.codecId.includes("profile-level-id=4d")?this._videoCodec="H264 (High)":e.codecId.includes("profile-level-id=42e")?this._videoCodec="H264 (Normal)":e.codecId.includes("profile-level-id=420")?this._videoCodec="H264 (Low)":this._videoCodec="Other ("+e.codecId+")",e.frameWidth&&(this._videoWidth=e.frameWidth),e.frameHeight&&(this._videoHeight=e.frameHeight),e.framesPerSecond&&(this._videoFps=e.framesPerSecond)):e.kind==="audio"&&e.codecId&&e.packetsReceived!==void 0&&e.packetsReceived>0&&(this._audioCodec=e.codecId)}readCandidatePair(e){this._activeRemoteCandidate=e.remoteCandidateId,this._rtt=e.currentRoundTripTime}readRemoteCandidate(e){e.id===this._activeRemoteCandidate&&(this.isPrivateIP(e.address)?this._remoteIsLocal=!0:this._remoteIsLocal=!1,this.isIpv6(e.address)?this._remoteIsIpv6=!0:this._remoteIsIpv6=!1,this._remoteHost=e.address,this._remotePort=e.port)}isPrivateIP(e){let t=e.split(".");return t[0]==="10"||t[0]==="172"&&parseInt(t[1],10)>=16&&parseInt(t[1],10)<=31||t[0]==="192"&&t[1]==="168"}isIpv6(e){return e.split(":").length>3}};var G=class{_player;_videoComponent;_overlays={debug:void 0};constructor(e,t){this._videoComponent=e,this._player=t}toggleDebug(){this._overlays.debug===void 0?this.createDebugOverlay():this.destroy()}createDebugOverlay(){this._overlays.debug=document.createElement("div"),this._overlays.debug.id="playerDebugOverlay",this._overlays.debug.style.position="absolute",this._overlays.debug.style.top="0",this._overlays.debug.style.left="0",this._overlays.debug.style.width=this._videoComponent.getElement()?.style.width||"100%",this._overlays.debug.style.height=this._videoComponent.getElement()?.style.height||"auto",this._overlays.debug.style.background="linear-gradient(0deg, rgba(0,0,0, 0) 0%, rgba(0,0,0, 0) 75%, rgba(0,0,0, 0.5) 100%)",this._overlays.debug.style.padding="10px",this._overlays.debug.style.pointerEvents="none",document.getElementById(this._player.getElementId())?.appendChild(this._overlays.debug),setTimeout(()=>{this.refreshDebugOverlay()},500),this.refreshDebugOverlay()}destroy(){this._overlays.debug?.remove(),this._overlays.debug=void 0}refreshDebugOverlay(){if(this._overlays.debug!==void 0){this._overlays.debug.innerHTML="",this._overlays.debug.appendChild(this.createLabel("Local Play",this._player.getStats()._remoteIsLocal?"Local":"Remote","ok")),this._overlays.debug.appendChild(this.createLabel("Resolution",this._player.getStats()._videoWidth+"x"+this._player.getStats()._videoHeight,"ok")),this._overlays.debug.appendChild(this.createLabel("Stream FPS",this._player.getStats()._videoFps.toString(),this._player.getStats()._videoFps>=58?"ok":"warning"));let e=this._videoComponent.getFrameStats?.();e&&(this._overlays.debug.appendChild(this.createLabel("Render FPS",e.renderingFps.toString(),e.renderingFps>=58?"ok":"warning")),this._overlays.debug.appendChild(this.createLabel("Render Delay",e.renderDelayMs.toString()+" ms",e.renderDelayMs<16?"ok":e.renderDelayMs<33?"warning":"error")),this._overlays.debug.appendChild(this.createLabel("Dropped Frames",e.droppedFrames.toString(),e.droppedFrames===0?"ok":e.droppedFrames<30?"warning":"error"))),this._overlays.debug.appendChild(this.createLabel("Connection",this._player.getStats()._remoteIsIpv6?"IPv6":"IPv4","ok")),this._overlays.debug.appendChild(this.createLabel("Video",this._player.getStats()._videoCodec,"ok"));let t=this._player.getStats()._rtt*1e3>75?"error":this._player.getStats()._rtt*1e3>40?"warning":"ok";this._overlays.debug.appendChild(this.createLabel("RTT",(this._player.getStats()._rtt*1e3).toString()+" ms",t)),setTimeout(()=>{this.refreshDebugOverlay()},500)}}createLabel(e,t="",n=""){let i={borderRadius:"5px",background:"linear-gradient(0deg, rgba(28,26,26,1) 0%, rgba(47,45,45,1) 100%)",padding:"10px",fontSize:"12px",marginTop:"3px",marginRight:"10px",marginBottom:"2px",textTransform:"uppercase",color:"white",display:"inline-block"},a=document.createElement("span"),s={error:"rgb(220, 53, 69)",warning:"rgb(255, 193, 7)",ok:"rgb(25, 135, 84)"};t!==""?a.innerHTML=`${e}: <span style="background: ${s[n]}; border-radius: 5px; padding: 4px; padding-left: 8px; padding-right: 8px;">${t}</span>`:a.innerText=e;for(let o in i)a.style[o]=i[o];return a}};var L=class{_player;_element;_overlay;constructor(e){this._player=e,this._overlay=new G(this,this._pLine truncated
in vec2 position;
out vec2 vUV;
void main() { vUV = position * 0.5 + 0.5; gl_Position = vec4(position, 0.0, 1.0); }`,j=`attribute vec2 position;
varying vec2 vUV;
void main() { vUV = position * 0.5 + 0.5; gl_Position = vec4(position, 0.0, 1.0); }`,Z=`#version 300 es
precision mediump float;
uniform sampler2D screenTexture;
uniform vec2 resolution;
uniform int filterId;
uniform float sharpenFactor;
uniform float brightness;
uniform float contrast;
uniform float gamma;
uniform float saturation;
uniform float deband;
out vec4 fragColor;
const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;
vec3 sharpen(vec2 coord, vec3 e) {
vec2 px = 1.0 / resolution;
vec3 b = texture(screenTexture, coord + px * vec2(0, 1)).rgb;
vec3 d = texture(screenTexture, coord + px * vec2(-1, 0)).rgb;
vec3 f = texture(screenTexture, coord + px * vec2(1, 0)).rgb;
vec3 h = texture(screenTexture, coord + px * vec2(0, -1)).rgb;
vec3 a = texture(screenTexture, coord + px * vec2(-1, 1)).rgb;
vec3 c = texture(screenTexture, coord + px * vec2(1, 1)).rgb;
vec3 g = texture(screenTexture, coord + px * vec2(-1, -1)).rgb;
vec3 i = texture(screenTexture, coord + px * vec2(1, -1)).rgb;
if (filterId == 1) {
vec3 blur = ((a + c + g + i) + (b + d + f + h) * 2.0 + e * 4.0) / 16.0;
return e + (e - blur) * sharpenFactor / 3.0;
}
vec3 minRgb = min(min(min(d, e), min(f, b)), h) + min(min(a, c), min(g, i));
vec3 maxRgb = max(max(max(d, e), max(f, b)), h) + max(max(a, c), max(g, i));
vec3 amplify = inversesqrt(clamp(min(minRgb, 2.0 - maxRgb) / maxRgb, 0.0, 1.0));
vec3 weight = -(1.0 / (amplify * CAS_CONTRAST_PEAK));
vec3 outColor = clamp(((b + d + f + h) * weight + e) / (4.0 * weight + 1.0), 0.0, 1.0);
return mix(e, outColor, sharpenFactor / 2.0);
}
float ign(vec2 p) { return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); }
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
vec3 color = texture(screenTexture, uv).rgb;
if (sharpenFactor > 0.0) color = sharpen(uv, color);
color = clamp(color + brightness, 0.0, 1.0);
color = (color - 0.5) * clamp(contrast + 1.0, 0.5, 2.0) + 0.5;
color = mix(vec3(dot(color, vec3(0.2126, 0.7152, 0.0722))), color, saturation);
color = pow(max(color, 0.0), vec3(1.0 / gamma));
if (deband > 0.0) color += vec3(((ign(gl_FragCoord.xy) + ign(gl_FragCoord.xy + vec2(11.0, 17.0))) - 1.0) * (deband / 255.0));
fragColor = vec4(color, 1.0);
}`,ee=`precision highp float;
#define FXAA_MIN_REDUCE (1.0 / 128.0)
#define FXAA_MUL_REDUCE (1.0 / 8.0)
#define MAX_SPAN 8.0
uniform sampler2D screenTexture;
uniform vec2 resolution;
const vec3 luma = vec3(0.299, 0.587, 0.114);
void main() {
vec2 invResolution = 1.0 / resolution;
vec3 rgbNW = texture2D(screenTexture, (gl_FragCoord.xy + vec2(-1.0, -1.0)) * invResolution).rgb;
vec3 rgbNE = texture2D(screenTexture, (gl_FragCoord.xy + vec2( 1.0, -1.0)) * invResolution).rgb;
vec3 rgbSW = texture2D(screenTexture, (gl_FragCoord.xy + vec2(-1.0, 1.0)) * invResolution).rgb;
vec3 rgbSE = texture2D(screenTexture, (gl_FragCoord.xy + vec2( 1.0, 1.0)) * invResolution).rgb;
vec3 rgbM = texture2D(screenTexture, gl_FragCoord.xy * invResolution).rgb;
float lumaNW = dot(rgbNW, luma);
float lumaNE = dot(rgbNE, luma);
float lumaSW = dot(rgbSW, luma);
float lumaSE = dot(rgbSE, luma);
float lumaM = dot(rgbM, luma);
float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));
float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));
vec2 dir;
dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));
dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE));
float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * 0.25 * FXAA_MUL_REDUCE, FXAA_MIN_REDUCE);
float minDirFactor = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);
dir = clamp(dir * minDirFactor, vec2(-MAX_SPAN), vec2(MAX_SPAN)) * invResolution;
vec4 rgbA = 0.5 * (
texture2D(screenTexture, gl_FragCoord.xy * invResolution + dir * (1.0 / 3.0 - 0.5)) +
texture2D(screenTexture, gl_FragCoord.xy * invResolution + dir * (2.0 / 3.0 - 0.5)));
vec4 rgbB = rgbA * 0.5 + 0.25 * (
texture2D(screenTexture, gl_FragCoord.xy * invResolution + dir * -0.5) +
texture2D(screenTexture, gl_FragCoord.xy * invResolution + dir * 0.5));
float lumaB = dot(rgbB, vec4(luma, 0.0));
gl_FragColor = lumaB < lumaMin || lumaB > lumaMax ? rgbA : rgbB;
}`,te=`precision highp float;
uniform sampler2D screenTexture;
uniform vec2 resolution;
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
float edgeThreshold = 0.2;
vec2 offset = vec2(1.0) / resolution;
vec3 colorCenter = texture2D(screenTexture, uv).rgb;
vec3 colorLeft = texture2D(screenTexture, uv - vec2(offset.x, 0.0)).rgb;
vec3 colorRight = texture2D(screenTexture, uv + vec2(offset.x, 0.0)).rgb;
vec3 colorUp = texture2D(screenTexture, uv - vec2(0.0, offset.y)).rgb;
vec3 colorDown = texture2D(screenTexture, uv + vec2(0.0, offset.y)).rgb;
float diffHorizontal = length(colorRight - colorLeft);
float diffVertical = length(colorUp - colorDown);
float edgeFactor = step(edgeThreshold, diffHorizontal + diffVertical);
vec3 outlineColor = mix(colorCenter, vec3(0.0), edgeFactor);
gl_FragColor = vec4(outlineColor, 1.0);
}`,ne=`precision highp float;
#define CA_AMOUNT 1.0025
#define SCANLINE_INTENSITY_X 0.125
#define SCANLINE_INTENSITY_Y 0.375
#define SCANLINE_SIZE 1024.0
uniform sampler2D screenTexture;
varying vec2 vUV;
void main() {
vec4 finalColor = texture2D(screenTexture, vUV);
finalColor.rgb = vec3(
texture2D(screenTexture, (vUV - 0.5) * CA_AMOUNT + 0.5).r,
finalColor.g,
texture2D(screenTexture, (vUV - 0.5) / CA_AMOUNT + 0.5).b
);
float scanlineX = abs(sin(vUV.x * SCANLINE_SIZE) * 0.5 * SCANLINE_INTENSITY_X);
float scanlineY = abs(sin(vUV.y * SCANLINE_SIZE) * 0.5 * SCANLINE_INTENSITY_Y);
gl_FragColor = vec4(mix(finalColor.rgb, vec3(0.0), scanlineX + scanlineY), finalColor.a);
}`,ie=`precision highp float;
#define PI 3.14159265
#define SCANLINE_INTENSITY 0.35
#define CHROMA_OFFSET 0.005
#define BLUR_RADIUS 0.002
#define WARP_AMOUNT 0.01
#define SCANLINE_DARKEN 0.5
uniform sampler2D screenTexture;
uniform int FrameCount;
uniform vec2 TextureSize;
uniform vec2 resolution;
varying vec2 vUV;
const mat3 yiq_mat = mat3(
0.299, 0.587, 0.114,
0.596, -0.275, -0.321,
0.212, -0.523, 0.311
);
const mat3 yiq2rgb_mat = mat3(
1.0, 0.956, 0.621,
1.0, -0.272, -0.647,
1.0, -1.106, 1.705
);
vec3 applyNTSC(vec2 uv) {
vec3 col = texture2D(screenTexture, uv).rgb;
vec3 yiq = col * yiq_mat;
float chromaPhase = PI * (mod(uv.y * TextureSize.y, 2.0) + float(FrameCount));
yiq.y *= cos(chromaPhase * 0.5);
yiq.z *= sin(chromaPhase * 0.5);
vec3 rgb = yiq * yiq2rgb_mat;
vec3 finalColor;
finalColor.r = texture2D(screenTexture, uv + vec2(CHROMA_OFFSET, 0.0)).r;
finalColor.g = texture2D(screenTexture, uv + vec2(0.0, BLUR_RADIUS)).g;
finalColor.b = texture2D(screenTexture, uv - vec2(CHROMA_OFFSET, 0.0)).b;
return finalColor;
}
vec3 applyScanlines(vec2 uv) {
vec3 col = texture2D(screenTexture, uv).rgb;
float scanline = abs(sin(uv.y * resolution.y * 2.0)) * SCANLINE_INTENSITY;
col *= 1.0 - (scanline * SCANLINE_DARKEN);
return col;
}
vec2 applyWarp(vec2 uv) {
uv = uv * 2.0 - 1.0;
float r = sqrt(uv.x * uv.x + uv.y * uv.y);
uv += uv * (r * r) * WARP_AMOUNT;
return uv * 0.5 + 0.5;
}
void main() {
vec2 warpedUV = applyWarp(vUV);
vec3 ntscColor = applyNTSC(warpedUV);
vec3 scanlineColor = applyScanlines(warpedUV);
vec3 finalColor = mix(ntscColor, scanlineColor, 0.7);
gl_FragColor = vec4(finalColor, 1.0);
}`,H=class{constructor(e,t){this.video=e;this.effects=t;this.canvas=document.createElement("canvas"),Object.assign(this.canvas.style,{position:"absolute",inset:"0",width:"100%",height:"100%",objectFit:"contain",zIndex:"1"}),e.parentElement?.appendChild(this.canvas);let n=this.canvas.getContext("webgl2",{alpha:!1,antialias:!1,depth:!1,desynchronized:!0,powerPreference:"high-performance"});if(n===null)throw new Error("WebGL2 unavailable");this.gl=n;let i=n.createBuffer();n.bindBuffer(n.ARRAY_BUFFER,i),n.bufferData(n.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),n.STATIC_DRAW),this.passes.set("base",this.compile(J,Z,["screenTexture","resolution","filterId","sharpenFactor","brightness","contrast","gamma","saturation","deband"])),this.videoTexture=this.texture(),this.schedule()}canvas;gl;passes=new Map;videoTexture;targets=[];width=0;height=0;frame=0;lastDraw=0;stopped=!1;idle=!1;pending=!1;update(e){this.effects=e}pause(){this.idle||this.stopped||(this.idle=!0,this.canvas.style.display="none")}resume(e){this.effects=e,!(!this.idle||this.stopped)&&(this.idle=!1,this.canvas.style.display="",this.schedule())}schedule(){this.pending||(this.pending=!0,this.video.requestVideoFrameCallback(this.onFrame))}compile(e,t,n){let i=this.gl,a=(o,d)=>{let u=i.createShader(o);if(i.shaderSource(u,d),i.compileShader(u),!i.getShaderParameter(u,i.COMPILE_STATUS))throw new Error(`shader: ${i.getShaderInfoLog(u)}`);return u},s=i.createProgram();if(i.attachShader(s,a(i.VERTEX_SHADER,e)),i.attachShader(s,a(i.FRAGMENT_SHADER,t)),i.bindAttribLocation(s,0,"position"),i.linkProgram(s),!i.getProgramParameter(s,i.LINK_STATUS))throw new Error(`program: ${i.getProgramInfoLog(s)}`);return{program:s,uniforms:new Map(n.map(o=>[o,i.getUniformLocation(s,o)]))}}extra(e){let t=this.passes.get(e);if(t===void 0){let n={fxaa:ee,toon:te,crt:ne,ntsc:ie}[e];t=this.compile(j,n,["screenTexture","resolution","TextureSize","FrameCount"]),this.passes.set(e,t)}return t}texture(){let e=this.gl,t=e.createTexture();return e.bindTexture(e.TEXTURE_2D,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),t}resize(e,t){let n=this.gl;this.width=e,this.height=t,this.canvas.width=e,this.canvas.height=t;for(let i of this.targets)n.deleteFramebuffer(i.framebuffer),n.deleteTexture(i.texture);this.targets=[0,1].map(()=>{let i=this.texture();n.texImage2D(n.TEXTURE_2D,0,n.RGBA,e,t,0,n.RGBA,n.UNSIGNED_BYTE,null);let a=n.createFramebuffer();return n.bindFramebuffer(n.FRAMEBUFFER,a),n.framebufferTexture2D(n.FRAMEBUFFER,n.COLOR_ATTACHMENT0,n.TEXTURE_2D,i,0),{framebuffer:a,texture:i}}),n.bindFramebuffer(n.FRAMEBUFFER,null)}onFrame=e=>{if(this.pending=!1,this.stopped||this.idle)return;this.schedule();let t=this.effects.maxFps;if(!(t>0&&e-this.lastDraw<1e3/t-2)){this.lastDraw=e;try{this.draw()}catch(n){window.DDCloud?.log(`effects: ${String(n).slice(0,200)}`),this.destroy()}}};draw(){let e=this.gl,t=this.effects,n=this.video.videoWidth,i=this.video.videoHeight;if(n===0||i===0)return;(n!==this.width||i!==this.height)&&this.resize(n,i),this.frame++,e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.videoTexture),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!0),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,this.video);let a=["fxaa","toon","crt","ntsc"].filter(d=>t[d]);e.viewport(0,0,n,i),e.enableVertexAttribArray(0),e.vertexAttribPointer(0,2,e.FLOAT,!1,0,0);let s=this.passes.get("base");e.bindFramebuffer(e.FRAMEBUFFER,a.length>0?this.targets[0].framebuffer:null),e.useProgram(s.program);let o=s.uniforms;e.uniform1i(o.get("screenTexture"),0),e.uniform2f(o.get("resolution"),n,i),e.uniform1i(o.get("filterId"),t.sharpenMode==="usm"?1:2),e.uniform1f(o.get("sharpenFactor"),t.sharpen),e.uniform1f(o.get("brightness"),t.brightness/100),e.uniform1f(o.get("contrast"),t.contrast/100),e.uniform1f(o.get("gamma"),Math.min(5,Math.max(.1,t.gamma))),e.uniform1f(o.get("saturation"),t.saturation/100),e.uniform1f(o.get("deband"),t.deband),e.drawArrays(e.TRIANGLES,0,3),a.forEach((d,u)=>{let h=this.extra(d),g=this.targets[u%2],p=u===a.length-1;e.bindFramebuffer(e.FRAMEBUFFER,p?null:this.targets[(u+1)%2].framebuffer),e.useProgram(h.program),e.bindTexture(e.TEXTURE_2D,g.texture);let c=h.uniforms;e.uniform1i(c.get("screenTexture")??null,0),e.uniform2f(c.get("resolution")??null,n,i),e.uniform2f(c.get("TextureSize")??null,n,i),e.uniform1i(c.get("FrameCount")??null,this.frame),e.drawArrays(e.TRIANGLES,0,3)})}destroy(){this.stopped=!0,this.canvas.remove(),this.gl.getExtension("WEBGL_lose_context")?.loseContext()}};var V=class{constructor(e){this.pc=e;this.element=document.createElement("div"),Object.assign(this.element.style,{position:"absolute",top:"8px",right:"8px",zIndex:"3",padding:"4px 8px",borderRadius:"6px",background:"rgba(0,0,0,0.55)",color:"#e8e8e8",font:"12px monospace",lineHeight:"16px",pointerEvent Line truncated
`:" \xB7 ")}destroy(){this.show(0,this.vertical,this.server,this.left),this.element.remove()}};function z(r,e){let t=e==="high"?"4d":e==="low"?"420":"42e",n=[];for(let d of r.matchAll(/a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g))d[2].startsWith(t)&&n.push(d[1]);if(n.length===0)return r;let i=r.split(`\r
`),a=i.findIndex(d=>d.startsWith("m=video"));if(a<0)return r;let s=i[a].trim().split(" "),o=s.slice(3).filter(d=>!n.includes(d));return i[a]=s.slice(0,3).concat(n,o).join(" "),i.join(`\r
`)}function K(r,e){let t=r.split(`\r
`),n=t.findIndex(a=>a.startsWith("m=video"));if(n<0)return r;let i=`b=AS:${e}`;for(let a=n+1;a<=t.length;a++){let s=t[a];if(s===void 0||s.startsWith("m=")){t.splice(a,0,i);break}if(!(s.startsWith("i=")||s.startsWith("c="))){if(s.startsWith("b=AS:")){t[a]=i;break}t.splice(a,0,i);break}}return t.join(`\r
`)}var m=JSON.parse(DDCloud.settings()),ae=RTCPeerConnection.prototype.setLocalDescription;RTCPeerConnection.prototype.setLocalDescription=function(r){return r?.sdp&&((m.codec==="high"||m.codec==="normal"||m.codec==="low")&&(r.sdp=z(r.sdp,m.codec)),m.bitrateMbps>0&&(r.sdp=K(r.sdp,m.bitrateMbps*1e3))),ae.call(this,r)};window.__ddRumble=m.rumble/100;window.addEventListener("error",r=>DDCloud.log(`error: ${r.message} @${r.lineno}`));window.addEventListener("unhandledrejection",r=>DDCloud.log(`rejection: ${String(r.reason).slice(0,200)}`));window.addEventListener("gamepadconnected",r=>DDCloud.log(`controller: ${r.gamepad.id.slice(0,60)} mapping=${r.gamepad.mapping||"none"} buttons=${r.gamepad.buttons.length} axes=${r.gamepad.axes.length}`));var W=new Map,re=1;window.__ddResolve=(r,e,t)=>{let n=W.get(r);W.delete(r),n&&n({status:e,body:t})};function C(r,e=""){return new Promise(t=>{let n=re++;W.set(n,t),DDCloud.call(n,r,typeof e=="string"?e:JSON.stringify(e))})}var B=r=>new Promise(e=>setTimeout(e,r)),se=r=>r.status>=200&&r.status<300;function x(r){let e=document.getElementById("status");e&&(e.textContent=r),DDCloud.status(r)}var T=class extends Error{constructor(t,n){super(t);this.stage=t;this.detail=n}};function k(r,e){if(!se(e))throw new T(r,`${e.status} ${e.body.slice(0,300)}`);return e}function oe(r,e){return new Promise(t=>{if(r.iceGatheringState==="complete")return t();let n=setTimeout(t,e);r.addEventListener("icegatheringstatechange",()=>{r.iceGatheringState==="complete"&&(clearTimeout(n),t())})})}async function de(){x("Starting the game on Xbox Cloud Gaming\u2026"),k("start",await C("start"));let r=!1,e=0;for(;;){let l=k("state",await C("state")),f=JSON.parse(l.body||"{}");if(f.state==="Provisioned")break;if(f.state==="Failed")throw new T("provision",JSON.stringify(f.errorDetails??{}).slice(0,300));f.state==="ReadyToConnect"&&!r?(r=!0,x("Signing in to the cloud console\u2026"),k("connect",await C("connect"))):f.state==="WaitingForResources"?x(`In the queue for a cloud console\u2026 ${Math.round(e/1e3)} s`):x(`Getting a cloud console ready\u2026 ${Math.round(e/1e3)} s`),await B(1e3),e+=1e3}x("Connecting\u2026");let t=new U("player");t.init();let n=t._peerConnection,i=t._channels.input,a=i.queueMetadataFrame.bind(i);i.queueMetadataFrame=l=>{if(m.preventDrops){let f=l.frameRenderedTimeMs-l.frameDecodedTimeMs;l.frameDecodedTimeMs=l.frameSubmittedTimeMs+10,l.frameRenderedTimeMs=l.frameDecodedTimeMs+f}a(l)};let s,o,d=()=>{if(!(o===void 0||s===void 0&&m.volume===100)){if(s===void 0){let l=new AudioContext;s=l.createGain(),l.createMediaStreamSource(o).connect(s).connect(l.destination);let f=document.querySelector("#player audio");f&&(f.muted=!0)}s.gain.value=m.volume/100}};n.addEventListener("track",l=>{l.track.kind==="audio"&&(o=l.streams[0],d())});let u=new V(n),h,g=()=>{let l=t.getVideoElement();if(l!==void 0){if(!X(m)){h?.pause();return}if(h===void 0)try{h=new H(l,m)}catch(f){DDCloud.log(`effects unavailable: ${String(f).slice(0,200)}`)}else h.resume(m)}};window.__ddApply=l=>{m=JSON.parse(l),window.__ddRumble=m.rumble/100,u.show(m.statsLevel,m.statsLayout==="vertical",m.server,m.statsCorner==="left"),d(),g()},n.addEventListener("connectionstatechange",()=>{n.connectionState==="connected"?(x(""),DDCloud.playing(),u.show(m.statsLevel,m.statsLayout==="vertical",m.server,m.statsCorner==="left"),setTimeout(g,500)):n.connectionState==="failed"?DDCloud.failed("webrtc","connection failed"):n.connectionState==="disconnected"&&x("Connection lost\u2026")});let p=await t.createOffer(),c=k("sdp",await C("sdp",p.sdp??"")),O=JSON.parse(JSON.parse(c.body).exchangeResponse);if(O.status&&O.status!=="success")throw new T("sdp-answer",JSON.stringify(O).slice(0,300));await t.setRemoteSDP(O.sdp),await oe(n,3e3);let Y=(await t.getICECandidates()).map(l=>JSON.stringify({candidate:l.candidate,sdpMid:l.sdpMid,sdpMLineIndex:l.sdpMLineIndex,usernameFragment:l.usernameFragment})),$=k("ice",await C("ice",Y));t.setRemoteIceCandidates(JSON.parse(JSON.parse($.body).exchangeResponse));let q=Date.now();for(;!t._channels.message._handshakeCompleted&&Date.now()-q<2e4;)await B(100);DDCloud.log(`handshake ${t._channels.message._handshakeCompleted?"done":"missing"} after ${Date.now()-q} ms`),await B(300);let b=new v(0,{enable_keyboard:!1});b.attach(t),DDCloud.log(`controllers at attach: ${Array.from(navigator.getGamepads()).filter(Boolean).length}`),window.__ddNexus=()=>{b.sendButtonState("Nexus",1),setTimeout(()=>b.sendButtonState("Nexus",0),200)},window.__ddButton=(l,f)=>b.sendButtonState(l,f),window.__ddInput=l=>{l?b.onWindowFocus():b.onWindowBlur()},setInterval(()=>{C("keepalive")},3e4),window.__ddStop=()=>{try{h?.destroy(),u.destroy(),b.detach(),t.destroy()}catch{}}}de().catch(r=>{r instanceof T?DDCloud.failed(r.stage,r.detail):DDCloud.failed("page",String(r).slice(0,300))});})();
+2 -2
View File
@@ -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)
-2
View File
@@ -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;
+21 -133
View File
@@ -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;
@@ -92,7 +93,6 @@ static constexpr unsigned int FAKE_INPUT_MAJOR = 13;
static constexpr unsigned int FAKE_INPUT_EVENT_MINOR_BASE = 64;
static constexpr unsigned int FAKE_INPUT_JS_MINOR_BASE = 0;
static constexpr int UINPUT_EVENT_BASE = 16;
static constexpr int MOUSE_EVENT_SLOT = 4;
static constexpr int UINPUT_EVENT_LIMIT = 64;
static constexpr uint32_t UINPUT_DESCRIPTION_MAGIC = 0x46495544;
@@ -121,12 +121,6 @@ static constexpr size_t FAKE_INPUT_RING_HEADER_SIZE =
static constexpr size_t FAKE_INPUT_RING_SIZE =
FAKE_INPUT_RING_HEADER_SIZE +
(FAKE_INPUT_RING_CAPACITY * FAKE_INPUT_EVENT_SIZE);
static constexpr size_t DECK_IMU_BLOCK_SIZE = 64;
static constexpr size_t FAKE_INPUT_RING_PAD_TYPE_OFFSET = 44;
static constexpr uint32_t PAD_TYPE_DECK = 0, PAD_TYPE_STEAM = 1, PAD_TYPE_XBOX = 2, PAD_TYPE_DS4 = 3,
PAD_TYPE_DUALSENSE = 4, PAD_TYPE_LAST = PAD_TYPE_DUALSENSE,
PAD_TYPE_MOUSE = 5;
static constexpr uint32_t PAD_TYPE_SET = 0x80000000u;
struct UinputDescription {
uint32_t magic;
@@ -155,7 +149,6 @@ struct FakeController {
size_t mapping_size = 0;
bool closed = false;
bool needs_keyframe = true;
bool mouse = false;
~FakeController() {
if (ring) munmap(ring, mapping_size);
@@ -390,6 +383,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) {
@@ -613,7 +607,7 @@ open_fake_input_ring(const char *event, int flags) {
int slot = get_event_number(event);
bool virtual_pad = slot >= UINPUT_EVENT_BASE;
std::string ring_path = virtual_pad ? uinput_ring_path(slot) : get_ring_path_for_slot(slot);
size_t mapping_size = virtual_pad ? UINPUT_FILE_SIZE : FAKE_INPUT_RING_SIZE + DECK_IMU_BLOCK_SIZE;
size_t mapping_size = virtual_pad ? UINPUT_FILE_SIZE : FAKE_INPUT_RING_SIZE;
if (ring_path.empty()) {
Logger::log("No input ring is configured for %s (slot %d)\n", event, slot);
errno = ENODEV;
@@ -657,22 +651,9 @@ open_fake_input_ring(const char *event, int flags) {
controller->uinput = description;
controller->mapping_size = mapping_size;
controller->generation = ring_generation(ring);
if (!virtual_pad) {
uint32_t pad_type = __atomic_load_n(
reinterpret_cast<volatile uint32_t *>(static_cast<uint8_t *>(mapping) + FAKE_INPUT_RING_SIZE +
FAKE_INPUT_RING_PAD_TYPE_OFFSET),
__ATOMIC_ACQUIRE);
controller->mouse = (pad_type & PAD_TYPE_SET) && (pad_type & ~PAD_TYPE_SET) == PAD_TYPE_MOUSE;
}
SnapshotState snap;
if (read_snapshot(ring, snap) && snap.generation == controller->generation) {
if (controller->mouse) {
controller->read_seq = snap.write_seq;
controller->needs_keyframe = false;
} else {
capture_keyframe(*controller, snap);
}
}
if (read_snapshot(ring, snap) && snap.generation == controller->generation)
capture_keyframe(*controller, snap);
{
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
controller_map()[fd] = controller;
@@ -1175,8 +1156,7 @@ struct DeckImu {
uint32_t pad_type;
uint32_t latency;
};
static_assert(offsetof(DeckImu, pad_type) == FAKE_INPUT_RING_PAD_TYPE_OFFSET,
"DeckImu::pad_type matches FakeInputWriter and the ring reader");
static_assert(offsetof(DeckImu, pad_type) == 44, "DeckImu::pad_type matches FakeInputWriter");
static_assert(offsetof(DeckImu, latency) == 48, "DeckImu::latency matches FakeInputWriter");
static constexpr uint32_t LATENCY_CONTROLLER = 1, LATENCY_TOUCH = 2;
static constexpr int LOW_LATENCY_REPORT_INTERVAL_US = 1000;
@@ -1193,6 +1173,7 @@ struct DeckExtras {
uint32_t controls = 0;
};
static constexpr uint32_t DECK_IMU_MAGIC = 0x31554D49;
static constexpr size_t DECK_IMU_BLOCK_SIZE = 64;
static_assert(sizeof(DeckImu) <= DECK_IMU_BLOCK_SIZE, "the IMU block is 64 bytes in the ring file");
struct TritonSharedData {
@@ -1241,6 +1222,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";
@@ -1288,6 +1270,10 @@ static bool fake_deck_enabled() {
return enabled == 1;
}
static constexpr uint32_t PAD_TYPE_DECK = 0, PAD_TYPE_STEAM = 1, PAD_TYPE_XBOX = 2, PAD_TYPE_DS4 = 3,
PAD_TYPE_DUALSENSE = 4, PAD_TYPE_LAST = PAD_TYPE_DUALSENSE;
static constexpr uint32_t PAD_TYPE_SET = 0x80000000u;
__attribute__((visibility("hidden"))) static uint32_t session_pad_type() {
static int type = -1;
if (type < 0) {
@@ -1422,12 +1408,6 @@ __attribute__((visibility("hidden"))) static bool is_withdrawn_pad_path(const ch
if (strncmp(event, "event", 5) && strncmp(event, "js", 2)) return false;
int number = get_event_number(event);
if (number < 0 || number >= UINPUT_EVENT_BASE) return false;
if (number == MOUSE_EVENT_SLOT && !strncmp(event, "event", 5)) {
char *fake = from_real_to_fake_path(pathname);
bool exposed = fake && path_exists(fake);
free(fake);
return !exposed;
}
return number == 0 || !process_is_steam_client();
}
@@ -2670,6 +2650,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 +2936,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 +3016,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);
@@ -3372,85 +3351,6 @@ static int copy_ioctl_bits(int op, void *destination, const unsigned char (&bits
return static_cast<int>(size);
}
static constexpr uint16_t MOUSE_VENDOR_ID = 0x1dd1;
static constexpr uint16_t MOUSE_PRODUCT_ID = 0x0004;
static int ioctl_mouse(int op, void *argp) {
int type = (op >> 8 & 0xFF);
int number = (op >> 0 & 0xFF);
if (type != 0x45) {
errno = EINVAL;
return -1;
}
switch (number) {
case 0x01: {
int version = 65536;
memcpy(argp, &version, sizeof(int));
return 0;
}
case 0x02: {
struct input_id id;
memset(&id, 0, sizeof(id));
id.bustype = 0x03;
id.vendor = MOUSE_VENDOR_ID;
id.product = MOUSE_PRODUCT_ID;
id.version = 1;
memcpy(argp, &id, sizeof(id));
return 0;
}
case 0x06:
if (argp && _IOC_SIZE(op)) snprintf(static_cast<char *>(argp), _IOC_SIZE(op), "DroidDeck Mouse");
return 0;
case 0x07:
if (argp && _IOC_SIZE(op)) snprintf(static_cast<char *>(argp), _IOC_SIZE(op), "usb-droiddeck/mouse");
return 0;
case 0x08:
if (argp && _IOC_SIZE(op)) static_cast<char *>(argp)[0] = '\0';
return 0;
case 0x09: {
unsigned char bitmask[(INPUT_PROP_MAX + 8) / 8] = {};
bitmask[INPUT_PROP_POINTER / 8] |= 1 << (INPUT_PROP_POINTER % 8);
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x18: {
unsigned char bitmask[(KEY_MAX + 8) / 8] = {};
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x20: {
unsigned char bitmask[(EV_MAX + 8) / 8] = {};
bitmask[EV_SYN / 8] |= 1 << (EV_SYN % 8);
bitmask[EV_KEY / 8] |= 1 << (EV_KEY % 8);
bitmask[EV_REL / 8] |= 1 << (EV_REL % 8);
bitmask[EV_MSC / 8] |= 1 << (EV_MSC % 8);
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x21: {
unsigned char bitmask[(KEY_MAX + 8) / 8] = {};
for (int button = BTN_LEFT; button <= BTN_TASK; button++)
if (button <= BTN_MIDDLE || button == BTN_SIDE || button == BTN_EXTRA)
bitmask[button / 8] |= 1 << (button % 8);
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x22: {
unsigned char bitmask[(REL_MAX + 8) / 8] = {};
for (uint16_t code : {REL_X, REL_Y, REL_WHEEL, REL_HWHEEL, REL_WHEEL_HI_RES, REL_HWHEEL_HI_RES})
bitmask[code / 8] |= 1 << (code % 8);
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x23: {
unsigned char bitmask[(ABS_MAX + 8) / 8] = {};
return copy_ioctl_bits(op, argp, bitmask);
}
case 0x35: {
unsigned char bitmask[(FF_MAX + 8) / 8] = {};
return copy_ioctl_bits(op, argp, bitmask);
}
default:
errno = EINVAL;
return -1;
}
}
static bool wait_snapshot(std::shared_ptr<FakeController> fake,
std::unique_lock<std::recursive_mutex> &guard,
SnapshotState &snap) {
@@ -3517,13 +3417,6 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) {
int event_number = controller->second->slot;
const UinputDescription *made = controller->second->uinput;
if (controller->second->mouse) {
int result = ioctl_mouse(op, argp);
if (result >= 0) return result;
guard.unlock();
return forward_ioctl(fd, op, argp);
}
if (type == 0x45 && number == 0x1) {
Logger::log("Hooking ioctl EVIOCGVERSION for event %s\n", event);
int version = 65536;
@@ -3677,6 +3570,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;
@@ -3786,12 +3680,6 @@ static ssize_t read_input(int fd, void *buf, size_t count) {
if (fake.needs_keyframe || snap.resync_seq != fake.resync_seq ||
snap.write_seq < fake.read_seq ||
snap.write_seq - fake.read_seq > FAKE_INPUT_RING_CAPACITY) {
if (fake.mouse) {
fake.read_seq = snap.write_seq;
fake.resync_seq = snap.resync_seq;
fake.needs_keyframe = false;
continue;
}
capture_keyframe(fake, snap);
} else {
size_t events = std::min<uint64_t>(requested, snap.write_seq - fake.read_seq);
@@ -4255,7 +4143,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, ...) {
+1 -2
View File
@@ -184,9 +184,8 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat , bool force) {
VkExtent2D e = levelExtent(extent, l);
if (!makeImage(pyrA_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
if (!makeImage(pyrB_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
if (!makeImage(flowLvl_[l], e, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false;
}
for (int l = flowFinest_; l < kLevels; ++l)
if (!makeImage(flowLvl_[l], levelExtent(extent, l), VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false;
pyrAs_.resize(kLevels);
for (int l = flowFinest_; l < kLevels; ++l)
if (!makeImage(pyrAs_[l], levelExtent(extent, l), VK_FORMAT_R32_SFLOAT, lumaUsage)) return false;
@@ -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_);
@@ -437,7 +439,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
makeUbo(dd, dev, mp, sizeof(FlowRegUBO), S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr);
if (S.flowRegUboPtr) { FlowRegUBO fu{cfg.frDt, cfg.frLambda, cfg.frEdge, cfg.frEps}; std::memcpy(S.flowRegUboPtr, &fu, sizeof(fu)); }
S.flowUbo.resize(kLevels); S.flowUboMem.resize(kLevels); S.flowUboPtr.resize(kLevels);
for (int l = fg->flowFinest(); l < kLevels; ++l) {
for (int l = 0; l < kLevels; ++l) {
makeUbo(dd, dev, mp, sizeof(FlowUBO), S.flowUbo[l], S.flowUboMem[l], S.flowUboPtr[l]);
if (S.flowUboPtr[l]) { FlowUBO u{cfg.flowScale, (uint32_t)l, 0.6f, 1.6f}; std::memcpy(S.flowUboPtr[l], &u, sizeof(u)); }
}
@@ -462,12 +464,13 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
}
S.sFlow.resize(kLevels);
for (int l = fg->flowFinest(); l < kLevels; ++l) {
for (int l = 0; l < kLevels; ++l) {
S.sFlow[l] = allocSet(dd, dev, pool, pFlow.setLayout);
int pred = (l + 1 < kLevels) ? l + 1 : l;
VkImageView currLvl = (l >= fg->flowFinest()) ? pyrAs[l].view : pyrA[l].view;
wUbo(dd, dev, S.sFlow[l], 0, S.flowUbo[l], sizeof(FlowUBO));
wImg(dd, dev, S.sFlow[l], 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[l].view, samp, GEN);
wImg(dd, dev, S.sFlow[l], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrAs[l].view, samp, GEN);
wImg(dd, dev, S.sFlow[l], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, currLvl, samp, GEN);
wImg(dd, dev, S.sFlow[l], 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[pred].view, samp, GEN);
wImg(dd, dev, S.sFlow[l], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowLvl[l].view, VK_NULL_HANDLE, GEN);
}
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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>
-154
View File
@@ -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
-77
View File
@@ -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
-159
View File
@@ -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
-27
View File
@@ -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
+2
View File
@@ -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();
+6 -27
View File
@@ -3,9 +3,13 @@
#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 };
enum venc_msg { VENC_CONFIG = 1, VENC_FRAME = 2, VENC_DONE = 3, VENC_BITRATE = 4, VENC_TOUCH = 5 };
enum venc_touch_action { VENC_TOUCH_DOWN = 0, VENC_TOUCH_MOVE = 1, VENC_TOUCH_UP = 2 };
enum venc_codec { VENC_H264 = 0, VENC_HEVC = 1 };
@@ -41,36 +45,11 @@ 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;
uint32_t x, y;
};
struct vdec_config {
uint32_t type;
uint32_t codec;
uint32_t width, height;
uint32_t stride, rows;
uint32_t bitstreams, bitstream_size;
};
struct vdec_capture {
uint32_t type;
uint32_t index;
uint32_t size;
};
struct vdec_packet {
uint32_t type;
uint32_t input, bytes, capture;
int64_t pts_us;
};
struct vdec_result {
uint32_t type;
int32_t status;
uint32_t frame;
};
#endif
+3 -242
View File
@@ -3,7 +3,6 @@
#include <jni.h>
#include <media/NdkMediaCodec.h>
#include <media/NdkMediaFormat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
@@ -12,7 +11,6 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cerrno>
#include <cstring>
#include <string>
@@ -33,8 +31,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) {
@@ -47,218 +45,6 @@ int32_t avc_profile(int32_t v4l2) {
}
}
struct DmaBufSync {
uint64_t flags;
};
constexpr unsigned long kDmaBufIoctlSync = _IOW('b', 0, DmaBufSync);
constexpr uint64_t kDmaBufSyncWrite = 2, kDmaBufSyncStart = 0, kDmaBufSyncEnd = 4;
constexpr int32_t kColorFormatYuv420Planar = 19;
class Decoder {
public:
~Decoder() { release(); }
void release() {
if (codec_) {
AMediaCodec_stop(codec_);
AMediaCodec_delete(codec_);
codec_ = nullptr;
}
if (bitstream_) munmap((void *)bitstream_, bitstream_size_);
bitstream_ = nullptr;
for (auto &c : captures_) drop(c);
}
bool configure(const vdec_config &c, int fd) {
release();
if (c.width < 16 || c.height < 16 || c.stride < c.width || c.rows < c.height || c.bitstreams == 0 ||
c.bitstreams > VENC_MAX_BUFFERS || c.bitstream_size == 0) {
LOG("bad decoder config: %ux%u stride %u rows %u, %u bitstreams", c.width, c.height, c.stride, c.rows,
c.bitstreams);
return false;
}
bitstream_size_ = (size_t)c.bitstreams * c.bitstream_size;
void *p = mmap(nullptr, bitstream_size_, PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
LOG("could not map the bitstream buffers: %s", strerror(errno));
return false;
}
bitstream_ = (const uint8_t *)p;
config_ = c;
codec_ = create(c, true);
if (!codec_) codec_ = create(c, false);
if (!codec_) {
LOG("no %s decoder for %ux%u", c.codec == VENC_HEVC ? "HEVC" : "H.264", c.width, c.height);
release();
return false;
}
src_stride_ = (int32_t)c.width;
src_rows_ = (int32_t)c.height;
src_width_ = (int32_t)c.width;
src_height_ = (int32_t)c.height;
src_planar_ = false;
shown_ = false;
using GetName = media_status_t (*)(AMediaCodec *, char **);
using ReleaseName = void (*)(AMediaCodec *, char *);
static auto get_name = (GetName)dlsym(RTLD_DEFAULT, "AMediaCodec_getName");
static auto release_name = (ReleaseName)dlsym(RTLD_DEFAULT, "AMediaCodec_releaseName");
char *name = nullptr;
if (get_name && get_name(codec_, &name) != AMEDIA_OK) name = nullptr;
LOG("decoding %s %ux%u with %s", c.codec == VENC_HEVC ? "HEVC" : "H.264", c.width, c.height,
name ? name : "MediaCodec");
if (name && release_name) release_name(codec_, name);
return true;
}
void capture(const vdec_capture &c, int fd) {
if (c.index >= VENC_MAX_BUFFERS) {
close(fd);
return;
}
Capture &cap = captures_[c.index];
drop(cap);
void *p = mmap(nullptr, c.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
LOG("could not map capture buffer %u: %s", c.index, strerror(errno));
close(fd);
return;
}
cap = {fd, (uint8_t *)p, c.size};
}
bool decode(const vdec_packet &p, bool *frame) {
*frame = false;
if (!codec_ || p.capture >= VENC_MAX_BUFFERS || !captures_[p.capture].data || p.input >= config_.bitstreams ||
p.bytes == 0 || p.bytes > config_.bitstream_size)
return false;
auto start = std::chrono::steady_clock::now();
ssize_t in = AMediaCodec_dequeueInputBuffer(codec_, 20000);
if (in < 0) return false;
size_t capacity = 0;
uint8_t *dst = AMediaCodec_getInputBuffer(codec_, (size_t)in, &capacity);
if (!dst || capacity < p.bytes) {
AMediaCodec_queueInputBuffer(codec_, (size_t)in, 0, 0, 0, 0);
return false;
}
memcpy(dst, bitstream_ + (size_t)p.input * config_.bitstream_size, p.bytes);
if (AMediaCodec_queueInputBuffer(codec_, (size_t)in, 0, p.bytes, (uint64_t)std::max<int64_t>(p.pts_us, 0), 0) !=
AMEDIA_OK)
return false;
auto wait = std::chrono::milliseconds(shown_ ? 25 : 250);
while (std::chrono::steady_clock::now() - start < wait) {
AMediaCodecBufferInfo info;
ssize_t o = AMediaCodec_dequeueOutputBuffer(codec_, &info, 2000);
if (o == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
read_format();
continue;
}
if (o < 0) continue;
size_t size = 0;
uint8_t *data = AMediaCodec_getOutputBuffer(codec_, (size_t)o, &size);
if (data && info.size > 0 && (size_t)info.offset + (size_t)info.size <= size)
*frame = copy(data + info.offset, (size_t)info.size, captures_[p.capture]) || *frame;
AMediaCodec_releaseOutputBuffer(codec_, (size_t)o, false);
if (info.presentationTimeUs >= p.pts_us) break;
}
shown_ = shown_ || *frame;
return true;
}
private:
struct Capture {
int fd = -1;
uint8_t *data = nullptr;
size_t size = 0;
};
static void drop(Capture &c) {
if (c.data) munmap(c.data, c.size);
if (c.fd >= 0) close(c.fd);
c = Capture();
}
AMediaCodec *create(const vdec_config &c, bool low_latency) {
const char *mime = c.codec == VENC_HEVC ? "video/hevc" : "video/avc";
AMediaCodec *codec = AMediaCodec_createDecoderByType(mime);
if (!codec) return nullptr;
AMediaFormat *f = AMediaFormat_new();
AMediaFormat_setString(f, AMEDIAFORMAT_KEY_MIME, mime);
AMediaFormat_setInt32(f, AMEDIAFORMAT_KEY_WIDTH, (int32_t)c.width);
AMediaFormat_setInt32(f, AMEDIAFORMAT_KEY_HEIGHT, (int32_t)c.height);
AMediaFormat_setInt32(f, AMEDIAFORMAT_KEY_COLOR_FORMAT, kColorFormatYuv420SemiPlanar);
AMediaFormat_setInt32(f, AMEDIAFORMAT_KEY_MAX_INPUT_SIZE, (int32_t)c.bitstream_size);
if (low_latency) {
AMediaFormat_setInt32(f, "low-latency", 1);
AMediaFormat_setInt32(f, "vendor.qti-ext-dec-low-latency.enable", 1);
AMediaFormat_setInt32(f, "vendor.qti-ext-dec-picture-order.enable", 1);
AMediaFormat_setInt32(f, "priority", 0);
AMediaFormat_setFloat(f, "operating-rate", 120.0f);
}
media_status_t s = AMediaCodec_configure(codec, f, nullptr, nullptr, 0);
AMediaFormat_delete(f);
if (s == AMEDIA_OK) s = AMediaCodec_start(codec);
if (s != AMEDIA_OK) {
AMediaCodec_delete(codec);
return nullptr;
}
return codec;
}
void read_format() {
AMediaFormat *f = AMediaCodec_getOutputFormat(codec_);
if (!f) return;
int32_t v;
if (AMediaFormat_getInt32(f, AMEDIAFORMAT_KEY_WIDTH, &v) && v > 0) src_width_ = v;
if (AMediaFormat_getInt32(f, AMEDIAFORMAT_KEY_HEIGHT, &v) && v > 0) src_height_ = v;
src_stride_ = AMediaFormat_getInt32(f, "stride", &v) && v >= src_width_ ? v : src_width_;
src_rows_ = AMediaFormat_getInt32(f, "slice-height", &v) && v >= src_height_ ? v : src_height_;
src_planar_ = AMediaFormat_getInt32(f, AMEDIAFORMAT_KEY_COLOR_FORMAT, &v) && v == kColorFormatYuv420Planar;
LOG("decoder output %dx%d stride %d rows %d %s", src_width_, src_height_, src_stride_, src_rows_,
src_planar_ ? "planar" : "semi-planar");
AMediaFormat_delete(f);
}
bool copy(const uint8_t *src, size_t size, const Capture &cap) {
size_t luma = (size_t)src_stride_ * src_rows_;
size_t chroma = src_planar_ ? 2 * (size_t)(src_stride_ / 2) * (src_rows_ / 2) : (size_t)src_stride_ * (src_rows_ / 2);
if (size < luma + chroma) return false;
uint32_t w = std::min<uint32_t>((uint32_t)src_width_, config_.stride);
uint32_t h = std::min<uint32_t>((uint32_t)src_height_, config_.rows);
if ((size_t)config_.stride * config_.rows * 3 / 2 > cap.size) return false;
DmaBufSync sync = {kDmaBufSyncStart | kDmaBufSyncWrite};
ioctl(cap.fd, kDmaBufIoctlSync, &sync);
uint8_t *y = cap.data, *uv = cap.data + (size_t)config_.stride * config_.rows;
for (uint32_t row = 0; row < h; row++)
memcpy(y + (size_t)row * config_.stride, src + (size_t)row * src_stride_, w);
const uint8_t *su = src + luma;
for (uint32_t row = 0; row < h / 2; row++) {
uint8_t *d = uv + (size_t)row * config_.stride;
if (!src_planar_) {
memcpy(d, su + (size_t)row * src_stride_, w & ~1u);
continue;
}
const uint8_t *u = su + (size_t)row * (src_stride_ / 2);
const uint8_t *vv = su + (size_t)(src_stride_ / 2) * (src_rows_ / 2) + (size_t)row * (src_stride_ / 2);
for (uint32_t x = 0; x < w / 2; x++) {
d[2 * x] = u[x];
d[2 * x + 1] = vv[x];
}
}
sync.flags = kDmaBufSyncEnd | kDmaBufSyncWrite;
ioctl(cap.fd, kDmaBufIoctlSync, &sync);
return true;
}
AMediaCodec *codec_ = nullptr;
vdec_config config_ = {};
const uint8_t *bitstream_ = nullptr;
size_t bitstream_size_ = 0;
Capture captures_[VENC_MAX_BUFFERS];
int32_t src_width_ = 0, src_height_ = 0, src_stride_ = 0, src_rows_ = 0;
bool src_planar_ = false;
bool shown_ = false;
};
class Session {
public:
explicit Session(int fd) : fd_(fd) {}
@@ -299,24 +85,6 @@ public:
memcpy(&b, buf, sizeof b);
set_bitrate(b.bitrate);
for (int i = 0; i < nfds; i++) close(fds[i]);
} else if (type == VDEC_CONFIG && n == (ssize_t)sizeof(vdec_config)) {
vdec_config c;
memcpy(&c, buf, sizeof c);
bool ok = nfds == 1 && decoder_.configure(c, fds[0]);
for (int i = 0; i < nfds; i++) close(fds[i]);
decoded(ok ? 0 : -1, false);
} else if (type == VDEC_CAPTURE && n == (ssize_t)sizeof(vdec_capture)) {
vdec_capture c;
memcpy(&c, buf, sizeof c);
if (nfds == 1) decoder_.capture(c, fds[0]);
else for (int i = 0; i < nfds; i++) close(fds[i]);
} else if (type == VDEC_PACKET && n == (ssize_t)sizeof(vdec_packet)) {
vdec_packet p;
memcpy(&p, buf, sizeof p);
bool frame = false;
bool ok = decoder_.decode(p, &frame);
for (int i = 0; i < nfds; i++) close(fds[i]);
decoded(ok ? 0 : -1, frame);
} else {
LOG("unexpected message %u of %zd bytes; closing", type, n);
for (int i = 0; i < nfds; i++) close(fds[i]);
@@ -349,13 +117,7 @@ private:
send(fd_, &d, sizeof d, MSG_NOSIGNAL);
}
void decoded(int32_t status, bool frame) {
vdec_result r = {VDEC_RESULT, status, frame ? 1u : 0u};
send(fd_, &r, sizeof r, MSG_NOSIGNAL);
}
void release() {
decoder_.release();
if (codec_) {
AMediaCodec_stop(codec_);
AMediaCodec_delete(codec_);
@@ -530,7 +292,6 @@ private:
int32_t dst_stride_ = 0, dst_rows_ = 0;
std::vector<Mapping> inputs_, outputs_;
std::vector<uint8_t> header_;
Decoder decoder_;
};
std::atomic<int> g_listener{-1};
@@ -549,7 +310,7 @@ void serve(int listener) {
}
}
}
} // namespace
extern "C" JNIEXPORT jboolean JNICALL
Java_com_droiddeck_launcher_session_VideoEncodeBridge_nativeStart(JNIEnv *env, jobject, jstring jpath) {
+13 -11
View File
@@ -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.
}
}
}
+1
View File
@@ -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;
}
+4
View File
@@ -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
@@ -36,8 +33,6 @@ add_library(droiddeckwayland SHARED
generated/banner-ahb-v1-protocol.c
src/framegen_bridge.c
src/framegen_jni.c
src/hudframes.c
src/hudframes_jni.c
)
target_compile_options(droiddeckwayland PRIVATE
$<$<COMPILE_LANGUAGE:C>:-Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers>)
@@ -46,7 +41,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)
@@ -101,5 +95,4 @@ target_link_libraries(droiddeckwayland
dl
log
android
nativewindow
)
@@ -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;
@@ -375,8 +371,8 @@ void ahb_swapchain_set_mode(int on, int live) {
: "; gralloc frames still in flight stay on the layer (or take the copy path) until then");
else
droiddeck_log("layer", "zero-copy: %s at launch (%s)", on ? "on" : "off",
on ? "HDR session: games present their own gralloc buffers on the display layer"
: "only HDR sessions turn it on");
on ? "DROIDDECK_WAYLAND_ZERO_COPY=1: games present their own gralloc buffers on the display layer"
: "the drawer's Zero-copy presentation switch turns it on live");
if (live) wl_display_flush_clients(droiddeck_get_display());
droiddeck_request_redraw();
}
+1 -7
View File
@@ -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,
File diff suppressed because it is too large. Load diff
@@ -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];
};
@@ -1,97 +0,0 @@
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include "hudframes.h"
#define HUD_FRAMES_MAGIC 0x46484444u
#define HUD_FRAMES_HEADER 128u
#define HUD_FRAMES_ENTRY 16u
struct hud_frames {
const uint8_t *map;
size_t size;
uint64_t seen;
};
static uint32_t load32(const uint8_t *p) { return __atomic_load_n((const uint32_t *)p, __ATOMIC_ACQUIRE); }
static uint64_t load64(const uint8_t *p) { return __atomic_load_n((const uint64_t *)p, __ATOMIC_ACQUIRE); }
struct hud_frames *hud_frames_open(const char *path) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) return NULL;
struct stat st;
void *map = MAP_FAILED;
if (fstat(fd, &st) == 0 && st.st_size >= (off_t)HUD_FRAMES_HEADER)
map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_SHARED, fd, 0);
close(fd);
if (map == MAP_FAILED) return NULL;
struct hud_frames *h = calloc(1, sizeof(*h));
if (!h) {
munmap(map, (size_t)st.st_size);
return NULL;
}
h->map = map;
h->size = (size_t)st.st_size;
return h;
}
void hud_frames_close(struct hud_frames *h) {
if (!h) return;
munmap((void *)h->map, h->size);
free(h);
}
static uint32_t capacity(const struct hud_frames *h) {
if (load32(h->map) != HUD_FRAMES_MAGIC) return 0;
uint32_t cap;
memcpy(&cap, h->map + 8, 4);
if (cap == 0 || HUD_FRAMES_HEADER + (size_t)cap * HUD_FRAMES_ENTRY > h->size) return 0;
return cap;
}
int hud_frames_poll(struct hud_frames *h, int64_t *frametimes, int64_t *times, int max) {
uint32_t cap = capacity(h);
if (!cap || max <= 0) return -1;
uint64_t count = load64(h->map + 16);
if (count < h->seen) h->seen = 0;
uint64_t from = h->seen;
if (count - from > cap) from = count - cap;
if (count - from > (uint64_t)max) from = count - (uint64_t)max;
int n = 0;
for (uint64_t i = from; i < count; i++, n++) {
const uint8_t *e = h->map + HUD_FRAMES_HEADER + (i % cap) * HUD_FRAMES_ENTRY;
memcpy(&frametimes[n], e, 8);
memcpy(&times[n], e + 8, 8);
}
uint64_t after = load64(h->map + 16);
uint64_t valid = after > cap ? after - cap : 0;
if (from < valid) {
int skip = (int)(valid - from < (uint64_t)n ? valid - from : (uint64_t)n);
memmove(frametimes, frametimes + skip, (size_t)(n - skip) * sizeof(int64_t));
memmove(times, times + skip, (size_t)(n - skip) * sizeof(int64_t));
n -= skip;
}
h->seen = count;
return n;
}
int hud_frames_info(const struct hud_frames *h, int32_t *info, char *engine, size_t engine_len) {
if (!capacity(h) || engine_len == 0) return 0;
for (int tries = 0; tries < 8; tries++) {
uint32_t seq = load32(h->map + 24);
if (seq & 1u) continue;
memcpy(&info[0], h->map + 12, 4);
memcpy(&info[1], h->map + 28, 16);
size_t n = engine_len - 1 < 40 ? engine_len - 1 : 40;
memcpy(engine, h->map + 44, n);
engine[n] = '\0';
__atomic_thread_fence(__ATOMIC_ACQUIRE);
if (__atomic_load_n((const uint32_t *)(h->map + 24), __ATOMIC_RELAXED) == seq) return 1;
}
return 0;
}
@@ -1,14 +0,0 @@
#ifndef DROIDDECK_HUDFRAMES_H
#define DROIDDECK_HUDFRAMES_H
#include <stddef.h>
#include <stdint.h>
struct hud_frames;
struct hud_frames *hud_frames_open(const char *path);
void hud_frames_close(struct hud_frames *h);
int hud_frames_poll(struct hud_frames *h, int64_t *frametimes, int64_t *times, int max);
int hud_frames_info(const struct hud_frames *h, int32_t *info, char *engine, size_t engine_len);
#endif
@@ -1,55 +0,0 @@
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include "hudframes.h"
JNIEXPORT jlong JNICALL
Java_com_droiddeck_launcher_hud_HudFrames_nativeOpen(JNIEnv *env, jclass clazz, jstring path) {
(void)clazz;
const char *p = path ? (*env)->GetStringUTFChars(env, path, NULL) : NULL;
if (!p) return 0;
struct hud_frames *h = hud_frames_open(p);
(*env)->ReleaseStringUTFChars(env, path, p);
return (jlong)(intptr_t)h;
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_hud_HudFrames_nativeClose(JNIEnv *env, jclass clazz, jlong handle) {
(void)env; (void)clazz;
hud_frames_close((struct hud_frames *)(intptr_t)handle);
}
JNIEXPORT jint JNICALL
Java_com_droiddeck_launcher_hud_HudFrames_nativePoll(JNIEnv *env, jclass clazz, jlong handle,
jlongArray frametimes, jlongArray times) {
(void)clazz;
struct hud_frames *h = (struct hud_frames *)(intptr_t)handle;
if (!h || !frametimes || !times) return -1;
jsize max = (*env)->GetArrayLength(env, frametimes);
jsize max2 = (*env)->GetArrayLength(env, times);
if (max2 < max) max = max2;
if (max <= 0) return -1;
int64_t *ft = malloc(sizeof(int64_t) * (size_t)max);
int64_t *tm = malloc(sizeof(int64_t) * (size_t)max);
int n = ft && tm ? hud_frames_poll(h, ft, tm, max) : -1;
if (n > 0) {
(*env)->SetLongArrayRegion(env, frametimes, 0, n, (const jlong *)ft);
(*env)->SetLongArrayRegion(env, times, 0, n, (const jlong *)tm);
}
free(ft);
free(tm);
return n;
}
JNIEXPORT jstring JNICALL
Java_com_droiddeck_launcher_hud_HudFrames_nativeInfo(JNIEnv *env, jclass clazz, jlong handle, jintArray info) {
(void)clazz;
const struct hud_frames *h = (const struct hud_frames *)(intptr_t)handle;
if (!h || !info || (*env)->GetArrayLength(env, info) < 5) return NULL;
int32_t values[5];
char engine[41];
if (!hud_frames_info(h, values, engine, sizeof(engine))) return NULL;
(*env)->SetIntArrayRegion(env, info, 0, 5, (const jint *)values);
return (*env)->NewStringUTF(env, engine);
}
@@ -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") ||
+21 -74
View File
@@ -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
+2 -2
View File
@@ -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) \
@@ -21,7 +21,7 @@
X(BindImageMemory) X(DestroyImage) X(FreeMemory) X(AcquireNextImageKHR) \
X(ResetCommandBuffer) X(BeginCommandBuffer) X(CmdPipelineBarrier) X(CmdBlitImage) \
X(EndCommandBuffer) X(ResetFences) X(QueueSubmit) X(QueuePresentKHR) X(WaitForFences) \
X(QueueWaitIdle) X(GetMemoryFdPropertiesKHR) X(GetAndroidHardwareBufferPropertiesANDROID) \
X(QueueWaitIdle) X(GetMemoryFdPropertiesKHR) \
X(GetImageSubresourceLayout) X(MapMemory) X(UnmapMemory) X(CmdClearColorImage) \
\
X(CreateShaderModule) X(DestroyShaderModule) X(CreatePipelineLayout) X(DestroyPipelineLayout) \
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);
@@ -19,14 +16,10 @@ enum vkp_align { VKP_ALIGN_CENTER = 0, VKP_ALIGN_TOP = 1, VKP_ALIGN_BOTTOM = 2,
void vk_present_set_scale_mode(int mode, int alignment);
int vkp_game_picture_rect(float rect[4]);
int vkp_output_to_scene(double ox, double oy, double *sx, double *sy);
int vkp_scene_to_output(double sx, double sy, double *ox, double *oy);
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,14 +32,8 @@ 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;
struct vkp_image *vkp_image_from_ahb(struct AHardwareBuffer *ahb);
void vkp_wait_idle(void);
void vkp_image_upload_shm(struct vkp_image *img, const void *data, int stride);
int vkp_image_width(const struct vkp_image *img);
@@ -66,10 +53,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);
@@ -9,7 +9,6 @@
#include <sys/resource.h>
#include <android/log.h>
#include <android/native_window_jni.h>
#include <android/hardware_buffer_jni.h>
#include "vk_present.h"
#include "droiddeck_ext.h"
#include "ahb_swapchain.h"
@@ -25,21 +24,12 @@ extern void droiddeck_wayland_send_pointer(int action, int x, int y);
extern void droiddeck_wayland_send_touch(int action, int pointer_id, int x, int y);
extern int droiddeck_cursor_snapshot(int *out, int cap);
extern int droiddeck_cursor_serial(void);
extern int droiddeck_hud_frame_times(float *out, int len, int64_t *total);
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);
extern unsigned droiddeck_stream_frames(void);
extern void droiddeck_wayland_output_resized(void);
extern void droiddeck_wayland_set_output_refresh_rate(int mhz);
extern volatile int g_fps_limit;
extern volatile int g_release_unpaced;
extern volatile int g_render_on_commit;
extern volatile int g_hide_shell;
extern volatile int g_zero_copy;
extern volatile unsigned g_zero_copy_last;
@@ -216,24 +206,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) {
@@ -291,58 +263,18 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot(
return n;
}
JNIEXPORT jlong JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHudFrameTimes(JNIEnv *env, jclass clazz, jfloatArray out) {
(void)clazz;
if (!out) return 0;
jint cap = (*env)->GetArrayLength(env, out);
float *buf = cap > 0 ? malloc(sizeof(float) * (size_t)cap) : NULL;
if (!buf) return 0;
int64_t total = 0;
int n = droiddeck_hud_frame_times(buf, cap, &total);
if (n > 0) (*env)->SetFloatArrayRegion(env, out, 0, n, buf);
free(buf);
return (jlong)total;
}
JNIEXPORT jint JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSerial(JNIEnv *env, jclass clazz) {
(void)env; (void)clazz;
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) {
droiddeck_wayland_send_key(evdev, state);
}
JNIEXPORT jboolean JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeFocusTakesTouch(
JNIEnv *env, jclass clazz) {
(void)env; (void)clazz;
return droiddeck_focus_takes_touch() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorInputPos(
JNIEnv *env, jclass clazz, jintArray out) {
(void)clazz;
if (!out || (*env)->GetArrayLength(env, out) < 2) return JNI_FALSE;
int pos[2];
if (!droiddeck_cursor_input_pos(&pos[0], &pos[1])) return JNI_FALSE;
jint jpos[2] = {pos[0], pos[1]};
(*env)->SetIntArrayRegion(env, out, 0, 2, jpos);
return JNI_TRUE;
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendSceneInput(
JNIEnv *env, jclass clazz, jint type, jint a, jint b) {
@@ -635,16 +567,9 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLookName(JNIEnv *
free(s);
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetRenderOnCommit(JNIEnv *env, jclass clazz, jboolean on) {
g_render_on_commit = on ? 1 : 0;
__android_log_print(ANDROID_LOG_INFO, TAG, "render on commit %s", on ? "on" : "off");
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFpsLimit(JNIEnv *env, jclass clazz, jint fps) {
g_fps_limit = fps > 0 ? fps : 0;
g_release_unpaced = fps < 0;
__android_log_print(ANDROID_LOG_INFO, TAG, "fps limit %d", fps);
}
@@ -716,6 +641,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);
}
@@ -741,20 +667,3 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardSpatialP
(*env)->GetFloatArrayRegion(env, pos, 0, 3, p);
vkp_cardboard_set_spatial_pose(m, p, live ? 1 : 0);
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStreamMode(JNIEnv *env, jclass clazz, jboolean on) {
droiddeck_stream_set(on ? 1 : 0);
}
JNIEXPORT void JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStreamFrame(JNIEnv *env, jclass clazz, jobject hardwareBuffer) {
if (!hardwareBuffer) return;
AHardwareBuffer *ahb = AHardwareBuffer_fromHardwareBuffer(env, hardwareBuffer);
if (ahb) droiddeck_stream_frame(ahb);
}
JNIEXPORT jint JNICALL
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStreamFrames(JNIEnv *env, jclass clazz) {
return (jint)droiddeck_stream_frames();
}
@@ -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,8 @@ 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
@@ -28,18 +25,8 @@ class App : Application() {
if (android.os.Build.VERSION.SDK_INT >= 28) android.webkit.WebView.setDataDirectorySuffix("xbox")
return
}
if (process == "$packageName:xboxcloud") {
if (android.os.Build.VERSION.SDK_INT >= 28) android.webkit.WebView.setDataDirectorySuffix("xboxcloud")
return
}
if (process == "$packageName:xcloudnative") {
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
if (process == "$packageName:vr_recovery" || process == "$packageName:rgb_diagnostic") return
CrashHandler.install(this)
com.droiddeck.launcher.core.SteamIntegration.syncShortcutsListing(this)
com.droiddeck.launcher.session.QuestExperimental.initialize(this)
com.droiddeck.launcher.session.SessionPrefs.applyNtsyncDefault(this)
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
@@ -50,19 +37,16 @@ class App : Application() {
com.droiddeck.launcher.glasses.Glasses.init(this)
com.droiddeck.launcher.glasses.GlassesMenuMirror.initialize(this)
BwrapSpawner.start(this)
com.droiddeck.launcher.runtime.RootSpawner.start(this)
com.droiddeck.launcher.frontend.GameFileSync.start(this)
com.droiddeck.launcher.stores.StoresState.init(this)
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.gog.GogBackend)
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)
@@ -603,19 +428,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
}
fun selectDriver(id: String, linux: Boolean) {
if (!linux && id == TurnipDriver.AUTO) {
DriverBundle.selectDisplay(activity, id)
pairModeOpen = false
lastAutoCheck = 0L
setMode(auto = true)
refreshDrivers()
return
}
val bundle = DriverBundle.containing(activity, id, linux)
when {
bundle != null -> useBundle(bundle)
linux -> SessionPrefs.setLinuxDriver(activity, id)
else -> DriverBundle.selectDisplay(activity, id)
else -> SessionPrefs.setAndroidDriver(activity, id)
}
pairModeOpen = false
setMode(auto = false, mix = true)
File diff suppressed because it is too large. Load diff
@@ -13,7 +13,7 @@ class HomeActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestedOrientation = com.droiddeck.launcher.core.AppUiPrefs.orientation(this)
requestedOrientation = com.droiddeck.launcher.session.SessionPrefs.orientation(this)
routeHome()
}
@@ -6,44 +6,20 @@ import android.graphics.Bitmap
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.graphics.Rect
import android.os.Process
import android.os.UserHandle
import android.os.UserManager
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.core.graphics.drawable.toBitmap
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.session.SessionState
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,
val packageName: String,
val className: String,
val icon: Bitmap?,
val system: Boolean = false,
val game: Boolean = false,
val installedAt: Long = 0L,
val updatedAt: Long = 0L,
val user: UserHandle? = null,
val userSerial: Long = 0L,
) {
val component get() = ComponentName(packageName, className)
val key get() = "$packageName/$className" + if (userSerial != 0L) "#$userSerial" else ""
val work get() = userSerial != 0L
}
)
fun isHomeScreenEnabled(context: Context): Boolean =
context.packageManager.getComponentEnabledSetting(ComponentName(context, HomeActivity::class.java)) ==
@@ -57,24 +33,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)
@@ -111,133 +69,33 @@ object HomeApp {
fun settingsIntent(): Intent = Intent(Settings.ACTION_HOME_SETTINGS)
fun launchableApps(context: Context): List<LaunchableApp> {
val launcher = context.getSystemService(LauncherApps::class.java)
val users = context.getSystemService(UserManager::class.java)
val pm = context.packageManager
val own = Process.myUserHandle()
val iconPx = (ICON_DP * context.resources.displayMetrics.density).roundToInt()
val apps = users.userProfiles.flatMap { user ->
val serial = if (user == own) 0L else users.getSerialNumberForUser(user)
runCatching { launcher.getActivityList(null, user) }.getOrDefault(emptyList()).mapNotNull { info ->
val component = info.componentName
if (component.packageName == context.packageName && user == own) return@mapNotNull null
val label = info.label?.toString()?.trim().orEmpty().ifEmpty { component.packageName }
val icon = runCatching { info.getBadgedIcon(0).toBitmap(iconPx, iconPx) }.getOrNull()
val app = info.applicationInfo
val updated = if (user == own) runCatching { pm.getPackageInfo(component.packageName, 0).lastUpdateTime }.getOrNull() else null
@Suppress("DEPRECATION")
val game = app.category == ApplicationInfo.CATEGORY_GAME || app.flags and ApplicationInfo.FLAG_IS_GAME != 0
LaunchableApp(
label, component.packageName, component.className, icon,
system = app.flags and (ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0,
game = game,
installedAt = info.firstInstallTime,
updatedAt = updated ?: info.firstInstallTime,
user = user,
userSerial = serial,
)
val launch = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
val apps = pm.queryIntentActivities(launch, 0)
.asSequence()
.mapNotNull { result ->
val activity = result.activityInfo ?: return@mapNotNull null
if (!activity.exported || activity.packageName == context.packageName) return@mapNotNull null
val label = result.loadLabel(pm)?.toString()?.trim().orEmpty().ifEmpty { activity.packageName }
val icon = runCatching { result.loadIcon(pm).toBitmap(48, 48) }.getOrNull()
LaunchableApp(label, activity.packageName, activity.name, icon)
}
.distinctBy { it.packageName }
.toList()
return apps.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
}
fun launch(context: Context, app: LaunchableApp, displayId: Int? = null) {
val intent = Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(ComponentName(app.packageName, app.className))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (displayId == null) {
context.startActivity(intent)
return
}
return apps.distinctBy { it.key }.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
}
fun launch(context: Context, app: LaunchableApp, displayId: Int? = null, bounds: Rect? = null) {
val options = displayId?.let { ActivityOptions.makeBasic().setLaunchDisplayId(it).toBundle() }
val launcher = context.getSystemService(LauncherApps::class.java)
launcher.startMainActivity(app.component, app.user ?: Process.myUserHandle(), bounds, options)
}
fun canUseShortcuts(context: Context): Boolean =
runCatching { context.getSystemService(LauncherApps::class.java).hasShortcutHostPermission() }.getOrDefault(false)
fun shortcuts(context: Context, app: LaunchableApp): List<ShortcutInfo> {
if (!canUseShortcuts(context)) return emptyList()
val query = LauncherApps.ShortcutQuery()
.setPackage(app.packageName)
.setActivity(app.component)
.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST)
return runCatching { context.getSystemService(LauncherApps::class.java).getShortcuts(query, app.user ?: Process.myUserHandle()) }
.getOrNull().orEmpty()
.filter { it.isEnabled }
.sortedWith(compareBy<ShortcutInfo> { if (it.isDeclaredInManifest) 0 else 1 }.thenBy { it.rank })
.take(5)
}
fun shortcutIcon(context: Context, info: ShortcutInfo, px: Int = (ICON_DP * context.resources.displayMetrics.density).roundToInt()): Bitmap? = runCatching {
context.getSystemService(LauncherApps::class.java)
.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
}
}
fun openAppInfo(context: Context, app: LaunchableApp) {
context.getSystemService(LauncherApps::class.java)
.startAppDetailsActivity(app.component, app.user ?: Process.myUserHandle(), null, null)
}
fun canUninstall(app: LaunchableApp): Boolean = !app.system && !app.work
fun uninstall(context: Context, app: LaunchableApp) {
context.startActivity(
Intent(Intent.ACTION_DELETE, Uri.fromParts("package", app.packageName, null))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
)
val options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
context.startActivity(intent, options.toBundle())
}
fun isActiveSteamSession(): Boolean =
@@ -1,65 +0,0 @@
package com.droiddeck.launcher
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import org.json.JSONArray
import org.json.JSONObject
import java.util.UUID
object HomeFolders {
data class Folder(val id: String, val name: String, val apps: List<String>)
private const val PREFS = "home_launcher"
private const val KEY = "folders"
var folders by mutableStateOf<List<Folder>>(emptyList())
private set
private var loaded = false
fun load(context: Context) {
if (loaded) return
loaded = true
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, null) ?: return
folders = runCatching {
val array = JSONArray(raw)
(0 until array.length()).map { i ->
val o = array.getJSONObject(i)
val apps = o.getJSONArray("apps")
Folder(o.getString("id"), o.getString("name"), (0 until apps.length()).map { apps.getString(it) })
}
}.getOrDefault(emptyList())
}
fun create(context: Context, name: String, appKey: String): Folder {
val folder = Folder(UUID.randomUUID().toString(), name, listOf(appKey))
update(context, without(appKey) + folder)
return folder
}
fun add(context: Context, folderId: String, appKey: String) =
update(context, without(appKey).map { if (it.id == folderId) it.copy(apps = it.apps + appKey) else it })
fun remove(context: Context, appKey: String) = update(context, without(appKey))
fun rename(context: Context, folderId: String, name: String) =
update(context, folders.map { if (it.id == folderId) it.copy(name = name) else it })
fun delete(context: Context, folderId: String) = update(context, folders.filterNot { it.id == folderId })
fun migrateMembers(context: Context, keyFor: (String) -> String?) {
if (folders.none { f -> f.apps.any { '/' !in it } }) return
update(context, folders.map { f -> f.copy(apps = f.apps.map { if ('/' in it) it else keyFor(it) ?: it }.distinct()) })
}
private fun without(appKey: String): List<Folder> =
folders.map { it.copy(apps = it.apps - appKey) }.filter { it.apps.isNotEmpty() }
private fun update(context: Context, next: List<Folder>) {
folders = next
val array = JSONArray()
for (f in next) array.put(JSONObject().put("id", f.id).put("name", f.name).put("apps", JSONArray(f.apps)))
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY, array.toString()).apply()
}
}
@@ -1,26 +0,0 @@
package com.droiddeck.launcher
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
object HomeHidden {
private const val PREFS = "home_launcher"
private const val KEY = "hidden"
var keys by mutableStateOf<Set<String>>(emptySet())
private set
private var loaded = false
fun load(context: Context) {
if (loaded) return
loaded = true
keys = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getStringSet(KEY, emptySet()).orEmpty().toSet()
}
fun set(context: Context, key: String, hidden: Boolean) {
keys = if (hidden) keys + key else keys - key
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putStringSet(KEY, keys).apply()
}
}
@@ -1,74 +0,0 @@
package com.droiddeck.launcher
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.os.Process
import android.os.UserManager
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
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 ""
val label get() = info.shortLabel?.toString()?.trim().orEmpty().ifEmpty { appLabel }
val work get() = userSerial != 0L
}
var pinned by mutableStateOf<List<Pinned>>(emptyList())
private set
private val main = Handler(Looper.getMainLooper())
private var listing = 0
fun refresh(context: Context) {
val app = context.applicationContext
val seq = ++listing
Thread({
val list = runCatching { load(app) }.getOrDefault(emptyList())
main.post { if (seq == listing) pinned = list }
}, "home-pins").start()
}
private fun load(context: Context): List<Pinned> {
if (!HomeApp.canUseShortcuts(context)) return emptyList()
val launcher = context.getSystemService(LauncherApps::class.java)
val users = context.getSystemService(UserManager::class.java)
val pm = context.packageManager
val own = Process.myUserHandle()
val query = LauncherApps.ShortcutQuery().setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
return users.userProfiles.flatMap { user ->
val serial = if (user == own) 0L else users.getSerialNumberForUser(user)
runCatching { launcher.getShortcuts(query, user) }.getOrNull().orEmpty()
.filter { it.isEnabled && it.isPinned }
.map { info ->
val appLabel = runCatching { pm.getApplicationLabel(pm.getApplicationInfo(info.`package`, 0)).toString() }
.getOrDefault(info.`package`)
Pinned(info, HomeApp.shortcutIcon(context, info), appLabel, serial)
}
}
}
fun accept(context: Context, request: LauncherApps.PinItemRequest): Boolean {
val ok = runCatching { request.isValid && request.accept() }.getOrDefault(false)
if (ok) refresh(context)
return ok
}
fun unpin(context: Context, pinned: Pinned) {
val info = pinned.info
val launcher = context.getSystemService(LauncherApps::class.java)
val query = LauncherApps.ShortcutQuery().setPackage(info.`package`).setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
val unpinned = runCatching {
val current = checkNotNull(launcher.getShortcuts(query, info.userHandle))
val keep = current.map { it.id } - info.id
launcher.pinShortcuts(info.`package`, keep, info.userHandle)
}.isSuccess
if (!unpinned) return
listing++
this.pinned = this.pinned.filterNot { it.key == pinned.key }
}
}
@@ -1,316 +0,0 @@
package com.droiddeck.launcher
import android.app.Activity
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.SizeF
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.graphics.drawable.toBitmap
import org.json.JSONArray
import org.json.JSONObject
import kotlin.math.hypot
import kotlin.math.roundToInt
object HomeWidgets {
data class Placed(val id: Int, val widthDp: Int, val heightDp: Int)
data class Provider(
val info: AppWidgetProviderInfo,
val label: String,
val appLabel: String,
val preview: Bitmap?,
val widthDp: Int,
val heightDp: Int,
)
private const val HOST_ID = 0x0DDE
private const val PREFS = "home_launcher"
private const val KEY = "widgets"
private const val REQUEST_BIND = 0x0DD1
private const val REQUEST_CONFIGURE = 0x0DD2
private const val REQUEST_RECONFIGURE = 0x0DD3
const val MIN_WIDTH_DP = 96
const val MIN_HEIGHT_DP = 56
var widgets by mutableStateOf<List<Placed>>(emptyList())
private set
var providers by mutableStateOf<List<Provider>?>(null)
private set
private var loaded = false
private var host: Host? = null
private var pending: AppWidgetProviderInfo? = null
private var pendingId = AppWidgetManager.INVALID_APPWIDGET_ID
private var pinRequest: LauncherApps.PinItemRequest? = null
private val main = Handler(Looper.getMainLooper())
private class Host(context: Context) : AppWidgetHost(context, HOST_ID) {
override fun onCreateView(context: Context, appWidgetId: Int, appWidget: AppWidgetProviderInfo?): AppWidgetHostView =
HomeWidgetView(context)
}
class HomeWidgetView(context: Context) : AppWidgetHostView(context) {
var onLongPress: (() -> Unit)? = null
private var fired = false
private var downX = 0f
private var downY = 0f
private val slop = ViewConfiguration.get(context).scaledTouchSlop
private val longPress = Runnable {
fired = true
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
onLongPress?.invoke()
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
fired = false
downX = ev.x
downY = ev.y
postDelayed(longPress, ViewConfiguration.getLongPressTimeout().toLong())
}
MotionEvent.ACTION_MOVE -> if (hypot(ev.x - downX, ev.y - downY) > slop) removeCallbacks(longPress)
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_DOWN -> removeCallbacks(longPress)
}
return fired
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (fired) {
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) fired = false
return true
}
return super.onTouchEvent(event)
}
}
private fun host(context: Context): Host = host ?: Host(context.applicationContext).also { host = it }
fun load(context: Context) {
if (loaded) return
loaded = true
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, null) ?: return
val manager = AppWidgetManager.getInstance(context)
val parsed = runCatching {
val array = JSONArray(raw)
(0 until array.length()).map { i ->
val o = array.getJSONObject(i)
Placed(o.getInt("id"), o.getInt("w"), o.getInt("h"))
}
}.getOrDefault(emptyList())
val live = parsed.filter { manager.getAppWidgetInfo(it.id) != null }
parsed.filterNot { it in live }.forEach { host(context).deleteAppWidgetId(it.id) }
widgets = live
if (live.size != parsed.size) save(context)
}
fun startListening(context: Context) {
load(context)
runCatching { host(context).startListening() }
}
fun stopListening(context: Context) {
runCatching { host(context).stopListening() }
}
fun info(context: Context, id: Int): AppWidgetProviderInfo? = AppWidgetManager.getInstance(context).getAppWidgetInfo(id)
fun createView(context: Context, id: Int): HomeWidgetView? {
val info = info(context, id) ?: return null
return host(context).createView(context, id, info) as? HomeWidgetView
}
fun applySize(view: AppWidgetHostView, widthDp: Int, heightDp: Int) {
if (Build.VERSION.SDK_INT >= 31) {
view.updateAppWidgetSize(Bundle(), listOf(SizeF(widthDp.toFloat(), heightDp.toFloat())))
} else {
@Suppress("DEPRECATION")
view.updateAppWidgetSize(null, widthDp, heightDp, widthDp, heightDp)
}
}
fun loadProviders(context: Context) {
if (providers != null) return
val app = context.applicationContext
Thread({
val pm = app.packageManager
val manager = AppWidgetManager.getInstance(app)
val dpi = app.resources.displayMetrics.densityDpi
val list = manager.installedProviders.mapNotNull { info ->
runCatching {
val appLabel = runCatching { pm.getApplicationLabel(pm.getApplicationInfo(info.provider.packageName, 0)).toString() }
.getOrDefault(info.provider.packageName)
val drawable = info.loadPreviewImage(app, dpi) ?: info.loadIcon(app, dpi)
val preview = drawable?.let { d ->
val w = d.intrinsicWidth.takeIf { it > 0 } ?: 192
val h = d.intrinsicHeight.takeIf { it > 0 } ?: 192
val scale = minOf(1f, 360f / maxOf(w, h))
d.toBitmap((w * scale).roundToInt().coerceAtLeast(1), (h * scale).roundToInt().coerceAtLeast(1))
}
val (w, h) = defaultSize(app, info)
Provider(info, info.loadLabel(pm)?.toString()?.trim().orEmpty().ifEmpty { appLabel }, appLabel, preview, w, h)
}.getOrNull()
}.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Provider::appLabel).thenBy(String.CASE_INSENSITIVE_ORDER, Provider::label))
main.post { providers = list }
}, "widget-providers").start()
}
fun refreshProviders(context: Context) {
providers = null
loadProviders(context)
}
private fun defaultSize(context: Context, info: AppWidgetProviderInfo): Pair<Int, Int> {
val density = context.resources.displayMetrics.density
var w = (info.minWidth / density).roundToInt()
var h = (info.minHeight / density).roundToInt()
if (Build.VERSION.SDK_INT >= 31) {
if (info.targetCellWidth > 0) w = maxOf(w, info.targetCellWidth * 80)
if (info.targetCellHeight > 0) h = maxOf(h, info.targetCellHeight * 80)
}
return w.coerceIn(160, 560) to h.coerceIn(MIN_HEIGHT_DP, 360)
}
fun minSize(context: Context, id: Int): Pair<Int, Int> {
val info = info(context, id) ?: return MIN_WIDTH_DP to MIN_HEIGHT_DP
val density = context.resources.displayMetrics.density
val w = (if (info.minResizeWidth > 0) info.minResizeWidth else info.minWidth) / density
val h = (if (info.minResizeHeight > 0) info.minResizeHeight else info.minHeight) / density
return w.roundToInt().coerceAtLeast(MIN_WIDTH_DP) to h.roundToInt().coerceAtLeast(MIN_HEIGHT_DP)
}
fun add(activity: Activity, provider: Provider) {
if (pendingId == AppWidgetManager.INVALID_APPWIDGET_ID) add(activity, provider.info, null)
}
fun addPinned(activity: Activity, request: LauncherApps.PinItemRequest): Boolean {
if (pendingId != AppWidgetManager.INVALID_APPWIDGET_ID) return false
val info = request.getAppWidgetProviderInfo(activity) ?: return false
load(activity)
add(activity, info, request)
return pending != null
}
private fun add(activity: Activity, info: AppWidgetProviderInfo, request: LauncherApps.PinItemRequest?) {
val h = host(activity)
val id = h.allocateAppWidgetId()
pending = info
pendingId = id
pinRequest = request
val manager = AppWidgetManager.getInstance(activity)
val bound = runCatching { manager.bindAppWidgetIdIfAllowed(id, info.profile, info.provider, null) }.getOrDefault(false)
if (bound) {
bound(activity, id, info)
return
}
val bind = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, info.profile)
@Suppress("DEPRECATION")
runCatching { activity.startActivityForResult(bind, REQUEST_BIND) }.onFailure { abandon(activity) }
}
private fun bound(activity: Activity, id: Int, info: AppWidgetProviderInfo) {
val request = pinRequest
if (request == null) {
configure(activity, id, info)
return
}
val accepted = runCatching { request.isValid && request.accept(Bundle().apply { putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, id) }) }
.getOrDefault(false)
if (accepted) place(activity, id, info) else abandon(activity)
}
private fun configure(activity: Activity, id: Int, info: AppWidgetProviderInfo) {
val optional = Build.VERSION.SDK_INT >= 31 &&
info.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0
if (info.configure == null || optional) {
place(activity, id, info)
return
}
runCatching { host(activity).startAppWidgetConfigureActivityForResult(activity, id, 0, REQUEST_CONFIGURE, null) }
.onFailure { place(activity, id, info) }
}
fun canReconfigure(context: Context, id: Int): Boolean {
val info = info(context, id) ?: return false
if (info.configure == null) return false
return Build.VERSION.SDK_INT < 28 || info.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0
}
fun reconfigure(activity: Activity, id: Int) {
runCatching { host(activity).startAppWidgetConfigureActivityForResult(activity, id, 0, REQUEST_RECONFIGURE, null) }
}
fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int): Boolean {
when (requestCode) {
REQUEST_BIND -> {
val info = pending
if (resultCode == Activity.RESULT_OK && info != null) bound(activity, pendingId, info) else abandon(activity)
}
REQUEST_CONFIGURE -> {
val info = pending
if (resultCode == Activity.RESULT_OK && info != null) place(activity, pendingId, info) else abandon(activity)
}
REQUEST_RECONFIGURE -> Unit
else -> return false
}
return true
}
private fun abandon(context: Context) {
if (pendingId != AppWidgetManager.INVALID_APPWIDGET_ID) host(context).deleteAppWidgetId(pendingId)
pending = null
pinRequest = null
pendingId = AppWidgetManager.INVALID_APPWIDGET_ID
}
private fun place(context: Context, id: Int, info: AppWidgetProviderInfo) {
val (w, h) = defaultSize(context, info)
pending = null
pinRequest = null
pendingId = AppWidgetManager.INVALID_APPWIDGET_ID
widgets = widgets + Placed(id, w, h)
save(context)
}
fun resize(context: Context, id: Int, widthDp: Int, heightDp: Int) {
widgets = widgets.map { if (it.id == id) it.copy(widthDp = widthDp, heightDp = heightDp) else it }
save(context)
}
fun move(context: Context, id: Int, by: Int) {
val i = widgets.indexOfFirst { it.id == id }
val j = i + by
if (i < 0 || j !in widgets.indices) return
widgets = widgets.toMutableList().apply { add(j, removeAt(i)) }
save(context)
}
fun remove(context: Context, id: Int) {
host(context).deleteAppWidgetId(id)
widgets = widgets.filterNot { it.id == id }
save(context)
}
private fun save(context: Context) {
val array = JSONArray()
for (w in widgets) array.put(JSONObject().put("id", w.id).put("w", w.widthDp).put("h", w.heightDp))
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY, array.toString()).apply()
}
}
@@ -1,346 +0,0 @@
package com.droiddeck.launcher
import android.app.ActivityOptions
import android.app.AppOpsManager
import android.app.PictureInPictureParams
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.os.Process
import android.provider.Settings
import android.util.Rational
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.droiddeck.launcher.core.AppLanguage
import com.droiddeck.launcher.core.GlobalSettings
import com.droiddeck.launcher.core.PhantomProcessLimit
import com.droiddeck.launcher.core.WirelessAdbPairingService
import com.droiddeck.launcher.core.WirelessAdbPairingService.Stage
import com.droiddeck.launcher.ui.DroidDeckTheme
import com.droiddeck.launcher.ui.LocalPalette
import com.droiddeck.launcher.ui.PrimaryButton
import com.droiddeck.launcher.ui.SecondaryButton
import kotlinx.coroutines.delay
class PairingGuideActivity : ComponentActivity() {
enum class Step { BUILD_NUMBER, CHILD_PROCESS, WIRELESS_DEBUGGING, PAIR, CODE, WORKING, DONE, FAILED }
private var settingsDisplay: Int? = null
private var toggleOnly = false
private var inPip by mutableStateOf(false)
private var launchPending = false
override fun attachBaseContext(newBase: Context) = super.attachBaseContext(AppLanguage.wrap(newBase))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
settingsDisplay = intent.getIntExtra(EXTRA_DISPLAY, -1).takeIf { it >= 0 }
toggleOnly = intent.getBooleanExtra(EXTRA_TOGGLE_ONLY, false)
setContent { DroidDeckTheme { Guide() } }
launchPending = savedInstanceState == null
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
settingsDisplay = intent.getIntExtra(EXTRA_DISPLAY, -1).takeIf { it >= 0 }
toggleOnly = intent.getBooleanExtra(EXTRA_TOGGLE_ONLY, false)
launchPending = true
}
override fun onResume() {
super.onResume()
if (!launchPending) return
if (inPip || settingsDisplay != null) {
launchSettings()
return
}
enterPip()
if (!inPip) launchSettings()
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
inPip = isInPictureInPictureMode
if (isInPictureInPictureMode && launchPending) launchSettings()
}
private fun launchSettings() {
launchPending = false
openSettings(stepFor(SettingsState.read(this), WirelessAdbPairingService.stage.value, toggleOnly))
}
private fun enterPip() {
inPip = runCatching {
enterPictureInPictureMode(PictureInPictureParams.Builder().setAspectRatio(Rational(4, 3)).build())
}.getOrDefault(false)
}
private fun openSettings(step: Step) {
val intent = if (step == Step.BUILD_NUMBER) {
Intent(Settings.ACTION_DEVICE_INFO_SETTINGS).putExtra(HIGHLIGHT_KEY, "build_number")
} else {
Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).apply {
if (step != Step.CHILD_PROCESS) putExtra(HIGHLIGHT_KEY, "toggle_adb_wireless")
}
}.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val display = settingsDisplay
runCatching {
if (display == null) startActivity(intent)
else startActivity(intent, ActivityOptions.makeBasic().setLaunchDisplayId(display).toBundle())
}.onFailure {
runCatching { startActivity(Intent(Settings.ACTION_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) }
}
}
private fun returnToApp() {
startActivity(Intent(this, FrontEndActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT))
finish()
}
@Composable
private fun Guide() {
val stage by WirelessAdbPairingService.stage.collectAsState()
val settingsState by produceState(SettingsState.read(this)) {
while (true) {
value = SettingsState.read(this@PairingGuideActivity)
delay(500)
}
}
val step = stepFor(settingsState, stage, toggleOnly)
var previous by remember { mutableStateOf(step) }
LaunchedEffect(step) {
if (previous == Step.BUILD_NUMBER && (step == Step.WIRELESS_DEBUGGING || step == Step.CHILD_PROCESS)) openSettings(step)
previous = step
when (step) {
Step.DONE -> { delay(1_500); returnToApp() }
Step.FAILED -> { delay(3_000); returnToApp() }
else -> Unit
}
}
LaunchedEffect(stage) { if (stage == Stage.Idle && !toggleOnly) finish() }
BackHandler(enabled = !inPip && settingsDisplay == null) { enterPip() }
Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center) {
if (inPip) CompactStep(step, stage)
else FullGuide(step, stage)
}
}
@Composable
private fun CompactStep(step: Step, stage: Stage) {
Column(Modifier.fillMaxSize().padding(10.dp), verticalArrangement = Arrangement.SpaceBetween) {
Text(
instruction(step, stage), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onBackground, maxLines = 3, overflow = TextOverflow.Ellipsis,
)
StepVisual(step, compact = true)
StepDots(step)
}
}
@Composable
private fun FullGuide(step: Step, stage: Stage) {
Column(
Modifier.widthIn(max = 520.dp).fillMaxWidth().padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(stringResource(R.string.gate_title), style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground)
Text(instruction(step, stage), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground)
StepVisual(step, compact = false)
StepDots(step)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
if (step == Step.DONE || step == Step.FAILED) {
PrimaryButton(stringResource(R.string.common_back), onClick = ::returnToApp)
} else {
PrimaryButton(stringResource(R.string.gate_open_dev), onClick = {
if (settingsDisplay == null) enterPip()
openSettings(step)
})
SecondaryButton(stringResource(R.string.common_cancel), onClick = {
if (!toggleOnly) WirelessAdbPairingService.cancel(this@PairingGuideActivity)
returnToApp()
})
}
}
}
}
@Composable
private fun instruction(step: Step, stage: Stage): String = when (step) {
Step.BUILD_NUMBER -> stringResource(R.string.pairguide_build_number)
Step.CHILD_PROCESS -> stringResource(R.string.pairguide_child_process)
Step.WIRELESS_DEBUGGING -> stringResource(R.string.pairguide_wireless)
Step.PAIR -> stringResource(R.string.pairguide_pair)
Step.CODE -> (stage as? Stage.CodeNeeded)?.error ?: stringResource(R.string.gate_step3)
Step.WORKING -> (stage as? Stage.Working)?.step ?: stringResource(R.string.adbpair_title_working)
Step.DONE -> stringResource(R.string.adbpair_title_done)
Step.FAILED -> (stage as? Stage.Failed)?.error ?: stringResource(R.string.adbpair_title_failed)
}
@Composable
private fun StepVisual(step: Step, compact: Boolean) {
val colors = MaterialTheme.colorScheme
val accent = LocalPalette.current.signal
val pulse = rememberInfiniteTransition(label = "guidePulse")
val beat by pulse.animateFloat(0f, 1f, infiniteRepeatable(tween(900, easing = LinearEasing), RepeatMode.Restart), label = "beat")
val label = when (step) {
Step.BUILD_NUMBER -> stringResource(R.string.pairguide_row_build_number)
Step.CHILD_PROCESS -> stringResource(R.string.pairguide_row_child_process)
Step.WIRELESS_DEBUGGING -> stringResource(R.string.pairguide_row_wireless)
Step.PAIR -> stringResource(R.string.pairguide_row_pair)
Step.CODE -> stringResource(R.string.adbpair_enter_code)
Step.WORKING, Step.DONE, Step.FAILED -> return
}
val rowHeight = if (compact) 34.dp else 56.dp
Row(
Modifier.fillMaxWidth().height(rowHeight).background(colors.surfaceVariant, RoundedCornerShape(10.dp))
.border(1.dp, accent.copy(alpha = 0.35f), RoundedCornerShape(10.dp)).padding(horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(label, style = if (compact) MaterialTheme.typography.labelMedium else MaterialTheme.typography.bodyLarge,
color = colors.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
when (step) {
Step.WIRELESS_DEBUGGING, Step.CHILD_PROCESS -> Toggle(on = beat > 0.5f, accent = accent, compact = compact)
else -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
if (step == Step.BUILD_NUMBER) Text("×7", style = MaterialTheme.typography.labelLarge, color = accent)
Box(contentAlignment = Alignment.Center) {
Box(Modifier.size(if (compact) 18.dp else 26.dp).scale(0.6f + beat * 0.8f).alpha(1f - beat)
.background(accent, CircleShape))
Box(Modifier.size(if (compact) 8.dp else 12.dp).background(accent, CircleShape))
}
}
}
}
}
@Composable
private fun Toggle(on: Boolean, accent: Color, compact: Boolean) {
val width = if (compact) 26.dp else 40.dp
val knob = if (compact) 12.dp else 18.dp
Box(
Modifier.width(width).height(knob + 4.dp)
.background(if (on) accent.copy(alpha = 0.6f) else MaterialTheme.colorScheme.outline, CircleShape).padding(2.dp),
contentAlignment = if (on) Alignment.CenterEnd else Alignment.CenterStart,
) { Box(Modifier.size(knob).background(Color.White, CircleShape)) }
}
@Composable
private fun StepDots(step: Step) {
val accent = LocalPalette.current.signal
val shown = if (toggleOnly) listOf(Step.BUILD_NUMBER, Step.CHILD_PROCESS)
else listOf(Step.BUILD_NUMBER, Step.WIRELESS_DEBUGGING, Step.PAIR, Step.CODE)
val at = shown.indexOf(step).takeIf { it >= 0 } ?: shown.size
Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically) {
shown.forEachIndexed { index, _ ->
Box(Modifier.size(if (index == at) 8.dp else 6.dp)
.background(if (index <= at) accent else MaterialTheme.colorScheme.outline, CircleShape))
}
Spacer(Modifier.width(2.dp))
}
}
private data class SettingsState(
val developerOptions: Boolean,
val wirelessDebugging: Boolean,
val childProcessBlocked: Boolean,
) {
companion object {
fun read(context: Context) = SettingsState(
GlobalSettings.developerOptionsOn(context),
GlobalSettings.wirelessDebuggingOn(context),
PhantomProcessLimit.blocksSteam(PhantomProcessLimit.read(context)),
)
}
}
companion object {
private const val EXTRA_DISPLAY = "settingsDisplay"
private const val EXTRA_TOGGLE_ONLY = "toggleOnly"
private const val HIGHLIGHT_KEY = ":settings:fragment_args_key"
private fun stepFor(settings: SettingsState, stage: Stage, toggleOnly: Boolean): Step = if (toggleOnly) when {
!settings.developerOptions -> Step.BUILD_NUMBER
settings.childProcessBlocked -> Step.CHILD_PROCESS
else -> Step.DONE
} else when (stage) {
is Stage.Working -> Step.WORKING
Stage.Done -> Step.DONE
is Stage.Failed -> Step.FAILED
is Stage.CodeNeeded -> Step.CODE
else -> when {
!settings.developerOptions -> Step.BUILD_NUMBER
!settings.wirelessDebugging -> Step.WIRELESS_DEBUGGING
else -> Step.PAIR
}
}
fun canFloat(context: Context): Boolean {
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) return false
val ops = context.getSystemService(AppOpsManager::class.java) ?: return true
val mode = if (Build.VERSION.SDK_INT >= 29) {
ops.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), context.packageName)
} else {
@Suppress("DEPRECATION")
ops.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), context.packageName)
}
return mode == AppOpsManager.MODE_ALLOWED
}
fun start(context: Context, settingsDisplay: Int?, toggleOnly: Boolean = false): Boolean {
if (settingsDisplay == null && !canFloat(context)) return false
context.startActivity(Intent(context, PairingGuideActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_TOGGLE_ONLY, toggleOnly)
.apply { if (settingsDisplay != null) putExtra(EXTRA_DISPLAY, settingsDisplay) })
return true
}
}
}
@@ -1,82 +0,0 @@
package com.droiddeck.launcher
import android.content.Intent
import android.content.pm.LauncherApps
import android.graphics.Bitmap
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.graphics.drawable.toBitmap
import com.droiddeck.launcher.core.AppUiPrefs
import com.droiddeck.launcher.ui.DroidDeckTheme
import com.droiddeck.launcher.ui.PinRequestPrompt
class PinRequestActivity : ComponentActivity() {
override fun attachBaseContext(newBase: android.content.Context) {
super.attachBaseContext(newBase)
com.droiddeck.launcher.core.AppLanguage.applyTo(this, newBase)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val request = runCatching { getSystemService(LauncherApps::class.java).getPinItemRequest(intent) }.getOrNull()
if (request == null || !request.isValid) {
finish()
return
}
val prompt = when (request.requestType) {
LauncherApps.PinItemRequest.REQUEST_TYPE_SHORTCUT -> request.shortcutInfo?.let { info ->
val appLabel = appLabel(info.`package`)
Triple(info.shortLabel?.toString().orEmpty().ifEmpty { appLabel }, appLabel, HomeApp.shortcutIcon(this, info))
}
LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET -> request.getAppWidgetProviderInfo(this)?.let { info ->
val appLabel = appLabel(info.provider.packageName)
val dpi = resources.displayMetrics.densityDpi
val icon = runCatching { (info.loadPreviewImage(this, dpi) ?: info.loadIcon(this, dpi))?.let(::scaled) }.getOrNull()
Triple(info.loadLabel(packageManager)?.toString().orEmpty().ifEmpty { appLabel }, appLabel, icon)
}
else -> null
}
if (prompt == null) {
finish()
return
}
val (label, appLabel, icon) = prompt
val widget = request.requestType == LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET
setContent {
DroidDeckTheme(appScale = AppUiPrefs.scale(this)) {
PinRequestPrompt(
label = label, appLabel = appLabel, icon = icon, wide = widget,
onAdd = {
if (!widget) {
HomePins.accept(this, request)
finish()
} else if (!HomeWidgets.addPinned(this, request)) finish()
},
onCancel = { finish() },
)
}
}
}
@Suppress("OVERRIDE_DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (HomeWidgets.onActivityResult(this, requestCode, resultCode)) {
finish()
return
}
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
}
private fun appLabel(packageName: String): String =
runCatching { packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, 0)).toString() }
.getOrDefault(packageName)
private fun scaled(d: android.graphics.drawable.Drawable): Bitmap {
val w = d.intrinsicWidth.takeIf { it > 0 } ?: 192
val h = d.intrinsicHeight.takeIf { it > 0 } ?: 192
val k = minOf(1f, 480f / maxOf(w, h))
return d.toBitmap((w * k).toInt().coerceAtLeast(1), (h * k).toInt().coerceAtLeast(1))
}
}
@@ -1,8 +1,6 @@
package com.droiddeck.launcher
import android.os.Handler
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -10,60 +8,31 @@ import androidx.compose.runtime.setValue
import com.droiddeck.launcher.session.ProtonExtras
import com.droiddeck.launcher.ui.ProtonRow
import com.droiddeck.launcher.session.SessionState
import com.droiddeck.launcher.session.SessionPhase
import com.droiddeck.launcher.frontend.AutoComponents
import com.droiddeck.launcher.runtime.LinuxRuntime
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 },
) {
private object Operation {
val rows = mutableStateOf<List<ProtonRow>>(emptyList())
val busyId = mutableStateOf<String?>(null)
val stage = mutableStateOf<String?>(null)
val percent = mutableIntStateOf(-1)
val problem = mutableStateOf<String?>(null)
}
var protonRows by Operation.rows
var protonBusyId by Operation.busyId
var protonStage by Operation.stage
var protonPercent by Operation.percent
var protonProblem by Operation.problem
internal class ProtonMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) {
var protonRows by mutableStateOf<List<ProtonRow>>(emptyList())
var protonBusyId by mutableStateOf<String?>(null)
var protonStage by mutableStateOf<String?>(null)
var protonPercent by mutableIntStateOf(-1)
private val checking = java.util.concurrent.atomic.AtomicBoolean(false)
@VisibleForTesting internal var installer: (ProtonExtras.Tool, (String, Int) -> Unit) -> String? =
{ tool, progress -> ProtonExtras.install(activity, tool, progress) }
fun installProton(id: String, automatic: Boolean = false) {
fun installProton(id: String) {
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
if (protonBusyId != null || ProtonExtras.installInProgress || SessionState.running) return
if (protonBusyId != null || SessionState.running) return
ProtonExtras.unqueue(activity, tool)
protonBusyId = id
protonProblem = null
protonStage = activity.getString(R.string.store_starting)
protonPercent = -1
Thread({
val problem = runCatching {
installer(tool) { label, value ->
ui.post { protonStage = label; protonPercent = value }
}
}.getOrElse { it.message ?: it.javaClass.simpleName }
if (problem == null && !automatic && tool.id == ProtonExtras.OWN) ProtonExtras.setDeclined(activity, false)
val problem = ProtonExtras.install(activity, tool) { label, value ->
ui.post { protonStage = label; protonPercent = value }
}
ui.post {
protonProblem = problem
protonBusyId = null
protonStage = null
protonPercent = -1
refreshProtons()
if (problem != null) {
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()
if (problem != null) android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show()
}
}, "install-proton-$id").start()
}
@@ -72,12 +41,10 @@ internal class ProtonMenu(
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
if (protonBusyId != null || SessionState.running) return
protonBusyId = id
protonProblem = null
protonStage = activity.getString(R.string.main_removing_named, dir ?: tool.name)
protonPercent = -1
Thread({
if (older) ProtonExtras.removeOlder(activity, tool) else ProtonExtras.remove(activity, tool, dir)
if (tool.id == ProtonExtras.OWN && ProtonExtras.installedAll(activity, tool).isEmpty()) ProtonExtras.setDeclined(activity, true)
ui.post {
protonBusyId = null
protonStage = null
@@ -97,39 +64,7 @@ internal class ProtonMenu(
if (!checking.compareAndSet(false, true)) return
Thread({
val asked = try { ProtonExtras.checkLatest(activity, force) } catch (_: Throwable) { false } finally { checking.set(false) }
ui.post {
if (asked) refreshProtons()
ensureAutoUpdates()
}
if (asked) ui.post { refreshProtons() }
}, "proton-updates").start()
}
internal fun ensureAutoUpdates(now: Long = System.currentTimeMillis()) {
if (activity.isFinishing || activity.isDestroyed || !idle() || protonBusyId != null ||
ProtonExtras.installInProgress || SessionState.running || LinuxRuntimeInstaller.isBusy() ||
SessionState.phase !in setOf(SessionPhase.IDLE, SessionPhase.FAILED) ||
!LinuxRuntime.isInstalled(activity) || !AutoComponents.online(activity) || AutoComponents.unfavourable(activity)) return
if (ProtonExtras.installedAll(activity, ProtonExtras.tools.first { it.id == ProtonExtras.OWN }).size > 1) {
protonBusyId = ProtonExtras.OWN
protonProblem = null
Thread({
ProtonExtras.tidy(activity)
val tidied = ProtonExtras.installedAll(activity, ProtonExtras.tools.first { it.id == ProtonExtras.OWN }).size <= 1
ui.post {
protonBusyId = null
refreshProtons()
if (tidied) ensureAutoUpdates()
}
}, "proton-tidy").start()
return
}
val attempts = activity.getSharedPreferences("proton-auto-updates", android.content.Context.MODE_PRIVATE)
val (tool, update) = ProtonExtras.tools.firstNotNullOfOrNull { tool ->
val update = ProtonExtras.update(activity, tool) ?: ProtonExtras.missing(activity, tool) ?: return@firstNotNullOfOrNull null
val previous = attempts.getLong("${tool.id}.at", 0L)
if (attempts.getString("${tool.id}.tag", null) != update || now - previous !in 0 until 15L * 60 * 1000) tool to update else null
} ?: return
attempts.edit().putString("${tool.id}.tag", update).putLong("${tool.id}.at", now).apply()
installProton(tool.id, automatic = true)
}
}
@@ -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
@@ -29,7 +29,6 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
private var pending: AppUpdates.Release? = null
private var lastCheck = 0L
private var request = 0
@Volatile private var session = -1
fun state() = UpdatesState(catalog, follow, checking, error, stage, percent, askPermission)
@@ -88,14 +87,13 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
if (SessionState.running) { error = activity.getString(R.string.upd_stop_first); return }
if (!SelfInstaller.canInstall(activity)) { pending = r; askPermission = true; return }
error = null
session = -1
stage = activity.getString(R.string.upd_stage_downloading)
percent = 0
Thread({
val result = runCatching {
val file = SelfInstaller.download(activity, apk) { pct -> ui.post { percent = pct } }
ui.post { stage = activity.getString(R.string.upd_stage_installing); percent = -1 }
SelfInstaller.install(activity, file) { id -> session = id }
SelfInstaller.install(activity, file)
}
result.onFailure { e -> ui.post { stage = null; error = e.message ?: activity.getString(R.string.upd_failed) } }
}, "app-update-install").start()
@@ -103,15 +101,13 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1).let { it < 0 || it != session }) return
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirm = if (Build.VERSION.SDK_INT >= 33) intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
else @Suppress("DEPRECATION") intent.getParcelableExtra(Intent.EXTRA_INTENT)
val safe = confirm?.let { SelfInstaller.safeConfirmIntent(activity, it) }
if (safe == null) { stage = null; error = activity.getString(R.string.upd_not_offered); return }
if (confirm == null) { stage = null; error = activity.getString(R.string.upd_not_offered); return }
stage = activity.getString(R.string.upd_stage_confirm)
if (runCatching { activity.startActivity(safe) }.isFailure) { stage = null; error = activity.getString(R.string.upd_not_offered) }
activity.startActivity(confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
PackageInstaller.STATUS_SUCCESS -> stage = null
else -> {
@@ -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() {
@@ -3,13 +3,9 @@ package com.droiddeck.launcher.agent
import android.content.Context
import android.util.Log
import com.droiddeck.launcher.runtime.LinuxRuntime
import com.droiddeck.launcher.runtime.ProotFastPath
import com.droiddeck.launcher.session.SessionPhase
import com.droiddeck.launcher.session.SessionState
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.security.MessageDigest
import java.nio.file.Files
import java.nio.file.LinkOption.NOFOLLOW_LINKS
import java.nio.file.StandardOpenOption.READ
@@ -18,6 +14,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()
@@ -58,9 +55,6 @@ object AgentEnv {
fun override(context: Context, name: String): JSONObject = synchronized(lock) {
AgentAccess.requireCommands(context)
if (NAME.matchEntire(name) == null || name in setOf(".", "..")) throw AgentException("INVALID_NAME", "Override names are letters, digits, '.', '_' and '-'")
if (name == "libblfastpath.so" && (SessionState.running || SessionState.guestPid > 0 || SessionState.phase == SessionPhase.STOPPING)) {
throw AgentException("SESSION_ACTIVE", "Stop the session before installing a fast-path library")
}
val source = File(inbox(context), name)
if (Files.isSymbolicLink(source.toPath())) throw AgentException("INVALID_NAME", "The override must be a regular file")
if (!source.isFile) throw AgentException("NO_SUCH_FILE", "Push the binary to ${source.path} first")
@@ -73,10 +67,6 @@ object AgentEnv {
staged.delete()
throw AgentException("OVERRIDE_FAILED", "Could not stage the override")
}
if (name == "libblfastpath.so" && !arm64SharedLibrary(staged)) {
staged.delete()
throw AgentException("INVALID_LIBRARY", "The fast path must be a 64-bit little-endian ARM64 ELF shared library")
}
if (!staged.setExecutable(true, false) || !staged.renameTo(target)) {
staged.delete()
throw AgentException("OVERRIDE_FAILED", "Could not install $name in the rootfs")
@@ -86,40 +76,6 @@ object AgentEnv {
state.put("pendingOverrides", JSONArray(merge(strings(state.optJSONArray("pendingOverrides")), listOf(name))))
save(context, state)
state.put("installed", "$GUEST_BIN/$name")
if (name == "libblfastpath.so") {
val digest = MessageDigest.getInstance("SHA-256")
target.inputStream().use { input ->
val buffer = ByteArray(65536)
while (true) {
val count = input.read(buffer)
if (count < 0) break
digest.update(buffer, 0, count)
}
}
state.put("sha256", digest.digest().joinToString("") { "%02x".format(it) })
}
state
}
fun fastPathLibrary(context: Context): String = synchronized(lock) {
if (!AgentAccess.commandsAllowed(context)) return@synchronized ProotFastPath.LIBRARY
if ("libblfastpath.so" !in strings(load(context).optJSONArray("activeOverrides"))) return@synchronized ProotFastPath.LIBRARY
val file = File(bin(context), "libblfastpath.so")
if (!file.isFile || Files.isSymbolicLink(file.toPath())) return@synchronized ProotFastPath.LIBRARY
"$GUEST_BIN/libblfastpath.so"
}
private fun arm64SharedLibrary(file: File): Boolean = file.inputStream().use { input ->
val header = ByteArray(20)
var count = 0
while (count < header.size) {
val read = input.read(header, count, header.size - count)
if (read < 0) return@use false
count += read
}
header.take(7) == listOf<Byte>(0x7f, 0x45, 0x4c, 0x46, 2, 1, 1) &&
header[16] == 3.toByte() && header[17] == 0.toByte() &&
header[18] == 183.toByte() && header[19] == 0.toByte()
}
fun beginSession(context: Context): List<String> = synchronized(lock) {
@@ -130,13 +86,13 @@ object AgentEnv {
val state = load(context)
val active = strings(state.optJSONArray("pending"))
val overrides = strings(state.optJSONArray("pendingOverrides"))
bin(context).listFiles()
?.filter { it.name !in overrides }?.forEach { it.delete() }
val lines = strings(state.optJSONArray("persistent")) + active
val withPath = if (overrides.none { it != "libblfastpath.so" }) lines else lines + "PATH=$GUEST_BIN:${pathOf(lines)}"
state.put("active", JSONArray(active)).put("pending", JSONArray())
state.put("activeOverrides", JSONArray(overrides)).put("pendingOverrides", JSONArray())
save(context, state)
bin(context).listFiles()
?.filter { it.name !in overrides }?.forEach { it.delete() }
val lines = strings(state.optJSONArray("persistent")) + active
val withPath = if (overrides.isEmpty()) lines else lines + "PATH=$GUEST_BIN:${pathOf(lines)}"
if (withPath.isNotEmpty()) Log.i(TAG, "agent environment keys for this session: ${withPath.map { it.substringBefore('=') }}")
withPath
}
@@ -159,7 +115,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()
@@ -22,18 +22,6 @@ object AgentGuest {
private set
var steamKeyboard by mutableStateOf(false)
private set
var desktopTextField by mutableStateOf(false)
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
@Volatile private var routeDir: File? = null
private const val TAG = "AgentGuest"
private const val POLL_MS = 50L
private val lock = Any()
@@ -47,12 +35,10 @@ object AgentGuest {
observer?.stopWatching()
observer = null
lastFocusKey = null
main.post { steamKeyboard = false; desktopTextField = false; gameTextField = false; gameProgram = null }
main.post { steamKeyboard = false }
val dir = dir(context)
dir.deleteRecursively()
dir.mkdirs()
routeDir = dir
writeKeyboardRoute(dir, keyboardRoute)
@Suppress("DEPRECATION")
observer = object : FileObserver(dir.path, MOVED_TO or CLOSE_WRITE) {
override fun onEvent(event: Int, path: String?) {
@@ -62,10 +48,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) }
}
}
}.also { it.startWatching() }
@@ -76,46 +58,10 @@ object AgentGuest {
synchronized(lock) {
observer?.stopWatching()
observer = null
main.post { steamKeyboard = false; desktopTextField = false; gameTextField = false; gameProgram = null }
main.post { steamKeyboard = false }
}
}
fun routeKeyboard(context: Context, bottom: Boolean) {
synchronized(lock) {
if (keyboardRoute == bottom) return
keyboardRoute = bottom
writeKeyboardRoute(routeDir ?: dir(context), bottom)
}
}
private fun writeKeyboardRoute(dir: File, bottom: Boolean) {
val file = File(dir, "keyboard-route")
runCatching {
if (!bottom) file.delete()
else if (dir.isDirectory) {
val temp = File(dir, "keyboard-route.tmp")
temp.writeText("bottom")
if (!temp.renameTo(file)) temp.delete()
}
}.onFailure { Log.w(TAG, "keyboard route not written", it) }
}
private fun recordKeyboardRequest(source: FileObserver) {
synchronized(lock) {
if (source !== observer) return
main.post { keyboardRequests++ }
}
SessionEvents.record("keyboard.requested", emptyMap())
}
private fun recordKeyboardDismiss(source: FileObserver) {
synchronized(lock) {
if (source !== observer) return
main.post { keyboardDismissals++ }
}
SessionEvents.record("keyboard.dismissed", emptyMap())
}
private fun recordKeyboard(source: FileObserver, snapshot: JSONObject) {
val showing = snapshot.optBoolean("steamKeyboard")
synchronized(lock) {
@@ -125,25 +71,6 @@ object AgentGuest {
SessionEvents.record("keyboard.changed", mapOf("steamKeyboard" to showing, "seq" to snapshot.optInt("seq")))
}
private fun recordTextField(source: FileObserver, snapshot: JSONObject) {
val focused = snapshot.optBoolean("textField")
synchronized(lock) {
if (source !== observer) return
main.post { desktopTextField = focused }
}
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")
}
@@ -23,9 +23,6 @@ class AgentStartActivity : Activity() {
Thread({
try {
AgentBridgeProvider.awaitRecovery()
val until = android.os.SystemClock.elapsedRealtime() + RUNTIME_WAIT_MS
while (com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isBusy() && !isFinishing &&
android.os.SystemClock.elapsedRealtime() < until) Thread.sleep(RUNTIME_POLL_MS)
runOnUiThread {
if (!isFinishing) startSession(request)
}
@@ -54,11 +51,6 @@ class AgentStartActivity : Activity() {
}
private fun startSession(request: JSONObject) {
if (com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isBusy()) {
Log.w(TAG, "the Linux runtime is being installed or changed")
finish()
return
}
try {
AgentAccess.requireStart(this, request)
} catch (e: AgentException) {
@@ -115,7 +107,5 @@ class AgentStartActivity : Activity() {
companion object {
const val EXTRA_REQUEST = "request"
private const val TAG = "AgentStartActivity"
private const val RUNTIME_WAIT_MS = 15 * 60_000L
private const val RUNTIME_POLL_MS = 500L
}
}
@@ -3,7 +3,6 @@ package com.droiddeck.launcher.audio;
import android.os.Process;
import android.util.Log;
import com.droiddeck.launcher.core.FileUtils;
import com.droiddeck.launcher.core.SessionPart;
import com.droiddeck.launcher.core.HostProcess;
@@ -20,13 +19,10 @@ public class DirectAudioRelayComponent extends SessionPart {
private volatile int pid = -1;
private final Object micLock = new Object();
private boolean micHot;
private final File micControl;
public java.util.function.Consumer<Boolean> onMicEnabled;
public DirectAudioRelayComponent(File socketPath, File micFifoPath) {
this.socketPath = socketPath;
this.micFifoPath = micFifoPath;
this.micControl = micFifoPath == null ? null : new File(micFifoPath.getParentFile(), "mic-enabled");
}
public void setLogFile(File file) {
@@ -67,13 +63,8 @@ public class DirectAudioRelayComponent extends SessionPart {
StringBuilder command = new StringBuilder(binary.getAbsolutePath());
command.append(" --socket ").append(socketPath.getAbsolutePath());
command.append(" --log");
final boolean withMic = micFifoPath != null && setMicEnabled(true);
if (withMic) {
if (micFifoPath != null) {
command.append(" --mic-fifo ").append(micFifoPath.getAbsolutePath());
command.append(" --mic-control ").append(micControl.getAbsolutePath());
} else if (micFifoPath != null) {
Log.w(TAG, "microphone control unavailable; starting with capture disabled");
command.append(" --mic-control /dev/null");
}
ArrayList<String> env = new ArrayList<>();
env.add("HOME=" + app().getFilesDir());
@@ -82,7 +73,7 @@ public class DirectAudioRelayComponent extends SessionPart {
try {
out = new java.io.PrintWriter(new java.io.FileWriter(logFile, true));
out.println("== DirectAudio relay helper starting"
+ (withMic ? " with the microphone" : " (no microphone)"));
+ (micFifoPath != null ? " with the microphone" : " (no microphone)"));
out.flush();
} catch (Exception e) {
Log.w(TAG, "could not open " + logFile, e);
@@ -93,23 +84,14 @@ public class DirectAudioRelayComponent extends SessionPart {
app().getFilesDir(), null,
line -> {
Log.i(TAG, line);
if (withMic) trackMic(line);
trackMic(line);
if (log != null) synchronized (log) { log.println(line); log.flush(); }
});
Log.i(TAG, "started pid=" + pid + " socket=" + socketPath
+ (withMic ? " mic=" + micFifoPath : " (no microphone)"));
}
public synchronized boolean setMicEnabled(boolean enabled) {
if (micControl == null) return false;
File next = new File(micControl.getPath() + ".new");
return FileUtils.writeString(next, enabled ? "1" : "0") && next.renameTo(micControl);
+ (micFifoPath != null ? " mic=" + micFifoPath : " (no microphone)"));
}
private void trackMic(String line) {
if (line.startsWith("DA-Relay: mic enabled=") && onMicEnabled != null) {
onMicEnabled.accept(line.equals("DA-Relay: mic enabled=1"));
}
boolean start = line.startsWith("DA-Relay: mic start ");
if (!start && !line.startsWith("DA-Relay: mic stop ")) return;
synchronized (micLock) {
@@ -118,6 +100,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) {
@@ -1,42 +0,0 @@
package com.droiddeck.launcher.audio
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioTrack
import android.util.Log
object DirectAudioTiming {
private const val TAG = "DirectAudioTiming"
const val PERIOD_ENV = "AUDIO_DIRECT_PERIOD_MS"
const val BUFFER_ENV = "AUDIO_DIRECT_MS"
const val SLOW_OUTPUT_PERIOD_MS = 20
const val SLOW_OUTPUT_BUFFER_MS = 60
fun environment(lowLatencyOutput: Boolean): List<String> =
if (lowLatencyOutput) emptyList()
else listOf("$PERIOD_ENV=$SLOW_OUTPUT_PERIOD_MS", "$BUFFER_ENV=$SLOW_OUTPUT_BUFFER_MS")
fun lowLatencyOutput(): Boolean = try {
val track = AudioTrack.Builder()
.setAudioAttributes(AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
.setSampleRate(48000)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.build())
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
.setTransferMode(AudioTrack.MODE_STREAM)
.build()
try {
track.performanceMode == AudioTrack.PERFORMANCE_MODE_LOW_LATENCY
} finally {
track.release()
}
} catch (e: Exception) {
Log.w(TAG, "audio: could not check for low-latency output; DirectAudio keeps its defaults", e)
true
}
}
@@ -26,7 +26,6 @@ public class PulseAudioComponent extends SessionPart {
private final File workingDir;
private final File deviceControls;
private File logFile;
private final String micFifoPath;
private volatile int pid = -1;
@@ -37,8 +36,6 @@ public class PulseAudioComponent extends SessionPart {
public PulseAudioComponent(Context context, String micFifoPath) {
this.workingDir = new File(context.getFilesDir(), "pulseaudio");
this.deviceControls = com.droiddeck.launcher.runtime.DeviceControlsComponent.directory(
com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context));
this.micFifoPath = micFifoPath;
}
@@ -97,7 +94,7 @@ public class PulseAudioComponent extends SessionPart {
config.add("load-module module-native-protocol-unix auth-anonymous=1 auth-cookie-enabled=0 socket=\""
+ socket().getAbsolutePath() + "\"");
config.add(".nofail");
config.add("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);
config.add("load-module module-directaudio-native-sink sink_name=DirectAudio performance_mode=1 adaptive=1 decay=0 limiter=1 watchdog=1 volume=1.0");
config.add("set-default-sink DirectAudio");
config.add(".fail");
if (micFifoPath != null && !micFifoPath.isEmpty()) {
@@ -130,46 +127,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") != 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 +152,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"
@@ -147,10 +146,7 @@ internal class AddonPack(
listOf("version", "xodus-cli", "xodus-service"))
val COMET = AddonPack("comet", BuildConfig.PACK_COMET_SHA256, BuildConfig.PACK_COMET_SIZE,
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"))
val AMAZON = AddonPack("amazon", BuildConfig.PACK_AMAZON_SHA256, BuildConfig.PACK_AMAZON_SIZE,
listOf("engine.json", "libdroiddeckamazon.so"), reuseDeviceCopy = false)
}
}
@@ -1,32 +1,11 @@
package com.droiddeck.launcher.core
import android.content.Context
import android.content.pm.ActivityInfo
import com.droiddeck.launcher.R
object AppUiPrefs {
const val DEFAULT_SCALE = 100
val scales = listOf(75, 85, 100, 115, 125, 150)
val orientationOptions = listOf(
ActivityInfo.SCREEN_ORIENTATION_USER to R.string.setup_launcher_orientation_auto,
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT to R.string.setup_orientation_portrait,
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE to R.string.setup_orientation_auto,
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE to R.string.setup_orientation_landscape,
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE to R.string.setup_orientation_reverse,
)
fun orientation(context: Context): Int =
context.getSharedPreferences("app-ui", Context.MODE_PRIVATE)
.getInt("orientation", ActivityInfo.SCREEN_ORIENTATION_USER)
.takeIf { value -> orientationOptions.any { it.first == value } }
?: ActivityInfo.SCREEN_ORIENTATION_USER
fun setOrientation(context: Context, value: Int) {
require(orientationOptions.any { it.first == value })
context.getSharedPreferences("app-ui", Context.MODE_PRIVATE).edit().putInt("orientation", value).apply()
}
internal fun normalizeScale(percent: Int): Int = percent.takeIf { it in scales } ?: DEFAULT_SCALE
fun scale(context: Context): Int = normalizeScale(
@@ -1,59 +0,0 @@
package com.droiddeck.launcher.core
import android.content.Context
import android.util.Log
import java.util.concurrent.Executors
object BackgroundAppLimit {
private const val TAG = "BackgroundAppLimit"
const val OFF = -1
val CHOICES = listOf(OFF, 4, 1, 0)
private const val KEY = "max_cached_processes"
private const val STATE = "background-app-limit"
private const val APPLIED = "applied"
private const val PREVIOUS = "previous"
private val worker = Executors.newSingleThreadExecutor { Thread(it, "background-app-limit").apply { isDaemon = true } }
internal fun readCommand() = "device_config get activity_manager $KEY"
internal fun setCommand(limit: Int): String {
require(limit in 0..1024)
return "device_config put activity_manager $KEY $limit"
}
internal fun restoreCommand(previous: String?) =
if (previous == null) "device_config delete activity_manager $KEY" else "device_config put activity_manager $KEY $previous"
internal fun parse(output: String): String? = output.trim().takeIf { it.toIntOrNull() != null }
private fun state(context: Context) = context.applicationContext.getSharedPreferences(STATE, Context.MODE_PRIVATE)
fun applied(context: Context): Boolean = state(context).getBoolean(APPLIED, false)
fun applyAsync(context: Context, limit: Int) {
val app = context.applicationContext
worker.execute {
runCatching {
if (!applied(app)) {
val previous = parse(WirelessAdbFix.shell(app, readCommand()))
check(state(app).edit().putBoolean(APPLIED, true).putString(PREVIOUS, previous).commit()) { "previous value not saved" }
}
WirelessAdbFix.shell(app, setCommand(limit))
}.onSuccess { Log.i(TAG, "at most $limit cached app(s) while the session runs") }
.onFailure { Log.w(TAG, "limit not set: ${it.message}") }
}
}
fun restoreAsync(context: Context) {
val app = context.applicationContext
worker.execute {
if (!applied(app)) return@execute
val previous = state(app).getString(PREVIOUS, null)
runCatching { WirelessAdbFix.shell(app, restoreCommand(previous)) }
.onSuccess {
state(app).edit().putBoolean(APPLIED, false).remove(PREVIOUS).commit()
Log.i(TAG, "cached-app limit put back (${previous ?: "Android's own"})")
}
.onFailure { Log.w(TAG, "limit not put back yet: ${it.message}") }
}
}
}
@@ -0,0 +1,27 @@
package com.droiddeck.launcher.core
import com.droiddeck.launcher.gpu.GpuInfo
import com.droiddeck.launcher.gpu.SocNames
object CpuAllocationDefaults {
data class Allocation(val client: String, val game: String)
val device: Allocation? by lazy {
forDevice(SocNames.model(), GpuInfo.systemProperty("ro.board.platform"),
CpuCores.all.associateWith(CpuCores::maxGhz))
}
fun forDevice(model: String, platform: String, frequencies: Map<Int, Double?>): Allocation? {
val chip = model.trim().uppercase().substringBefore('-')
val gen2 = chip in setOf("SM8550", "QCS8550") ||
(chip.isEmpty() && platform.trim().equals("kalama", ignoreCase = true))
if (!gen2 || frequencies.size != 8 || frequencies.keys.any { it < 0 } ||
frequencies.values.any { it == null || !it.isFinite() || it <= 0.0 }) return null
val clusters = frequencies.entries.groupBy { it.value }.toSortedMap(compareBy { it ?: 0.0 }).values.toList()
if (clusters.map { it.size } != listOf(3, 4, 1)) return null
return Allocation(
client = CpuCores.format(clusters[2].map { it.key }.toSet()),
game = CpuCores.format(clusters[1].map { it.key }.toSet()),
)
}
}
Loaded 100 of 1820 files, more files were not shown because too many files have changed in this diff. Show more