mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
Merge commit 'e1a7d527fd412b6bd78833e01fe80f515e324971' into merge/main-into-non-adreno-all
# Conflicts: # .github/workflows/build.yml # app/build.gradle # app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt # app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt # app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt # app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java # app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt # app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java # app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt # app/src/main/java/com/droiddeck/launcher/session/SessionService.kt # app/src/main/res/values/strings.xml # app/src/test/java/com/droiddeck/launcher/ui/ComponentsPageProfileTest.kt # tools/build_local.sh # tools/gamescope/PATCHES.md # tools/gamescope/release.env # tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env # tools/linuxfs/preload/netif.c # tools/linuxfs/preload/opens.c # tools/release/runtime_inputs.py
This commit is contained in:
commit
e7b523d653
574 files changed
+41749
-2616
No files matched your search
@@ -0,0 +1,40 @@
|
||||
name: Build validation
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened, edited, ready_for_review]
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_run:
|
||||
workflows: [Build APK]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: Pull request to validate; empty for main
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
pull-requests: read
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Build APK
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.workflow_sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Validate source and APK compilation
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh auth setup-git
|
||||
python3 tools/release/validate_apk.py
|
||||
@@ -64,6 +64,7 @@ jobs:
|
||||
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)"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-kde
|
||||
@@ -74,6 +75,8 @@ jobs:
|
||||
out/desktop-kde.packages.txt
|
||||
out/desktop-kde.kwin.txt
|
||||
out/desktop-kde.sha256
|
||||
out/desktop-kde.i18n.txt
|
||||
out/desktop-kde-i18n-*.tar.zst
|
||||
- name: Publish
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
|
||||
env:
|
||||
@@ -88,5 +91,6 @@ jobs:
|
||||
fi
|
||||
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.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
|
||||
gh release edit "$TAG" --latest=false --prerelease=false
|
||||
+134
-22
@@ -29,8 +29,65 @@ env:
|
||||
PRELOAD_CACHE_VERSION: v6
|
||||
|
||||
jobs:
|
||||
xbox:
|
||||
name: Xbox ARM64 backend
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
base-sha: ${{ steps.merge.outputs.base-sha }}
|
||||
tree-sha: ${{ steps.source.outputs.tree-sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Pin and merge the APK source
|
||||
id: merge
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin main
|
||||
echo "base-sha=$(git rev-parse origin/main)" >> "$GITHUB_OUTPUT"
|
||||
git config user.name "ci"
|
||||
git config user.email "ci@users.noreply.github.com"
|
||||
git merge --no-ff --no-edit origin/main
|
||||
- name: Record backend source tree
|
||||
id: source
|
||||
run: echo "tree-sha=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/cache@v4
|
||||
id: xbox-cache
|
||||
with:
|
||||
path: |
|
||||
app/src/main/assets/linuxfs/usr/local/lib/droiddeck-xbox
|
||||
app/build/xbox/xodus-droiddeck-source.tar.xz
|
||||
app/build/xbox/runtime/*-source.tar.xz
|
||||
key: xbox-arm64-${{ hashFiles('tools/xbox/**') }}
|
||||
- name: Build Xbox backend
|
||||
if: steps.xbox-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
rustup toolchain install 1.99.0 --profile minimal
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y build-essential pkg-config libssl-dev protobuf-compiler cmake xz-utils
|
||||
bash tools/xbox/build.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xbox-arm64
|
||||
path: app/src/main/assets/linuxfs/usr/local/lib/droiddeck-xbox
|
||||
retention-days: 14
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xbox-corresponding-source
|
||||
path: |
|
||||
app/build/xbox/xodus-droiddeck-source.tar.xz
|
||||
app/build/xbox/runtime/*-source.tar.xz
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
build:
|
||||
name: Build APK
|
||||
needs: xbox
|
||||
name: Compile and test APK
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
@@ -42,18 +99,24 @@ jobs:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check co-authors
|
||||
if: github.event_name == 'pull_request'
|
||||
- name: Check human authorship
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BEFORE: ${{ github.event.before }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin main
|
||||
base=$(git merge-base origin/main HEAD)
|
||||
messages=$(git log --format=%B "$base..HEAD")
|
||||
messages+=$'\n'"$(jq -r '.pull_request.title, (.pull_request.body // "")' "$GITHUB_EVENT_PATH")"
|
||||
if grep -Eiq '^[[:space:]]*Co-authored-by:.*(Claude|Anthropic|OpenAI|ChatGPT|GPT|Copilot|Cursor|Gemini|Codex|Devin|Windsurf|Codeium|Amazon Q|Tabnine|Cody|\[bot\])' <<<"$messages"; then
|
||||
echo "::error::AI and bot identities cannot be co-authors; remove the Co-authored-by trailer."
|
||||
exit 1
|
||||
if [ "$EVENT_NAME" = pull_request ]; then
|
||||
git fetch --no-tags origin main
|
||||
base=$(git merge-base origin/main HEAD)
|
||||
checker="$RUNNER_TEMP/droiddeck-check-authorship.py"
|
||||
git show origin/main:tools/release/check_authorship.py > "$checker"
|
||||
python3 "$checker" "$base..HEAD" --event "$GITHUB_EVENT_PATH"
|
||||
elif [ "$EVENT_NAME" = push ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then
|
||||
python3 tools/release/check_authorship.py "$BEFORE..HEAD"
|
||||
else
|
||||
python3 tools/release/check_authorship.py HEAD
|
||||
fi
|
||||
|
||||
- name: The packages a signed build ships as
|
||||
@@ -87,14 +150,17 @@ jobs:
|
||||
|
||||
- name: Merge onto main, or say what conflicts
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
BUILD_BASE_SHA: ${{ needs.xbox.outputs.base-sha }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
git config user.name "ci"
|
||||
git config user.email "ci@users.noreply.github.com"
|
||||
git fetch --no-tags origin main
|
||||
BASE=$(git rev-parse --short origin/main)
|
||||
git fetch --no-tags origin "$BUILD_BASE_SHA"
|
||||
BASE=$(git rev-parse --short FETCH_HEAD)
|
||||
HEAD=$(git rev-parse --short HEAD)
|
||||
if git merge --no-ff --no-edit origin/main >/tmp/merge.out 2>&1; then
|
||||
if git merge --no-ff --no-edit FETCH_HEAD >/tmp/merge.out 2>&1; then
|
||||
{
|
||||
echo "### Mergeable"
|
||||
echo
|
||||
@@ -141,6 +207,11 @@ jobs:
|
||||
echo "::error::the pull request conflicts with main: $(echo $FILES | tr '\n' ' ')"
|
||||
exit 1
|
||||
|
||||
- name: Verify backend and APK source match
|
||||
env:
|
||||
XBOX_SOURCE_TREE: ${{ needs.xbox.outputs.tree-sha }}
|
||||
run: test "$(git rev-parse 'HEAD^{tree}')" = "$XBOX_SOURCE_TREE"
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
@@ -185,15 +256,14 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: app/src/main/assets/linuxfs
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'app/src/main/cpp/adrenotools/src/hook/kgsl.h', 'tools/quest/*', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/linuxfs/xclipse-layer/*', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*', 'tools/msitools/**', 'tools/comet/**', 'external/MaliBridge/client/**', 'external/MaliBridge/common/**', 'external/MaliBridge/tests/**', 'external/MaliBridge/scripts/build-client-linux.sh', 'tools/venus/glprobe.c', 'tools/linuxfs/debian-image.env', 'tools/vr/**') }}
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'app/src/main/cpp/adrenotools/src/hook/kgsl.h', 'tools/quest/*', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/linuxfs/xclipse-layer/*', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*', 'tools/msitools/**', 'tools/comet/**', 'tools/xbox/**', 'external/MaliBridge/client/**', 'external/MaliBridge/common/**', 'external/MaliBridge/tests/**', 'external/MaliBridge/scripts/build-client-linux.sh', 'tools/venus/glprobe.c', 'tools/linuxfs/debian-image.env', 'tools/vr/**') }}
|
||||
|
||||
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get -o Dir::Etc::sourcelist=sources.list.d/ubuntu.sources \
|
||||
-o Dir::Etc::sourceparts=- -o Acquire::Retries=3 update -qq
|
||||
sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libvulkan-dev libvulkan1 mesa-vulkan-drivers >/dev/null
|
||||
D=app/src/main/assets/linuxfs
|
||||
mkdir -p "$D"
|
||||
@@ -237,7 +307,7 @@ jobs:
|
||||
done
|
||||
if [ -f tools/gamescope/release.env ]; then
|
||||
. tools/gamescope/release.env
|
||||
gh release download "${GAMESCOPE_TAG}" -R "${GAMESCOPE_REPO:-${GITHUB_REPOSITORY}}" -p gamescope.tzst -O gamescope.tzst
|
||||
curl -fsSL --retry 3 -o gamescope.tzst "https://github.com/${GAMESCOPE_REPO:-${GITHUB_REPOSITORY}}/releases/download/${GAMESCOPE_TAG}/gamescope.tzst"
|
||||
echo "${GAMESCOPE_SHA256} gamescope.tzst" | sha256sum -c -
|
||||
tar --use-compress-program=unzstd -xf gamescope.tzst -C "$D"
|
||||
test -f "$D/usr/local/bin/gamescope"
|
||||
@@ -268,6 +338,17 @@ jobs:
|
||||
install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp"
|
||||
rm -rf mango-pkgs
|
||||
ls -l "$M"
|
||||
mkdir -p pw-pkgs
|
||||
grep -v '^#' tools/pipewire/packages.txt | while read -r sum url; do
|
||||
[ -n "$url" ] || continue
|
||||
curl -fsSL --retry 3 -o pw-pkgs/pkg.tar.zst "$url"
|
||||
echo "$sum pw-pkgs/pkg.tar.zst" | sha256sum -c -
|
||||
tar --use-compress-program=unzstd -xf pw-pkgs/pkg.tar.zst -C pw-pkgs
|
||||
rm pw-pkgs/pkg.tar.zst
|
||||
done
|
||||
tools/pipewire/assemble.sh pw-pkgs "$D/usr/local/lib/droiddeck-pipewire.tzst"
|
||||
rm -rf pw-pkgs
|
||||
ls -l "$D/usr/local/lib/droiddeck-pipewire.tzst"
|
||||
T="$D/usr/local/lib/droiddeck-msitools"
|
||||
mkdir -p "$T" msi-pkgs
|
||||
grep -v '^#' tools/msitools/packages.txt | while read -r sum url; do
|
||||
@@ -340,18 +421,17 @@ jobs:
|
||||
ls -l app/src/main/assets/linuxfs/x86_64 app/src/main/assets/linuxfs/i386
|
||||
|
||||
- name: Bundle the droiddeck-esync packs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -f tools/droiddeck-esync/release.env ]; then
|
||||
. tools/droiddeck-esync/release.env
|
||||
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p "$SYNC_BUNDLE_ASSET" -O sync-bundle.tzst
|
||||
release_url="https://github.com/${SYNC_BUNDLE_REPO}/releases/download/${SYNC_BUNDLE_TAG}"
|
||||
curl -fsSL --retry 3 -o sync-bundle.tzst "$release_url/$SYNC_BUNDLE_ASSET"
|
||||
echo "$SYNC_BUNDLE_SHA256 sync-bundle.tzst" | sha256sum -c -
|
||||
mkdir -p app/src/main/assets/droiddeck-esync
|
||||
tar --use-compress-program=unzstd -xf sync-bundle.tzst -C app/src/main/assets/droiddeck-esync
|
||||
rm sync-bundle.tzst
|
||||
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p index.json -O sync-index.json
|
||||
curl -fsSL --retry 3 -o sync-index.json "$release_url/index.json"
|
||||
revoked=$(python3 -c 'import json, sys; print(" ".join(p["id"] for p in json.load(open(sys.argv[1]))["packs"] if p.get("revoked") is True))' sync-index.json)
|
||||
for id in $revoked; do
|
||||
if [ -e "app/src/main/assets/droiddeck-esync/packs/$id.tzst" ]; then
|
||||
@@ -450,6 +530,8 @@ jobs:
|
||||
/tmp/test-xr-tracking
|
||||
g++ -std=c++17 -Wall -Wextra -Werror tools/vr/tests/test_xr_touch.cpp -o /tmp/test-xr-touch
|
||||
/tmp/test-xr-touch
|
||||
g++ -std=c++17 -Wall -Wextra -Werror tools/vr/tests/test_pad_controller_config.cpp -o /tmp/test-pad-controller-config
|
||||
/tmp/test-pad-controller-config
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_vsync.cpp -o /tmp/test-xr-vsync
|
||||
/tmp/test-xr-vsync
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_frames.cpp -o /tmp/test-xr-frames
|
||||
@@ -499,6 +581,11 @@ jobs:
|
||||
restore-keys: |
|
||||
cxx-${{ env.NDK_VERSION }}-
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: xbox-arm64
|
||||
path: app/src/main/assets/linuxfs/usr/local/lib/droiddeck-xbox
|
||||
|
||||
- name: Build GOG Galaxy companion
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -506,6 +593,9 @@ jobs:
|
||||
rustup target add --toolchain 1.90.0 aarch64-unknown-linux-gnu
|
||||
RUSTUP_TOOLCHAIN=1.90.0 bash tools/comet/build.sh
|
||||
|
||||
- name: Build the add-on packs
|
||||
run: bash tools/packs/build.sh app/build/packs
|
||||
|
||||
- name: Build
|
||||
id: build
|
||||
env:
|
||||
@@ -545,6 +635,11 @@ jobs:
|
||||
path: app/src/main/rust/stores/Cargo.lock
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Install Python test dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq python3-yaml
|
||||
|
||||
- name: Steam import and game environment tests
|
||||
run: |
|
||||
# The Xclipse layer regression test compiles the layer against the distribution's headers.
|
||||
@@ -608,6 +703,14 @@ jobs:
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: droiddeck-packs
|
||||
path: app/build/packs/*.tar.zst
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
|
||||
- name: Report on the pull request
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
@@ -710,8 +813,17 @@ jobs:
|
||||
variants: ${{ needs.build.outputs.variants }}
|
||||
secrets: inherit
|
||||
|
||||
packs:
|
||||
needs: build
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
uses: ./.github/workflows/publish-packs.yml
|
||||
secrets: inherit
|
||||
|
||||
announce:
|
||||
needs: [build, sign]
|
||||
needs: [build, sign, packs]
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -176,6 +176,27 @@ jobs:
|
||||
pattern: droiddeck-pr-signed-*
|
||||
merge-multiple: true
|
||||
path: signed
|
||||
- name: The add-on packs main has not published
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN: ${{ needs.authorize.outputs.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh run download "$RUN" -R "$GITHUB_REPOSITORY" -n droiddeck-packs -D packs
|
||||
mkdir -p pr-packs
|
||||
for file in packs/*.tar.zst; do
|
||||
name=$(basename "$file" .tar.zst)
|
||||
sha=$(sha256sum "$file" | cut -d' ' -f1)
|
||||
tag="pack-$name-${sha:0:16}"
|
||||
digest=$({ gh api "repos/Droid-Deck/DroidDeck-Components/releases/tags/$tag" 2>/dev/null || echo '{}'; } |
|
||||
jq -r --arg n "$name.tar.zst" '.assets // [] | .[] | select(.name == $n) | .digest // empty')
|
||||
if [ "$digest" = "sha256:$sha" ]; then
|
||||
echo "$name: served from DroidDeck-Components $tag"
|
||||
else
|
||||
cp "$file" "pr-packs/$tag.tar.zst"
|
||||
echo "$name: attached to the PR release as $tag.tar.zst"
|
||||
fi
|
||||
done
|
||||
- name: Describe the change
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -233,6 +254,7 @@ jobs:
|
||||
first=true
|
||||
fi
|
||||
echo "first=$first" >> "$GITHUB_OUTPUT"
|
||||
cp pr-packs/* signed/ 2>/dev/null || true
|
||||
gh release upload "$tag" signed/* -R "$repo" --clobber
|
||||
gh release edit "$tag" -R "$repo" --title "PR #$PR_NUMBER" --notes "$notes" --prerelease
|
||||
keep=$(cd signed && ls | jq -R . | jq -sc .)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
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:
|
||||
run_id:
|
||||
description: The Build APK run whose packs to publish; empty for the calling run's own
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: publish add-on packs
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
concurrency:
|
||||
group: publish-packs
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: The packs (this run's build)
|
||||
if: inputs.run_id == ''
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: droiddeck-packs
|
||||
path: packs
|
||||
|
||||
- name: The packs (another Build APK run)
|
||||
if: inputs.run_id != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN: ${{ inputs.run_id }}
|
||||
run: gh run download "$RUN" -R "$GITHUB_REPOSITORY" -n droiddeck-packs -D packs
|
||||
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: components-token
|
||||
with:
|
||||
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
|
||||
owner: Droid-Deck
|
||||
repositories: DroidDeck-Components
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.components-token.outputs.token }}
|
||||
REPO: Droid-Deck/DroidDeck-Components
|
||||
BUILD_RUN: ${{ inputs.run_id || github.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
published_digest() {
|
||||
{ gh api "repos/$REPO/releases/tags/$1" 2>/dev/null || echo '{}'; } |
|
||||
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; 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)
|
||||
tag="pack-$name-${sha:0:16}"
|
||||
digest=$(published_digest "$tag" "$name.tar.zst")
|
||||
if [ -z "$digest" ]; then
|
||||
notes="DroidDeck add-on pack \`$name\`, built by [Build APK run $BUILD_RUN]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN) from \`$commit\` with tools/packs/build.sh and published by [run $GITHUB_RUN_ID]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID).
|
||||
|
||||
Size: $(stat -c %s "$file") bytes. SHA-256: $sha. APKs built in the same run pin this digest."
|
||||
gh release create "$tag" -R "$REPO" --title "$tag" --notes "$notes" --latest=false "$file" \
|
||||
|| echo "::warning::creating $tag failed; checking whether another run published it"
|
||||
digest=$(published_digest "$tag" "$name.tar.zst")
|
||||
fi
|
||||
if [ "$digest" != "sha256:$sha" ]; then
|
||||
echo "::error::$tag holds ${digest:-nothing} for $name.tar.zst, not sha256:$sha"
|
||||
exit 1
|
||||
fi
|
||||
echo "- \`$name\`: [$tag]($GITHUB_SERVER_URL/$REPO/releases/tag/$tag) \`$sha\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
done
|
||||
@@ -63,9 +63,19 @@ jobs:
|
||||
artifact: droiddeck-release
|
||||
secrets: inherit
|
||||
|
||||
packs:
|
||||
needs: find
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
uses: ./.github/workflows/publish-packs.yml
|
||||
with:
|
||||
run_id: ${{ needs.find.outputs.run_id }}
|
||||
secrets: inherit
|
||||
|
||||
publish:
|
||||
name: publish the release
|
||||
needs: [find, sign]
|
||||
needs: [find, sign, packs]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
|
||||
@@ -21,7 +21,7 @@ On Android 14 and newer, before Steam launches, you must turn on **Disable child
|
||||
|
||||
## Smart glasses
|
||||
|
||||
Use **Setup → Smart glasses** for app-wide virtual-screen settings across DroidDeck menus, launchers, desktop sessions, and games. Supported VITURE, RayNeo, and XREAL 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.
|
||||
Use **Setup → Smart glasses** for app-wide virtual-screen settings across DroidDeck menus, launchers, desktop sessions, and games. Supported VITURE, RayNeo, XREAL, and Rokid connections provide 3DoF; Luma Ultra adds optional experimental 6DoF and stereo-assisted hand/Joy-Con tracking. Model support and SDK requirements differ: see the [smart-glasses support matrix](docs/smart-glasses-support.md) for supported devices, controls, setup, and limitations.
|
||||
|
||||
## Community
|
||||
|
||||
@@ -47,10 +47,12 @@ Compatibility and performance vary by device; hardware validation is limited. De
|
||||
|
||||
## Credits and licence
|
||||
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). 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. 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. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). 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. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
|
||||
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). 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 and REDMAGIC are trademarks of their owners; this project is not affiliated with them.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
+33
-3
@@ -84,6 +84,23 @@ def commitTime = (gitValue(['show', '-s', '--format=%ct', 'HEAD']) ?: '0').with
|
||||
def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
|
||||
.findAll { it.contains('=') && !it.startsWith('#') }
|
||||
.collectEntries { def kv = it.split('=', 2); [(kv[0].trim()): kv[1].trim()] }
|
||||
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 addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet'].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")
|
||||
return [(name): [sha256: '', size: 0L]]
|
||||
}
|
||||
def digest = java.security.MessageDigest.getInstance('SHA-256')
|
||||
pack.withInputStream { input ->
|
||||
byte[] buffer = new byte[1 << 20]
|
||||
int n
|
||||
while ((n = input.read(buffer)) > 0) digest.update(buffer, 0, n)
|
||||
}
|
||||
[(name): [sha256: digest.digest().encodeHex().toString(), size: pack.length()]]
|
||||
}
|
||||
def desktopRemovalSources = new groovy.json.JsonSlurper().parse(file('src/main/assets/desktop-removal/sources.json'))
|
||||
if (desktopRemovalSources.archives[desktopKde.DESKTOP_KDE_TAG] != desktopKde.DESKTOP_KDE_SHA256) {
|
||||
throw new GradleException('Desktop removal inventories do not match the pinned archive')
|
||||
@@ -119,6 +136,10 @@ android {
|
||||
buildConfigField "String", "DESKTOP_KDE_URL", "\"https://github.com/${desktopKde.DESKTOP_KDE_REPO}/releases/download/${desktopKde.DESKTOP_KDE_TAG}/desktop-kde.tar.zst\""
|
||||
buildConfigField "String", "DESKTOP_KDE_SHA256", "\"${desktopKde.DESKTOP_KDE_SHA256}\""
|
||||
buildConfigField "long", "DESKTOP_KDE_SIZE", "${desktopKde.DESKTOP_KDE_SIZE}L"
|
||||
addonPacks.each { name, pack ->
|
||||
buildConfigField "String", "PACK_${name.toUpperCase()}_SHA256", "\"${pack.sha256}\""
|
||||
buildConfigField "long", "PACK_${name.toUpperCase()}_SIZE", "${pack.size}L"
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a'
|
||||
@@ -175,6 +196,11 @@ android {
|
||||
disable 'ExpiredTargetSdkVersion'
|
||||
}
|
||||
|
||||
androidResources {
|
||||
// aapt's default ignore list, plus the trees shipped as add-on packs (tools/packs).
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~:<dir>droiddeck-xbox:<dir>droiddeck-comet:<dir>directaudio'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF']
|
||||
@@ -182,6 +208,8 @@ 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']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +223,8 @@ android {
|
||||
unitTests.returnDefaultValues = true
|
||||
unitTests.includeAndroidResources = true
|
||||
unitTests.all {
|
||||
// Robolectric keeps a sandbox per SDK level the tests ask for; seven no longer fit in Gradle's default 512 MB.
|
||||
maxHeapSize = '2g'
|
||||
def logTarget = providers.systemProperty('robolectric.logging').orNull
|
||||
if (logTarget != null) systemProperty 'robolectric.logging', logTarget
|
||||
}
|
||||
@@ -316,9 +346,8 @@ def verifyHandModel = tasks.register('verifyHandModel') {
|
||||
}
|
||||
}
|
||||
tasks.named('preBuild').configure { dependsOn(verifyHandModel) }
|
||||
|
||||
dependencies {
|
||||
implementation 'org.opencv:opencv:4.13.0'
|
||||
implementation "org.opencv:opencv:${trackingPack.OPENCV_VERSION}"
|
||||
implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49'
|
||||
implementation 'com.github.luben:zstd-jni:1.5.7-4@aar'
|
||||
implementation 'org.apache.commons:commons-compress:1.27.1'
|
||||
@@ -337,7 +366,8 @@ dependencies {
|
||||
implementation 'com.termux.termux-app:terminal-view:0.118.0'
|
||||
implementation 'androidx.camera:camera-camera2:1.4.2'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.4.2'
|
||||
implementation('com.google.mediapipe:tasks-vision:1.0.0') {
|
||||
implementation 'androidx.webkit:webkit:1.12.1'
|
||||
implementation("com.google.mediapipe:tasks-vision:${trackingPack.MEDIAPIPE_VERSION}") {
|
||||
exclude group: 'com.google.auto.value', module: 'auto-value'
|
||||
}
|
||||
|
||||
|
||||
@@ -65,11 +65,6 @@
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden|navigation">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.CAR_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -88,6 +83,31 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".LauncherDark"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.CAR_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".LauncherLight"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:icon="@mipmap/ic_launcher_light"
|
||||
android:enabled="false"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.CAR_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name=".HomeActivity"
|
||||
android:enabled="false"
|
||||
@@ -127,6 +147,10 @@
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity android:name=".stores.xbox.XboxLoginActivity" android:exported="false"
|
||||
android:process=":xboxlogin" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".stores.StoreLoginActivity"
|
||||
android:exported="false"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Holger Bartnick
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Binary file not shown.
@@ -11,23 +11,23 @@ add_subdirectory(adrenotools)
|
||||
# DXVK's DXBC->SPIR-V translator is zlib licensed (vendored under thirdparty/dxbc).
|
||||
add_subdirectory(thirdparty/dxbc)
|
||||
|
||||
set(WINFG_UPSTREAM "v0.3.0")
|
||||
set(WINFG_CHAIN_SRC
|
||||
framegen/winfg/framegen.cpp
|
||||
framegen/winfg/framegen.hpp
|
||||
framegen/winfg/record_impl.inc
|
||||
framegen/winfg/config.hpp
|
||||
framegen/winfg/embedded_shaders.hpp
|
||||
framegen/winfg/winfg_engine.cpp
|
||||
set(DDFG_UPSTREAM "v0.3.0")
|
||||
set(DDFG_CHAIN_SRC
|
||||
framegen/ddfg/framegen.cpp
|
||||
framegen/ddfg/framegen.hpp
|
||||
framegen/ddfg/record_impl.inc
|
||||
framegen/ddfg/config.hpp
|
||||
framegen/ddfg/embedded_shaders.hpp
|
||||
framegen/ddfg/ddfg_engine.cpp
|
||||
)
|
||||
set(WINFG_HASH_INPUT "")
|
||||
foreach(f ${WINFG_CHAIN_SRC})
|
||||
set(DDFG_HASH_INPUT "")
|
||||
foreach(f ${DDFG_CHAIN_SRC})
|
||||
file(SHA256 "${CMAKE_CURRENT_SOURCE_DIR}/${f}" _h)
|
||||
string(APPEND WINFG_HASH_INPUT "${_h}")
|
||||
string(APPEND DDFG_HASH_INPUT "${_h}")
|
||||
endforeach()
|
||||
string(SHA256 WINFG_CHAIN_HASH_FULL "${WINFG_HASH_INPUT}")
|
||||
string(SUBSTRING "${WINFG_CHAIN_HASH_FULL}" 0 12 WINFG_CHAIN_HASH)
|
||||
message(STATUS "win-fg chain: ${WINFG_UPSTREAM} (src ${WINFG_CHAIN_HASH})")
|
||||
string(SHA256 DDFG_CHAIN_HASH_FULL "${DDFG_HASH_INPUT}")
|
||||
string(SUBSTRING "${DDFG_CHAIN_HASH_FULL}" 0 12 DDFG_CHAIN_HASH)
|
||||
message(STATUS "dd-fg chain: win-fg ${DDFG_UPSTREAM} (src ${DDFG_CHAIN_HASH})")
|
||||
|
||||
add_subdirectory(waylandcomp)
|
||||
add_subdirectory(questxr)
|
||||
@@ -39,6 +39,10 @@ target_link_libraries(deviceinfo dl)
|
||||
|
||||
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)
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#ifdef __GLIBC__
|
||||
#include <gnu/lib-names.h>
|
||||
#endif
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <linux/joystick.h>
|
||||
@@ -1181,8 +1184,9 @@ struct TritonSharedData {
|
||||
uint32_t input_length;
|
||||
uint8_t input[64];
|
||||
uint64_t output_seq;
|
||||
uint32_t output_feature;
|
||||
uint32_t output_length;
|
||||
uint16_t output_feature;
|
||||
uint16_t output_length;
|
||||
uint32_t output_ack;
|
||||
uint8_t output[64];
|
||||
uint64_t get_request;
|
||||
uint64_t get_reply;
|
||||
@@ -1199,6 +1203,8 @@ static_assert(offsetof(TritonSharedData, report_id) == 16, "Triton report ID off
|
||||
static_assert(offsetof(TritonSharedData, input) == 24, "Triton input offset");
|
||||
static_assert(offsetof(TritonSharedData, output_seq) == 88, "Triton output sequence offset");
|
||||
static_assert(offsetof(TritonSharedData, output_feature) == 96, "Triton output feature offset");
|
||||
static_assert(offsetof(TritonSharedData, output_length) == 98, "Triton output length offset");
|
||||
static_assert(offsetof(TritonSharedData, output_ack) == 100, "Triton output ack offset");
|
||||
static_assert(offsetof(TritonSharedData, output) == 104, "Triton output offset");
|
||||
static_assert(offsetof(TritonSharedData, get_request) == 168, "Triton feature request offset");
|
||||
static_assert(offsetof(TritonSharedData, get_reply) == 176, "Triton feature reply offset");
|
||||
@@ -1216,6 +1222,8 @@ 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";
|
||||
static const uint8_t kTritonReportDescriptor[] = {
|
||||
@@ -2571,11 +2579,21 @@ __attribute__((visibility("hidden"))) static void triton_queue_output(DeckHidraw
|
||||
if (!data || !size || size > 64 || triton.mapping_size < TRITON_SHARED_OFFSET + TRITON_SHARED_BLOCK_SIZE) return;
|
||||
auto *shared = reinterpret_cast<TritonSharedData *>(reinterpret_cast<uint8_t *>(triton.ring) + TRITON_SHARED_OFFSET);
|
||||
if (__atomic_load_n(&shared->magic, __ATOMIC_ACQUIRE) != TRITON_SHARED_MAGIC) return;
|
||||
static std::mutex one_at_a_time;
|
||||
std::lock_guard<std::mutex> guard(one_at_a_time);
|
||||
for (int waited = 0;
|
||||
static_cast<uint32_t>(__atomic_load_n(&shared->output_seq, __ATOMIC_ACQUIRE)) !=
|
||||
__atomic_load_n(&shared->output_ack, __ATOMIC_ACQUIRE) &&
|
||||
__atomic_load_n(&shared->connected, __ATOMIC_ACQUIRE) != 0 && waited < TRITON_OUTPUT_TIMEOUT_MS * 4;
|
||||
waited++) {
|
||||
struct timespec interval = {0, 250 * 1000L};
|
||||
nanosleep(&interval, nullptr);
|
||||
}
|
||||
uint64_t seq = __atomic_load_n(&shared->output_seq, __ATOMIC_RELAXED);
|
||||
if (seq & 1) seq++;
|
||||
__atomic_store_n(&shared->output_seq, seq + 1, __ATOMIC_RELEASE);
|
||||
shared->output_feature = feature ? 1 : 0;
|
||||
shared->output_length = static_cast<uint32_t>(size);
|
||||
shared->output_length = static_cast<uint16_t>(size);
|
||||
memcpy(shared->output, data, size);
|
||||
if (size < sizeof(shared->output)) memset(shared->output + size, 0, sizeof(shared->output) - size);
|
||||
__atomic_thread_fence(__ATOMIC_RELEASE);
|
||||
@@ -2632,6 +2650,38 @@ __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;
|
||||
char exe[PATH_MAX];
|
||||
ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
|
||||
if (n > 0) {
|
||||
std::string path(exe, static_cast<size_t>(n));
|
||||
size_t bin = path.rfind('/');
|
||||
size_t root = bin == std::string::npos || bin == 0 ? std::string::npos : path.rfind('/', bin - 1);
|
||||
if (bin != std::string::npos && bin > 0) roots.push_back(path.substr(0, bin));
|
||||
if (root != std::string::npos) roots.push_back(path.substr(0, root));
|
||||
}
|
||||
if (const char *home = getenv("HOME")) roots.push_back(std::string(home) + "/.local/share/Steam");
|
||||
uint32_t newest = TRITON_FW_TIMESTAMP;
|
||||
for (const std::string &root : roots) {
|
||||
std::ifstream cfg(root + "/bin/hardwareupdater/hardwareupdater.cfg");
|
||||
std::string line;
|
||||
while (std::getline(cfg, line)) {
|
||||
if (line.rfind("TRITON_FW_TS:", 0) != 0) continue;
|
||||
uint32_t shipped = static_cast<uint32_t>(strtoul(line.c_str() + 13, nullptr, 16));
|
||||
if (shipped > newest) newest = shipped;
|
||||
}
|
||||
}
|
||||
if (newest != TRITON_FW_TIMESTAMP) Logger::log("triton: reporting firmware %08x, the build Steam ships\n", newest);
|
||||
return newest;
|
||||
}();
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static int triton_pad_feature(DeckHidraw &triton, uint8_t *buf, size_t size, bool set) {
|
||||
if (!buf || size < 2) {
|
||||
errno = EINVAL;
|
||||
@@ -2652,7 +2702,7 @@ __attribute__((visibility("hidden"))) static int triton_pad_feature(DeckHidraw &
|
||||
switch (triton.pending_feature) {
|
||||
case DECK_GET_ATTRIBUTES_VALUES: {
|
||||
static const uint8_t kIds[] = {0x01, 0x02, 0x04, 0x09, 0x0a, 0x0b};
|
||||
const uint32_t values[] = {static_cast<uint32_t>(TRITON_WIRED_PRODUCT), 0, TRITON_FW_TIMESTAMP, TRITON_HWID, 0, 4000};
|
||||
const uint32_t values[] = {static_cast<uint32_t>(TRITON_WIRED_PRODUCT), 0, triton_fw_timestamp(), TRITON_HWID, 0, 4000};
|
||||
reply[2] = sizeof(kIds) * 5;
|
||||
for (size_t i = 0; i < sizeof(kIds); i++) {
|
||||
reply[3 + i * 5] = kIds[i];
|
||||
@@ -3577,7 +3627,21 @@ EXPORT int close(int fd) {
|
||||
static constexpr int kMaxRingWaitMs = 2;
|
||||
static constexpr int kLowLatencyRingWaitUs = 250;
|
||||
|
||||
EXPORT ssize_t read(int fd, void *buf, size_t count) {
|
||||
static ssize_t copy_input_events(void *buf, const struct input_event *events, size_t count) {
|
||||
size_t bytes = count * sizeof(struct input_event);
|
||||
struct iovec local = {const_cast<struct input_event *>(events), bytes};
|
||||
struct iovec remote = {buf, bytes};
|
||||
ssize_t copied = syscall(SYS_process_vm_writev, getpid(), &local, 1, &remote, 1, 0);
|
||||
if (copied < 0) return -1;
|
||||
if (copied != static_cast<ssize_t>(bytes)) {
|
||||
errno = EFAULT;
|
||||
copied -= copied % sizeof(struct input_event);
|
||||
if (!copied) return -1;
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
static ssize_t read_input(int fd, void *buf, size_t count) {
|
||||
if (!watched(fd)) return syscall(SYS_read, fd, buf, count);
|
||||
std::unique_lock<std::recursive_mutex> guard(controller_mutex());
|
||||
auto controller = controller_map().find(fd);
|
||||
@@ -3622,42 +3686,44 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) {
|
||||
if (events == 0) break;
|
||||
const uint8_t *ring_events = reinterpret_cast<const uint8_t *>(fake.ring) +
|
||||
FAKE_INPUT_RING_HEADER_SIZE;
|
||||
std::vector<struct input_event> input_events(events);
|
||||
for (size_t i = 0; i < events; ++i) {
|
||||
size_t index = (fake.read_seq + i) % FAKE_INPUT_RING_CAPACITY;
|
||||
RingEvent stored;
|
||||
memcpy(&stored, ring_events + index * FAKE_INPUT_EVENT_SIZE, FAKE_INPUT_EVENT_SIZE);
|
||||
struct input_event ev = {};
|
||||
struct input_event &ev = input_events[i];
|
||||
ev.time.tv_sec = stored.sec;
|
||||
ev.time.tv_usec = stored.usec;
|
||||
ev.type = stored.type;
|
||||
ev.code = stored.code;
|
||||
ev.value = stored.value;
|
||||
if (ev.type == EV_ABS) ev.code = presented_abs_code(fake, ev.code);
|
||||
memcpy(static_cast<uint8_t *>(buf) + i * sizeof(ev), &ev, sizeof(ev));
|
||||
}
|
||||
__atomic_thread_fence(__ATOMIC_ACQUIRE);
|
||||
if (snap.sequence != __atomic_load_n(&fake.ring->snapshot_seq, __ATOMIC_RELAXED))
|
||||
continue;
|
||||
fake.read_seq += events;
|
||||
return static_cast<ssize_t>(events * sizeof(struct input_event));
|
||||
ssize_t copied = copy_input_events(buf, input_events.data(), events);
|
||||
if (copied > 0) fake.read_seq += copied / sizeof(struct input_event);
|
||||
return copied;
|
||||
}
|
||||
}
|
||||
|
||||
size_t events = std::min(requested, fake.keyframe_remaining);
|
||||
struct input_event input_events[kNeutralEventCount] = {};
|
||||
struct timeval now = {};
|
||||
gettimeofday(&now, nullptr);
|
||||
for (size_t i = 0; i < events; ++i) {
|
||||
size_t index = kNeutralEventCount - fake.keyframe_remaining;
|
||||
struct input_event ev = {};
|
||||
size_t index = kNeutralEventCount - fake.keyframe_remaining + i;
|
||||
struct input_event &ev = input_events[i];
|
||||
ev.time = now;
|
||||
ev.type = kNeutralEvents[index].type;
|
||||
ev.code = kNeutralEvents[index].code;
|
||||
ev.value = keyframe_value(fake, ev.type, ev.code);
|
||||
if (ev.type == EV_ABS) ev.code = presented_abs_code(fake, ev.code);
|
||||
memcpy(static_cast<uint8_t *>(buf) + i * sizeof(ev), &ev, sizeof(ev));
|
||||
--fake.keyframe_remaining;
|
||||
}
|
||||
return static_cast<ssize_t>(events * sizeof(struct input_event));
|
||||
ssize_t copied = copy_input_events(buf, input_events, events);
|
||||
if (copied > 0) fake.keyframe_remaining -= copied / sizeof(struct input_event);
|
||||
return copied;
|
||||
}
|
||||
if (nonblock) {
|
||||
errno = EAGAIN;
|
||||
@@ -3673,6 +3739,52 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) {
|
||||
}
|
||||
}
|
||||
|
||||
EXPORT ssize_t read(int fd, void *buf, size_t count) {
|
||||
return read_input(fd, buf, count);
|
||||
}
|
||||
|
||||
EXPORT ssize_t readv(int fd, const struct iovec *iov, int iovcnt) {
|
||||
if (!is_fake_input_fd(fd)) return syscall(SYS_readv, fd, iov, iovcnt);
|
||||
if (iovcnt < 0 || iovcnt > IOV_MAX) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (iovcnt && !iov) {
|
||||
errno = EFAULT;
|
||||
return -1;
|
||||
}
|
||||
std::vector<struct iovec> copied_iov(static_cast<size_t>(iovcnt));
|
||||
if (iovcnt) {
|
||||
size_t bytes = copied_iov.size() * sizeof(struct iovec);
|
||||
struct iovec local = {copied_iov.data(), bytes};
|
||||
struct iovec remote = {const_cast<struct iovec *>(iov), bytes};
|
||||
ssize_t copied = syscall(SYS_process_vm_readv, getpid(), &local, 1, &remote, 1, 0);
|
||||
if (copied != static_cast<ssize_t>(bytes)) {
|
||||
errno = EFAULT;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
iov = copied_iov.data();
|
||||
size_t size = 0;
|
||||
for (int i = 0; i < iovcnt; ++i) {
|
||||
if (iov[i].iov_len > static_cast<size_t>(SSIZE_MAX) - size) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
size += iov[i].iov_len;
|
||||
}
|
||||
|
||||
ssize_t total = 0;
|
||||
for (int i = 0; i < iovcnt; ++i) {
|
||||
if (!iov[i].iov_len) continue;
|
||||
ssize_t got = read_input(fd, iov[i].iov_base, iov[i].iov_len);
|
||||
if (got < 0) return total ? total : -1;
|
||||
total += got;
|
||||
if (static_cast<size_t>(got) != iov[i].iov_len) break;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
EXPORT ssize_t write(int fd, const void *buf, size_t count) {
|
||||
static auto my_write = reinterpret_cast<decltype(&::write)>(dlsym(RTLD_NEXT, "write"));
|
||||
|
||||
@@ -4103,4 +4215,92 @@ EXPORT int __fxstat64(int version, int fd, struct stat64 *buf) {
|
||||
if (!real) return fstat64(fd, buf);
|
||||
return fake_stat_fd(fd, buf, [version](int f, struct stat64 *b) { return real(version, f, b); });
|
||||
}
|
||||
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
using dlsym_fn = void *(*)(void *, const char *);
|
||||
|
||||
static const char *const interposed_names[] = {
|
||||
"open", "open64", "openat", "openat64", "close", "read", "readv", "write", "writev", "ioctl",
|
||||
"poll", "ppoll", "select", "stat", "stat64", "fstat", "fstat64", "__xstat", "__xstat64",
|
||||
"__fxstat", "__fxstat64", "statx", "statfs", "statfs64", "fstatfs", "fstatfs64", "access",
|
||||
"faccessat", "opendir", "scandir", "inotify_add_watch",
|
||||
};
|
||||
|
||||
static dlsym_fn next_dlsym() {
|
||||
static const dlsym_fn fn = [] {
|
||||
#ifdef __x86_64__
|
||||
const char *base = "GLIBC_2.2.5";
|
||||
#else
|
||||
const char *base = "GLIBC_2.0";
|
||||
#endif
|
||||
for (const char *version : {base, "GLIBC_2.34"}) {
|
||||
if (auto found = reinterpret_cast<dlsym_fn>(dlvsym(RTLD_NEXT, "dlsym", version))) return found;
|
||||
}
|
||||
return dlsym_fn{};
|
||||
}();
|
||||
return fn;
|
||||
}
|
||||
|
||||
static void *global_dlsym(void *, const char *name) { return next_dlsym()(RTLD_DEFAULT, name); }
|
||||
|
||||
static void *missing_dlsym(void *, const char *) { return nullptr; }
|
||||
|
||||
static bool interposed(const char *name) {
|
||||
for (const char *interposed_name : interposed_names) {
|
||||
if (strcmp(interposed_name, name) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" __attribute__((visibility("hidden"), used)) void *fakeinput_dlsym_target(void *handle, const char *name) {
|
||||
dlsym_fn next = next_dlsym();
|
||||
if (!next) return reinterpret_cast<void *>(missing_dlsym);
|
||||
if (handle == RTLD_DEFAULT || handle == RTLD_NEXT || name == nullptr) return reinterpret_cast<void *>(next);
|
||||
static void *const libc = dlopen(LIBC_SO, RTLD_LAZY | RTLD_NOLOAD);
|
||||
if (handle != libc || !interposed(name)) return reinterpret_cast<void *>(next);
|
||||
return reinterpret_cast<void *>(global_dlsym);
|
||||
}
|
||||
|
||||
#ifdef __x86_64__
|
||||
__asm__(".text\n"
|
||||
".globl dlsym\n"
|
||||
".type dlsym, @function\n"
|
||||
"dlsym:\n"
|
||||
".cfi_startproc\n"
|
||||
"pushq %rdi\n"
|
||||
".cfi_adjust_cfa_offset 8\n"
|
||||
"pushq %rsi\n"
|
||||
".cfi_adjust_cfa_offset 8\n"
|
||||
"subq $8, %rsp\n"
|
||||
".cfi_adjust_cfa_offset 8\n"
|
||||
"call fakeinput_dlsym_target\n"
|
||||
"addq $8, %rsp\n"
|
||||
".cfi_adjust_cfa_offset -8\n"
|
||||
"popq %rsi\n"
|
||||
".cfi_adjust_cfa_offset -8\n"
|
||||
"popq %rdi\n"
|
||||
".cfi_adjust_cfa_offset -8\n"
|
||||
"jmp *%rax\n"
|
||||
".cfi_endproc\n"
|
||||
".size dlsym, .-dlsym\n");
|
||||
#else
|
||||
__asm__(".text\n"
|
||||
".globl dlsym\n"
|
||||
".type dlsym, @function\n"
|
||||
"dlsym:\n"
|
||||
".cfi_startproc\n"
|
||||
"subl $12, %esp\n"
|
||||
".cfi_adjust_cfa_offset 12\n"
|
||||
"movl 20(%esp), %eax\n"
|
||||
"movl %eax, 4(%esp)\n"
|
||||
"movl 16(%esp), %eax\n"
|
||||
"movl %eax, (%esp)\n"
|
||||
"call fakeinput_dlsym_target\n"
|
||||
"addl $12, %esp\n"
|
||||
".cfi_adjust_cfa_offset -12\n"
|
||||
"jmp *%eax\n"
|
||||
".cfi_endproc\n"
|
||||
".size dlsym, .-dlsym\n");
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
@@ -131,7 +131,7 @@ struct VkTable {
|
||||
#include <string>
|
||||
|
||||
namespace lsfg { class Engine; }
|
||||
namespace winfg { class Engine; }
|
||||
namespace ddfg { class Engine; }
|
||||
|
||||
static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2;
|
||||
|
||||
@@ -280,7 +280,7 @@ public:
|
||||
static constexpr int32_t kFgCaptureGame = -1;
|
||||
void setFrameGenTuning(float flowScale, float refreshHz, int32_t captureHeight = 0);
|
||||
void setFrameGenEngine(int kind);
|
||||
void setWinFgTuning(int model, int perfPreset);
|
||||
void setDdFgTuning(int model, int perfPreset);
|
||||
|
||||
private:
|
||||
struct WinTex {
|
||||
@@ -468,12 +468,12 @@ private:
|
||||
uint64_t fgSourceFrames_ = 0;
|
||||
std::string lsfgCachePath_;
|
||||
bool lsfgEngineTried_ = false;
|
||||
std::unique_ptr<winfg::Engine> winfgEngine_;
|
||||
bool winfgEngineTried_ = false;
|
||||
std::unique_ptr<ddfg::Engine> ddfgEngine_;
|
||||
bool ddfgEngineTried_ = false;
|
||||
std::atomic<int> fgEngineKind_{0};
|
||||
std::atomic<int> fgModel_{4};
|
||||
std::atomic<int> fgPerfPreset_{1};
|
||||
bool ensureWinFgEngine();
|
||||
bool ensureDdFgEngine();
|
||||
bool fgCapsOk() const;
|
||||
|
||||
uint32_t syncSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; }
|
||||
|
||||
File renamed without changes.
File renamed without changes.
+36
-36
@@ -6,7 +6,7 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace winfg {
|
||||
namespace ddfg {
|
||||
|
||||
struct Config {
|
||||
bool enabled = false;
|
||||
@@ -213,48 +213,48 @@ static inline void apply_toml(Config& c, const std::string& path) {
|
||||
}
|
||||
|
||||
static inline std::string conf_path() {
|
||||
if (const char* p = std::getenv("WIN_FG_CONF")) return p;
|
||||
if (const char* p = std::getenv("DD_FG_CONF")) return p;
|
||||
const char* home = std::getenv("HOME");
|
||||
return home ? std::string(home) + "/.config/win-fg/conf.toml" : std::string();
|
||||
return home ? std::string(home) + "/.config/dd-fg/conf.toml" : std::string();
|
||||
}
|
||||
|
||||
static inline std::string capture_root(const Config& c) {
|
||||
if (!c.capDir.empty()) return c.capDir;
|
||||
if (const char* home = std::getenv("HOME")) return std::string(home) + "/.cache/winfg-capture";
|
||||
return std::string("/tmp/winfg-capture");
|
||||
if (const char* home = std::getenv("HOME")) return std::string(home) + "/.cache/ddfg-capture";
|
||||
return std::string("/tmp/ddfg-capture");
|
||||
}
|
||||
|
||||
static inline Config load_config() {
|
||||
Config c;
|
||||
c.enabled = envi("WIN_FG_ENABLE", 0) != 0;
|
||||
if (const char* d = std::getenv("WIN_FG_DEBUG")) c.debug = parse_bool(d, c.debug);
|
||||
if (const char* pc = std::getenv("WIN_FG_PACING")) c.pacing = parse_bool(pc, c.pacing);
|
||||
c.model = envi("WIN_FG_MODEL", c.model);
|
||||
c.multiplier = envi("WIN_FG_MULT", c.multiplier);
|
||||
c.extraImages = envi("WIN_FG_EXTRA_IMAGES", c.extraImages);
|
||||
c.perfPreset = envi("WIN_FG_PERF_PRESET", c.perfPreset);
|
||||
if (const char* g = std::getenv("WIN_FG_GM")) c.gmMode = parse_tristate(g, c.gmMode);
|
||||
if (const char* r = std::getenv("WIN_FG_FLOWREG")) c.frMode = parse_tristate(r, c.frMode);
|
||||
c.frIters = envi("WIN_FG_FR_ITERS", c.frIters);
|
||||
c.frLambda = envf("WIN_FG_FR_LAMBDA", c.frLambda);
|
||||
c.frDt = envf("WIN_FG_FR_DT", c.frDt);
|
||||
c.frEdge = envf("WIN_FG_FR_EDGE", c.frEdge);
|
||||
c.frEps = envf("WIN_FG_FR_EPS", c.frEps);
|
||||
c.flowScale = envf("WIN_FG_FLOWSCALE", c.flowScale);
|
||||
c.beta = envf("WIN_FG_BETA", c.beta);
|
||||
c.lambda = envf("WIN_FG_LAMBDA", c.lambda);
|
||||
c.epsilon = envf("WIN_FG_EPSILON", c.epsilon);
|
||||
c.photoScale = envf("WIN_FG_PHOTOSCALE", c.photoScale);
|
||||
if (const char* cc = std::getenv("WIN_FG_CAPTURE")) c.capture = parse_bool(cc, c.capture);
|
||||
c.captureW = envi("WIN_FG_CAPTURE_W", c.captureW);
|
||||
c.captureH = envi("WIN_FG_CAPTURE_H", c.captureH);
|
||||
if (const char* cm = std::getenv("WIN_FG_CAPTURE_MODE")) c.capMode = parse_capmode(cm, c.capMode);
|
||||
c.capPatches = envi("WIN_FG_CAPTURE_PATCHES", c.capPatches);
|
||||
c.capPatchSize = envi("WIN_FG_CAPTURE_PATCH", c.capPatchSize);
|
||||
c.capMotion = envf("WIN_FG_CAPTURE_MOTION", c.capMotion);
|
||||
c.capShardMB = envi("WIN_FG_CAPTURE_SHARD_MB", c.capShardMB);
|
||||
if (const char* cd = std::getenv("WIN_FG_CAPTURE_DIR")) c.capDir = cd;
|
||||
if (const char* h = std::getenv("WIN_FG_HUD_RECT")) {
|
||||
c.enabled = envi("DD_FG_ENABLE", 0) != 0;
|
||||
if (const char* d = std::getenv("DD_FG_DEBUG")) c.debug = parse_bool(d, c.debug);
|
||||
if (const char* pc = std::getenv("DD_FG_PACING")) c.pacing = parse_bool(pc, c.pacing);
|
||||
c.model = envi("DD_FG_MODEL", c.model);
|
||||
c.multiplier = envi("DD_FG_MULT", c.multiplier);
|
||||
c.extraImages = envi("DD_FG_EXTRA_IMAGES", c.extraImages);
|
||||
c.perfPreset = envi("DD_FG_PERF_PRESET", c.perfPreset);
|
||||
if (const char* g = std::getenv("DD_FG_GM")) c.gmMode = parse_tristate(g, c.gmMode);
|
||||
if (const char* r = std::getenv("DD_FG_FLOWREG")) c.frMode = parse_tristate(r, c.frMode);
|
||||
c.frIters = envi("DD_FG_FR_ITERS", c.frIters);
|
||||
c.frLambda = envf("DD_FG_FR_LAMBDA", c.frLambda);
|
||||
c.frDt = envf("DD_FG_FR_DT", c.frDt);
|
||||
c.frEdge = envf("DD_FG_FR_EDGE", c.frEdge);
|
||||
c.frEps = envf("DD_FG_FR_EPS", c.frEps);
|
||||
c.flowScale = envf("DD_FG_FLOWSCALE", c.flowScale);
|
||||
c.beta = envf("DD_FG_BETA", c.beta);
|
||||
c.lambda = envf("DD_FG_LAMBDA", c.lambda);
|
||||
c.epsilon = envf("DD_FG_EPSILON", c.epsilon);
|
||||
c.photoScale = envf("DD_FG_PHOTOSCALE", c.photoScale);
|
||||
if (const char* cc = std::getenv("DD_FG_CAPTURE")) c.capture = parse_bool(cc, c.capture);
|
||||
c.captureW = envi("DD_FG_CAPTURE_W", c.captureW);
|
||||
c.captureH = envi("DD_FG_CAPTURE_H", c.captureH);
|
||||
if (const char* cm = std::getenv("DD_FG_CAPTURE_MODE")) c.capMode = parse_capmode(cm, c.capMode);
|
||||
c.capPatches = envi("DD_FG_CAPTURE_PATCHES", c.capPatches);
|
||||
c.capPatchSize = envi("DD_FG_CAPTURE_PATCH", c.capPatchSize);
|
||||
c.capMotion = envf("DD_FG_CAPTURE_MOTION", c.capMotion);
|
||||
c.capShardMB = envi("DD_FG_CAPTURE_SHARD_MB", c.capShardMB);
|
||||
if (const char* cd = std::getenv("DD_FG_CAPTURE_DIR")) c.capDir = cd;
|
||||
if (const char* h = std::getenv("DD_FG_HUD_RECT")) {
|
||||
float r[4] = {0,0,0,0}; int n = 0;
|
||||
std::string s = h; size_t start = 0;
|
||||
for (; n < 4; ++n) {
|
||||
@@ -266,8 +266,8 @@ static inline Config load_config() {
|
||||
c.hudX0 = r[0]; c.hudY0 = r[1]; c.hudX1 = r[2]; c.hudY1 = r[3];
|
||||
}
|
||||
const char* home = std::getenv("HOME");
|
||||
if (home) apply_toml(c, std::string(home) + "/.config/win-fg/conf.toml");
|
||||
if (const char* p = std::getenv("WIN_FG_CONF")) apply_toml(c, p);
|
||||
if (home) apply_toml(c, std::string(home) + "/.config/dd-fg/conf.toml");
|
||||
if (const char* p = std::getenv("DD_FG_CONF")) apply_toml(c, p);
|
||||
c.sanitize();
|
||||
return c;
|
||||
}
|
||||
+13
-10
@@ -1,10 +1,10 @@
|
||||
#include "winfg_engine.h"
|
||||
#include "winfg_vkd.h"
|
||||
#include "ddfg_engine.h"
|
||||
#include "ddfg_vkd.h"
|
||||
#include "log.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace winfg {
|
||||
namespace ddfg {
|
||||
namespace {
|
||||
|
||||
float alphaFor(uint32_t k, uint32_t n) {
|
||||
@@ -31,18 +31,18 @@ Engine::~Engine() { destroyRing(); fg_.destroy(); }
|
||||
|
||||
bool Engine::init(const VkTable& table, VkPhysicalDevice phys, VkDevice dev,
|
||||
uint32_t queueFamily, VkQueue queue) {
|
||||
if (!winfgVkdInit(table, dd_, id_)) {
|
||||
WFG_LOGE("native: dispatch incomplete; frame generation unavailable");
|
||||
if (!ddfgVkdInit(table, dd_, id_)) {
|
||||
DDFG_LOGE("native: dispatch incomplete; frame generation unavailable");
|
||||
return false;
|
||||
}
|
||||
phys_ = phys; dev_ = dev; qf_ = queueFamily; queue_ = queue;
|
||||
id_.GetPhysicalDeviceMemoryProperties(phys_, &memProps_);
|
||||
if (!fg_.init(&dd_, &id_, phys_, dev_, qf_, queue_)) {
|
||||
WFG_LOGE("native: FrameGen::init failed");
|
||||
DDFG_LOGE("native: FrameGen::init failed");
|
||||
unavailable_ = true;
|
||||
return false;
|
||||
}
|
||||
WFG_LOGI("native: chain ready (10 pipelines, embedded shaders)");
|
||||
DDFG_LOGI("native: chain ready (10 pipelines, embedded shaders)");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ bool Engine::prepare(uint32_t w, uint32_t h, VkFormat fmt) {
|
||||
|
||||
|
||||
if (!fg_.onResize(VkExtent2D{w, h}, fmt, false)) {
|
||||
WFG_LOGE("native: onResize %ux%u failed; frame generation unavailable", w, h);
|
||||
DDFG_LOGE("native: onResize %ux%u failed; frame generation unavailable", w, h);
|
||||
unavailable_ = true;
|
||||
return false;
|
||||
}
|
||||
@@ -71,11 +71,13 @@ bool Engine::prepare(uint32_t w, uint32_t h, VkFormat fmt) {
|
||||
}
|
||||
builtW_ = w; builtH_ = h; builtFmt_ = fmt;
|
||||
history_ = 0; curr_ = 0; frames_ = 0;
|
||||
WFG_LOGI("native: built %ux%u fmt=%d model=%d preset=%d flow=%.2f",
|
||||
DDFG_LOGI("native: built %ux%u fmt=%d model=%d preset=%d flow=%.2f",
|
||||
w, h, (int)fmt, cfg_.model, cfg_.perfPreset, (double)cfg_.flowScale);
|
||||
return true;
|
||||
}
|
||||
|
||||
static_assert(kMaxGenerations <= (uint32_t)FrameGen::kSynthSlots, "each generation needs its own synth slot");
|
||||
|
||||
uint32_t Engine::plan(uint32_t capacity) const {
|
||||
if (!valid() || history_ < 2) return 0;
|
||||
const uint32_t want = (uint32_t)std::max(cfg_.multiplier - 1, 0);
|
||||
@@ -131,7 +133,8 @@ void Engine::generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t cou
|
||||
dd_.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toWrite);
|
||||
|
||||
fg_.record(cmd, prev.view, curr.view, targetView, alphaFor(generation, count), gmSlot_);
|
||||
fg_.record(cmd, prev.view, curr.view, targetView, alphaFor(generation, count), gmSlot_,
|
||||
generation, generation > 0);
|
||||
if (generation + 1 == count) gmSlot_ = (gmSlot_ + 1) % (uint32_t)FrameGen::kGmSlots;
|
||||
|
||||
(void)w; (void)h;
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
struct VkTable;
|
||||
|
||||
namespace winfg {
|
||||
namespace ddfg {
|
||||
|
||||
constexpr uint32_t kMaxGenerations = 3;
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
#include "winfg_vkd.h"
|
||||
#include "ddfg_vkd.h"
|
||||
#include "../VulkanRendererContext.h"
|
||||
#include <android/log.h>
|
||||
|
||||
bool winfgVkdInit(const VkTable& table, winfg::DeviceDispatch& dd, winfg::InstanceDispatch& id) {
|
||||
bool ddfgVkdInit(const VkTable& table, ddfg::DeviceDispatch& dd, ddfg::InstanceDispatch& id) {
|
||||
bool ok = true;
|
||||
#define COPY(dst, fn) \
|
||||
do { \
|
||||
dst.fn = table.fn; \
|
||||
if (!dst.fn) { \
|
||||
__android_log_print(ANDROID_LOG_ERROR, "WinFgVkd", \
|
||||
__android_log_print(ANDROID_LOG_ERROR, "DdFgVkd", \
|
||||
"entry point vk" #fn " did not resolve"); \
|
||||
ok = false; \
|
||||
} \
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include "vk_dispatch.hpp"
|
||||
struct VkTable;
|
||||
bool ddfgVkdInit(const VkTable& table, ddfg::DeviceDispatch& dd, ddfg::InstanceDispatch& id);
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
namespace winfg { namespace embedded {
|
||||
namespace ddfg { namespace embedded {
|
||||
static const uint32_t OF3_LUMA_SPV[] = {0x07230203u,0x00010300u,0x0008000bu,0x00000054u,0x00000000u,0x00020011u,0x00000001u,0x00020011u,0x00000032u,0x0006000bu,0x00000001u,0x4c534c47u,0x6474732eu,0x3035342eu,0x00000000u,0x0003000eu,0x00000000u,0x00000001u,0x0006000fu,0x00000005u,0x00000004u,0x6e69616du,0x00000000u,0x0000000du,0x00060010u,0x00000004u,0x00000011u,0x00000010u,0x00000010u,0x00000001u,0x00030003u,0x00000002u,0x000001c2u,0x00040005u,0x00000004u,0x6e69616du,0x00000000u,0x00030005u,0x00000009u,0x00646967u,0x00080005u,0x0000000du,0x475f6c67u,0x61626f6cu,0x766e496cu,0x7461636fu,0x496e6f69u,0x00000044u,0x00030005u,0x00000012u,0x006d6964u,0x00040005u,0x00000016u,0x74734475u,0x00000000u,0x00030005u,0x00000030u,0x00007675u,0x00030005u,0x0000003bu,0x00626772u,0x00040005u,0x0000003fu,0x63725375u,0x00000000u,0x00030005u,0x00000047u,0x00005966u,0x00040047u,0x0000000du,0x0000000bu,0x0000001cu,0x00030047u,0x00000016u,0x00000019u,0x00040047u,0x00000016u,0x00000021u,0x00000030u,0x00040047u,0x00000016u,0x00000022u,0x00000000u,0x00040047u,0x0000003fu,0x00000021u,0x00000020u,0x00040047u,0x0000003fu,0x00000022u,0x00000000u,0x00040047u,0x00000053u,0x0000000bu,0x00000019u,0x00020013u,0x00000002u,0x00030021u,0x00000003u,0x00000002u,0x00040015u,0x00000006u,0x00000020u,0x00000001u,0x00040017u,0x00000007u,0x00000006u,0x00000002u,0x00040020u,0x00000008u,0x00000007u,0x00000007u,0x00040015u,0x0000000au,0x00000020u,0x00000000u,0x00040017u,0x0000000bu,0x0000000au,0x00000003u,0x00040020u,0x0000000cu,0x00000001u,0x0000000bu,0x0004003bu,0x0000000cu,0x0000000du,0x00000001u,0x00040017u,0x0000000eu,0x0000000au,0x00000002u,0x00030016u,0x00000013u,0x00000020u,0x00090019u,0x00000014u,0x00000013u,0x00000001u,0x00000000u,0x00000000u,0x00000000u,0x00000002u,0x00000003u,0x00040020u,0x00000015u,0x00000000u,0x00000014u,0x0004003bu,0x00000015u,0x00000016u,0x00000000u,0x00020014u,0x00000019u,0x0004002bu,0x0000000au,0x0000001au,0x00000000u,0x00040020u,0x0000001bu,0x00000007u,0x00000006u,0x0004002bu,0x0000000au,0x00000024u,0x00000001u,0x00040017u,0x0000002eu,0x00000013u,0x00000002u,0x00040020u,0x0000002fu,0x00000007u,0x0000002eu,0x0004002bu,0x00000013u,0x00000033u,0x3f000000u,0x00040017u,0x00000039u,0x00000013u,0x00000003u,0x00040020u,0x0000003au,0x00000007u,0x00000039u,0x00090019u,0x0000003cu,0x00000013u,0x00000001u,0x00000000u,0x00000000u,0x00000000u,0x00000001u,0x00000000u,0x0003001bu,0x0000003du,0x0000003cu,0x00040020u,0x0000003eu,0x00000000u,0x0000003du,0x0004003bu,0x0000003eu,0x0000003fu,0x00000000u,0x0004002bu,0x00000013u,0x00000042u,0x00000000u,0x00040017u,0x00000043u,0x00000013u,0x00000004u,0x00040020u,0x00000046u,0x00000007u,0x00000013u,0x0004002bu,0x00000013u,0x00000049u,0x3e59b3d0u,0x0004002bu,0x00000013u,0x0000004au,0x3f371759u,0x0004002bu,0x00000013u,0x0000004bu,0x3d93dd98u,0x0006002cu,0x00000039u,0x0000004cu,0x00000049u,0x0000004au,0x0000004bu,0x0004002bu,0x0000000au,0x00000052u,0x00000010u,0x0006002cu,0x0000000bu,0x00000053u,0x00000052u,0x00000052u,0x00000024u,0x00050036u,0x00000002u,0x00000004u,0x00000000u,0x00000003u,0x000200f8u,0x00000005u,0x0004003bu,0x00000008u,0x00000009u,0x00000007u,0x0004003bu,0x00000008u,0x00000012u,0x00000007u,0x0004003bu,0x0000002fu,0x00000030u,0x00000007u,0x0004003bu,0x0000003au,0x0000003bu,0x00000007u,0x0004003bu,0x00000046u,0x00000047u,0x00000007u,0x0004003du,0x0000000bu,0x0000000fu,0x0000000du,0x0007004fu,0x0000000eu,0x00000010u,0x0000000fu,0x0000000fu,0x00000000u,0x00000001u,0x0004007cu,0x00000007u,0x00000011u,0x00000010u,0x0003003eu,0x00000009u,0x00000011u,0x0004003du,0x00000014u,0x00000017u,0x00000016u,0x00040068u,0x00000007u,0x00000018u,0x00000017u,0x0003003eu,0x00000012u,0x00000018u,0x00050041u,0x0000001bu,0x0000001cu,0x00000009u,0x0000001au,0x0004003du,0x00000006u,0x0000001du,0x0000001cu,0x00050041u,0x0000001bu,0x0000001eu,0x00000012u,0x0000001au,0x0004003du,0x00000006u,0x0000001fu,0x0000001eu,0x000500afu,0x00000019u,0x00000020u,0x0000001du,0x0000001fu,0x000400a8u,0x00000019u,0x00000021u,0x00000020u,0x000300f7u,0x00000023u,0x00000000u,0x000400fau,0x00000021u,0x00000022u,0x00000023u,0x000200f8u,0x00000022u,0x00050041u,0x0000001bu,0x00000025u,0x00000009u,0x00000024u,0x0004003du,0x00000006u,0x00000026u,0x00000025u,0x00050041u,0x0000001bu,0x00000027u,0x00000012u,0x00000024u,0x0004003du,0x00000006u,0x00000028u,0x00000027u,0x000500afu,0x00000019u,0x00000029u,0x00000026u,0x00000028u,0x000200f9u,0x00000023u,0x000200f8u,0x00000023u,0x000700f5u,0x00000019u,0x0000002au,0x00000020u,0x00000005u,0x00000029u,0x00000022u,0x000300f7u,0x0000002cu,0x00000000u,0x000400fau,0x0000002au,0x0000002bu,0x0000002cu,0x000200f8u,0x0000002bu,0x000100fdu,0x000200f8u,0x0000002cu,0x0004003du,0x00000007u,0x00000031u,0x00000009u,0x0004006fu,0x0000002eu,0x00000032u,0x00000031u,0x00050050u,0x0000002eu,0x00000034u,0x00000033u,0x00000033u,0x00050081u,0x0000002eu,0x00000035u,0x00000032u,0x00000034u,0x0004003du,0x00000007u,0x00000036u,0x00000012u,0x0004006fu,0x0000002eu,0x00000037u,0x00000036u,0x00050088u,0x0000002eu,0x00000038u,0x0Line truncated
|
||||
static const size_t OF3_LUMA_LEN = 481;
|
||||
static const uint32_t OF3_DOWNSAMPLE_SPV[] = {0x07230203u,0x00010300u,0x0008000bu,0x0000008au,0x00000000u,0x00020011u,0x00000001u,0x00020011u,0x00000032u,0x0006000bu,0x00000001u,0x4c534c47u,0x6474732eu,0x3035342eu,0x00000000u,0x0003000eu,0x00000000u,0x00000001u,0x0006000fu,0x00000005u,0x00000004u,0x6e69616du,0x00000000u,0x0000000du,0x00060010u,0x00000004u,0x00000011u,0x00000010u,0x00000010u,0x00000001u,0x00030003u,0x00000002u,0x000001c2u,0x00040005u,0x00000004u,0x6e69616du,0x00000000u,0x00030005u,0x00000009u,0x00646967u,0x00080005u,0x0000000du,0x475f6c67u,0x61626f6cu,0x766e496cu,0x7461636fu,0x496e6f69u,0x00000044u,0x00030005u,0x00000012u,0x006d6964u,0x00040005u,0x00000016u,0x74734475u,0x00000000u,0x00040005u,0x00000030u,0x44637273u,0x00006d69u,0x00040005u,0x00000034u,0x63725375u,0x00000000u,0x00040005u,0x0000003au,0x65736162u,0x00000000u,0x00030005u,0x00000044u,0x00000068u,0x00030005u,0x0000004au,0x00000061u,0x00030005u,0x00000059u,0x00000062u,0x00030005u,0x00000065u,0x00000063u,0x00030005u,0x00000071u,0x00000064u,0x00040047u,0x0000000du,0x0000000bu,0x0000001cu,0x00030047u,0x00000016u,0x00000019u,0x00040047u,0x00000016u,0x00000021u,0x00000030u,0x00040047u,0x00000016u,0x00000022u,0x00000000u,0x00040047u,0x00000034u,0x00000021u,0x00000020u,0x00040047u,0x00000034u,0x00000022u,0x00000000u,0x00040047u,0x00000089u,0x0000000bu,0x00000019u,0x00020013u,0x00000002u,0x00030021u,0x00000003u,0x00000002u,0x00040015u,0x00000006u,0x00000020u,0x00000001u,0x00040017u,0x00000007u,0x00000006u,0x00000002u,0x00040020u,0x00000008u,0x00000007u,0x00000007u,0x00040015u,0x0000000au,0x00000020u,0x00000000u,0x00040017u,0x0000000bu,0x0000000au,0x00000003u,0x00040020u,0x0000000cu,0x00000001u,0x0000000bu,0x0004003bu,0x0000000cu,0x0000000du,0x00000001u,0x00040017u,0x0000000eu,0x0000000au,0x00000002u,0x00030016u,0x00000013u,0x00000020u,0x00090019u,0x00000014u,0x00000013u,0x00000001u,0x00000000u,0x00000000u,0x00000000u,0x00000002u,0x00000003u,0x00040020u,0x00000015u,0x00000000u,0x00000014u,0x0004003bu,0x00000015u,0x00000016u,0x00000000u,0x00020014u,0x00000019u,0x0004002bu,0x0000000au,0x0000001au,0x00000000u,0x00040020u,0x0000001bu,0x00000007u,0x00000006u,0x0004002bu,0x0000000au,0x00000024u,0x00000001u,0x00040017u,0x0000002eu,0x00000013u,0x00000002u,0x00040020u,0x0000002fu,0x00000007u,0x0000002eu,0x00090019u,0x00000031u,0x00000013u,0x00000001u,0x00000000u,0x00000000u,0x00000000u,0x00000001u,0x00000000u,0x0003001bu,0x00000032u,0x00000031u,0x00040020u,0x00000033u,0x00000000u,0x00000032u,0x0004003bu,0x00000033u,0x00000034u,0x00000000u,0x0004002bu,0x00000006u,0x00000036u,0x00000000u,0x0004002bu,0x00000013u,0x0000003du,0x40000000u,0x0004002bu,0x00000013u,0x0000003fu,0x3f800000u,0x0004002bu,0x00000013u,0x00000045u,0x3f000000u,0x00040020u,0x00000049u,0x00000007u,0x00000013u,0x0004002bu,0x00000013u,0x00000055u,0x00000000u,0x00040017u,0x00000056u,0x00000013u,0x00000004u,0x0004002bu,0x00000013u,0x00000085u,0x3e800000u,0x0004002bu,0x0000000au,0x00000088u,0x00000010u,0x0006002cu,0x0000000bu,0x00000089u,0x00000088u,0x00000088u,0x00000024u,0x00050036u,0x00000002u,0x00000004u,0x00000000u,0x00000003u,0x000200f8u,0x00000005u,0x0004003bu,0x00000008u,0x00000009u,0x00000007u,0x0004003bu,0x00000008u,0x00000012u,0x00000007u,0x0004003bu,0x0000002fu,0x00000030u,0x00000007u,0x0004003bu,0x0000002fu,0x0000003au,0x00000007u,0x0004003bu,0x0000002fu,0x00000044u,0x00000007u,0x0004003bu,0x00000049u,0x0000004au,0x00000007u,0x0004003bu,0x00000049u,0x00000059u,0x00000007u,0x0004003bu,0x00000049u,0x00000065u,0x00000007u,0x0004003bu,0x00000049u,0x00000071u,0x00000007u,0x0004003du,0x0000000bu,0x0000000fu,0x0000000du,0x0007004fu,0x0000000eu,0x00000010u,0x0000000fu,0x0000000fu,0x00000000u,0x00000001u,0x0004007cu,0x00000007u,0x00000011u,0x00000010u,0x0003003eu,0x00000009u,0x00000011u,0x0004003du,0x00000014u,0x00000017u,0x00000016u,0x00040068u,0x00000007u,0x00000018u,0x00000017u,0x0003003eu,0x00000012u,0x00000018u,0x00050041u,0x0000001bu,0x0000001cu,0x00000009u,0x0000001au,0x0004003du,0x00000006u,0x0000001du,0x0000001cu,0x00050041u,0x0000001bu,0x0000001eu,0x00000012u,0x0000001au,0x0004003du,0x00000006u,0x0000001fu,0x0000001eu,0x000500afu,0x00000019u,0x00000020u,0x0000001du,0x0000001fu,0x000400a8u,0x00000019u,0x00000021u,0x00000020u,0x000300f7u,0x00000023u,0x00000000u,0x000400fau,0x00000021u,0x00000022u,0x00000023u,0x000200f8u,0x00000022u,0x00050041u,0x0000001bu,0x00000025u,0x00000009u,0x00000024u,0x0004003du,0x00000006u,0x00000026u,0x00000025u,0x00050041u,0x0000001bu,0x00000027u,0x00000012u,0x00000024u,0x0004003du,0x00000006u,0x00000028u,0x00000027u,0x000500afu,0x00000019u,0x00000029u,0x00000026u,0x00000028u,0x000200f9u,0x00000023u,0x000200f8u,0x00000023u,0x000700f5u,0x00000019u,0x0000002au,0x00000020u,0x00000005u,0x00000029u,0x00000022u,0x000300f7u,0x0000002cu,0x00000000u,0x000400fau,0x0000002au,0x0000002bu,0x0000002cu,0x000200f8u,0x0000002bu,0x000100fdu,0x000200f8u,0x0000002cu,0x0004003du,0x00000032u,0x00000035u,0x00000034u,0x00040064u,0x00000031u,0x00000037u,0x00000035u,0x0005006Line truncated
|
||||
@@ -20,10 +20,10 @@ static const uint32_t OF3_GM_PREWARP_SPV[] = {0x07230203u,0x00010300u,0x0008000b
|
||||
static const size_t OF3_GM_PREWARP_LEN = 706;
|
||||
static const uint32_t OF3_FLOWREG_SPV[] = {0x07230203u,0x00010300u,0x0008000bu,0x00000144u,0x00000000u,0x00020011u,0x00000001u,0x00020011u,0x00000032u,0x0006000bu,0x00000001u,0x4c534c47u,0x6474732eu,0x3035342eu,0x00000000u,0x0003000eu,0x00000000u,0x00000001u,0x0006000fu,0x00000005u,0x00000004u,0x6e69616du,0x00000000u,0x00000037u,0x00060010u,0x00000004u,0x00000011u,0x00000010u,0x00000010u,0x00000001u,0x00030003u,0x00000002u,0x000001c2u,0x00040005u,0x00000004u,0x6e69616du,0x00000000u,0x00060005u,0x0000000eu,0x63746566u,0x76285568u,0x763b3269u,0x003b3269u,0x00030005u,0x0000000cu,0x00000070u,0x00030005u,0x0000000du,0x006d6964u,0x00060005u,0x00000013u,0x63746566u,0x76284c68u,0x763b3269u,0x003b3269u,0x00030005u,0x00000011u,0x00000070u,0x00030005u,0x00000012u,0x006d6964u,0x00040005u,0x00000018u,0x6f6c4675u,0x006e4977u,0x00040005u,0x00000026u,0x6d754c75u,0x00000061u,0x00030005u,0x00000034u,0x00646967u,0x00080005u,0x00000037u,0x475f6c67u,0x61626f6cu,0x766e496cu,0x7461636fu,0x496e6f69u,0x00000044u,0x00030005u,0x0000003cu,0x006d6964u,0x00030005u,0x00000055u,0x00007464u,0x00030005u,0x00000056u,0x00000055u,0x00050006u,0x00000056u,0x00000000u,0x61726170u,0x0000736du,0x00030005u,0x00000058u,0x00000000u,0x00040005u,0x0000005cu,0x626d616cu,0x00006164u,0x00050005u,0x0000005fu,0x65676465u,0x68706c41u,0x00000061u,0x00040005u,0x00000063u,0x54737065u,0x00000056u,0x00030005u,0x00000068u,0x00004375u,0x00040005u,0x00000069u,0x61726170u,0x0000006du,0x00040005u,0x0000006bu,0x61726170u,0x0000006du,0x00030005u,0x0000006eu,0x00004c75u,0x00040005u,0x00000073u,0x61726170u,0x0000006du,0x00040005u,0x00000074u,0x61726170u,0x0000006du,0x00030005u,0x00000077u,0x00005275u,0x00040005u,0x0000007bu,0x61726170u,0x0000006du,0x00040005u,0x0000007cu,0x61726170u,0x0000006du,0x00030005u,0x0000007fu,0x00005575u,0x00040005u,0x00000083u,0x61726170u,0x0000006du,0x00040005u,0x00000084u,0x61726170u,0x0000006du,0x00030005u,0x00000087u,0x00004475u,0x00040005u,0x0000008bu,0x61726170u,0x0000006du,0x00040005u,0x0000008cu,0x61726170u,0x0000006du,0x00040005u,0x0000008fu,0x6d69646cu,0x00000000u,0x00030005u,0x00000093u,0x00004c6cu,0x00040005u,0x00000096u,0x61726170u,0x0000006du,0x00040005u,0x00000097u,0x61726170u,0x0000006du,0x00030005u,0x0000009au,0x0000526cu,0x00040005u,0x0000009du,0x61726170u,0x0000006du,0x00040005u,0x0000009eu,0x61726170u,0x0000006du,0x00030005u,0x000000a1u,0x0000556cu,0x00040005u,0x000000a4u,0x61726170u,0x0000006du,0x00040005u,0x000000a5u,0x61726170u,0x0000006du,0x00030005u,0x000000a8u,0x0000446cu,0x00040005u,0x000000abu,0x61726170u,0x0000006du,0x00040005u,0x000000acu,0x61726170u,0x0000006du,0x00040005u,0x000000afu,0x67616d67u,0x00000000u,0x00030005u,0x000000bbu,0x00000067u,0x00030005u,0x000000c4u,0x00004c64u,0x00030005u,0x000000c8u,0x00005264u,0x00030005u,0x000000ccu,0x00005564u,0x00030005u,0x000000d0u,0x00004464u,0x00030005u,0x000000d4u,0x00003265u,0x00030005u,0x000000d8u,0x00004c77u,0x00030005u,0x000000e1u,0x00005277u,0x00030005u,0x000000eau,0x00005577u,0x00030005u,0x000000f3u,0x00004477u,0x00040005u,0x000000fcu,0x576d7573u,0x00000000u,0x00040005u,0x00000104u,0x576d7573u,0x00000055u,0x00040005u,0x00000114u,0x66694475u,0x00000066u,0x00030005u,0x00000120u,0x00003066u,0x00050005u,0x00000121u,0x6f6c4675u,0x73624f77u,0x00000000u,0x00040005u,0x0000012au,0x74656874u,0x00000061u,0x00030005u,0x0000012eu,0x00000072u,0x00040005u,0x00000132u,0x77654e75u,0x00000000u,0x00050005u,0x0000013eu,0x6f6c4675u,0x74754f77u,0x00000000u,0x00040047u,0x00000018u,0x00000021u,0x00000020u,0x00040047u,0x00000018u,0x00000022u,0x00000000u,0x00040047u,0x00000026u,0x00000021u,0x00000022u,0x00040047u,0x00000026u,0x00000022u,0x00000000u,0x00040047u,0x00000037u,0x0000000bu,0x0000001cu,0x00030047u,0x00000056u,0x00000002u,0x00050048u,0x00000056u,0x00000000u,0x00000023u,0x00000000u,0x00040047u,0x00000058u,0x00000021u,0x00000000u,0x00040047u,0x00000058u,0x00000022u,0x00000000u,0x00040047u,0x00000121u,0x00000021u,0x00000021u,0x00040047u,0x00000121u,0x00000022u,0x00000000u,0x00030047u,0x0000013eu,0x00000019u,0x00040047u,0x0000013eu,0x00000021u,0x00000030u,0x00040047u,0x0000013eu,0x00000022u,0x00000000u,0x00040047u,0x00000143u,0x0000000bu,0x00000019u,0x00020013u,0x00000002u,0x00030021u,0x00000003u,0x00000002u,0x00040015u,0x00000006u,0x00000020u,0x00000001u,0x00040017u,0x00000007u,0x00000006u,0x00000002u,0x00040020u,0x00000008u,0x00000007u,0x00000007u,0x00030016u,0x00000009u,0x00000020u,0x00040017u,0x0000000au,0x00000009u,0x00000004u,0x00050021u,0x0000000bu,0x0000000au,0x00000008u,0x00000008u,0x00050021u,0x00000010u,0x00000009u,0x00000008u,0x00000008u,0x00090019u,0x00000015u,0x00000009u,0x00000001u,0x00000000u,0x00000000u,0x00000000u,0x00000001u,0x00000000u,0x0003001bu,0x00000016u,0x00000015u,0x00040020u,0x00000017u,0x00000000u,0x00000016u,0x0004003bu,0x00000017u,0x00000018u,0x00000000u,0x0004002bu,0x00000006u,0x0000001bu,0x00000000u,0x0005002cu,0x00000007u,0x0000001cu,0x0000001bu,0x0000001bu,0x0004002bu,0x00000006u,0x0000001eu,0x00000001u,0x0004003bu,0x00000017u,0x00000026u,0x00000000u,Line truncated
|
||||
static const size_t OF3_FLOWREG_LEN = 1936;
|
||||
static const uint32_t WFG_SYNTH_SPV[] = {0x07230203u,0x00010300u,0x0008000bu,0x00000208u,0x00000000u,0x00020011u,0x00000001u,0x00020011u,0x00000032u,0x0006000bu,0x00000001u,0x4c534c47u,0x6474732eu,0x3035342eu,0x00000000u,0x0003000eu,0x00000000u,0x00000001u,0x0006000fu,0x00000005u,0x00000004u,0x6e69616du,0x00000000u,0x0000001cu,0x00060010u,0x00000004u,0x00000011u,0x00000010u,0x00000010u,0x00000001u,0x00030003u,0x00000002u,0x000001c2u,0x00040005u,0x00000004u,0x6e69616du,0x00000000u,0x00050005u,0x0000000bu,0x616d756cu,0x33667628u,0x0000003bu,0x00030005u,0x0000000au,0x00000063u,0x00030005u,0x00000018u,0x00646967u,0x00080005u,0x0000001cu,0x475f6c67u,0x61626f6cu,0x766e496cu,0x7461636fu,0x496e6f69u,0x00000044u,0x00030005u,0x00000021u,0x006d6964u,0x00040005u,0x00000024u,0x74754f75u,0x00000000u,0x00040005u,0x0000003eu,0x44766e69u,0x00006d69u,0x00030005u,0x00000044u,0x00007675u,0x00030005u,0x0000004du,0x00000055u,0x00060006u,0x0000004du,0x00000000u,0x776f6c66u,0x6c616353u,0x00000065u,0x00050006u,0x0000004du,0x00000001u,0x68706c61u,0x00000061u,0x00050006u,0x0000004du,0x00000002u,0x61746562u,0x00000000u,0x00050006u,0x0000004du,0x00000003u,0x626d616cu,0x00006164u,0x00050006u,0x0000004du,0x00000004u,0x69737065u,0x006e6f6cu,0x00060006u,0x0000004du,0x00000005u,0x746f6870u,0x6163536fu,0x0000656cu,0x00050006u,0x0000004du,0x00000006u,0x30646170u,0x00000000u,0x00050006u,0x0000004du,0x00000007u,0x31646170u,0x00000000u,0x00050006u,0x0000004du,0x00000008u,0x52647568u,0x00746365u,0x00030005u,0x0000004fu,0x00000000u,0x00040005u,0x0000008cu,0x72754375u,0x00000072u,0x00030005u,0x00000098u,0x00676264u,0x00030005u,0x0000009fu,0x00007366u,0x00040005u,0x000000a9u,0x64774273u,0x00000000u,0x00050005u,0x000000aau,0x6f6c4675u,0x64774277u,0x00000000u,0x00040005u,0x000000aeu,0x64774673u,0x00000000u,0x00050005u,0x000000afu,0x6f6c4675u,0x64774677u,0x00000000u,0x00030005u,0x000000b3u,0x00313046u,0x00030005u,0x000000bau,0x00303146u,0x00040005u,0x000000c1u,0x31306663u,0x00000000u,0x00040005u,0x000000c5u,0x30316663u,0x00000000u,0x00030005u,0x000000c9u,0x0031306du,0x00030005u,0x000000d5u,0x0030316du,0x00030005u,0x000000e0u,0x00004170u,0x00030005u,0x000000e8u,0x00004270u,0x00030005u,0x000000f0u,0x00003043u,0x00040005u,0x000000f1u,0x65725075u,0x00000076u,0x00030005u,0x000000f6u,0x00003143u,0x00030005u,0x000000fbu,0x00004165u,0x00040005u,0x000000fcu,0x61726170u,0x0000006du,0x00040005u,0x0000010cu,0x61726170u,0x0000006du,0x00030005u,0x00000111u,0x00004265u,0x00040005u,0x00000112u,0x61726170u,0x0000006du,0x00040005u,0x00000122u,0x61726170u,0x0000006du,0x00040005u,0x00000127u,0x76556266u,0x00000000u,0x00040005u,0x0000012bu,0x65526266u,0x00646973u,0x00040005u,0x00000138u,0x736e6f63u,0x00747369u,0x00030005u,0x00000149u,0x0000305au,0x00030005u,0x00000157u,0x0000315au,0x00040005u,0x00000163u,0x30756174u,0x00000000u,0x00040005u,0x00000167u,0x31756174u,0x00000000u,0x00040005u,0x0000016au,0x78616d7au,0x00000000u,0x00030005u,0x0000016eu,0x00003057u,0x00030005u,0x00000179u,0x00003157u,0x00040005u,0x00000183u,0x6d757357u,0x00000000u,0x00040005u,0x00000187u,0x70726177u,0x00006465u,0x00050005u,0x000001a0u,0x736f7263u,0x64616673u,0x00000065u,0x00050005u,0x000001adu,0x746f6870u,0x7272456fu,0x00000000u,0x00050005u,0x000001b1u,0x746f6870u,0x7572546fu,0x00007473u,0x00040005u,0x000001b7u,0x73757274u,0x00000074u,0x00040005u,0x000001c3u,0x76657270u,0x00000043u,0x00040005u,0x000001c8u,0x72727563u,0x00000043u,0x00050005u,0x000001cdu,0x746e6f63u,0x44746e65u,0x00666669u,0x00050005u,0x000001d2u,0x72746e75u,0x65747375u,0x00000064u,0x00040005u,0x000001d7u,0x44676962u,0x00666669u,0x00050005u,0x000001dcu,0x70616e73u,0x75436f54u,0x00007272u,0x00040005u,0x000001e5u,0x6374756fu,0x00000000u,0x00030005u,0x000001eeu,0x0067616du,0x00030005u,0x000001f2u,0x0000006eu,0x00030005u,0x000001f6u,0x00746173u,0x00040047u,0x0000001cu,0x0000000bu,0x0000001cu,0x00030047u,0x00000024u,0x00000019u,0x00040047u,0x00000024u,0x00000021u,0x00000030u,0x00040047u,0x00000024u,0x00000022u,0x00000000u,0x00030047u,0x0000004du,0x00000002u,0x00050048u,0x0000004du,0x00000000u,0x00000023u,0x00000000u,0x00050048u,0x0000004du,0x00000001u,0x00000023u,0x00000004u,0x00050048u,0x0000004du,0x00000002u,0x00000023u,0x00000008u,0x00050048u,0x0000004du,0x00000003u,0x00000023u,0x0000000cu,0x00050048u,0x0000004du,0x00000004u,0x00000023u,0x00000010u,0x00050048u,0x0000004du,0x00000005u,0x00000023u,0x00000014u,0x00050048u,0x0000004du,0x00000006u,0x00000023u,0x00000018u,0x00050048u,0x0000004du,0x00000007u,0x00000023u,0x0000001cu,0x00050048u,0x0000004du,0x00000008u,0x00000023u,0x00000020u,0x00040047u,0x0000004fu,0x00000021u,0x00000000u,0x00040047u,0x0000004fu,0x00000022u,0x00000000u,0x00040047u,0x0000008cu,0x00000021u,0x00000021u,0x00040047u,0x0000008cu,0x00000022u,0x00000000u,0x00040047u,0x000000aau,0x00000021u,0x00000023u,0x00040047u,0x000000aau,0x00000022u,0x00000000u,0x00040047u,0x000000afu,0x00000021u,0x00000022u,0x00040047u,0x000000afu,0x00000022u,0x00000000u,0x00040047u,0x000000f1u,0x00000021u,0x00000020u,0x00040047u,0x000000f1u,0xLine truncated
|
||||
static const size_t WFG_SYNTH_LEN = 3082;
|
||||
static const uint32_t DDFG_SYNTH_SPV[] = {0x07230203u,0x00010300u,0x0008000bu,0x00000208u,0x00000000u,0x00020011u,0x00000001u,0x00020011u,0x00000032u,0x0006000bu,0x00000001u,0x4c534c47u,0x6474732eu,0x3035342eu,0x00000000u,0x0003000eu,0x00000000u,0x00000001u,0x0006000fu,0x00000005u,0x00000004u,0x6e69616du,0x00000000u,0x0000001cu,0x00060010u,0x00000004u,0x00000011u,0x00000010u,0x00000010u,0x00000001u,0x00030003u,0x00000002u,0x000001c2u,0x00040005u,0x00000004u,0x6e69616du,0x00000000u,0x00050005u,0x0000000bu,0x616d756cu,0x33667628u,0x0000003bu,0x00030005u,0x0000000au,0x00000063u,0x00030005u,0x00000018u,0x00646967u,0x00080005u,0x0000001cu,0x475f6c67u,0x61626f6cu,0x766e496cu,0x7461636fu,0x496e6f69u,0x00000044u,0x00030005u,0x00000021u,0x006d6964u,0x00040005u,0x00000024u,0x74754f75u,0x00000000u,0x00040005u,0x0000003eu,0x44766e69u,0x00006d69u,0x00030005u,0x00000044u,0x00007675u,0x00030005u,0x0000004du,0x00000055u,0x00060006u,0x0000004du,0x00000000u,0x776f6c66u,0x6c616353u,0x00000065u,0x00050006u,0x0000004du,0x00000001u,0x68706c61u,0x00000061u,0x00050006u,0x0000004du,0x00000002u,0x61746562u,0x00000000u,0x00050006u,0x0000004du,0x00000003u,0x626d616cu,0x00006164u,0x00050006u,0x0000004du,0x00000004u,0x69737065u,0x006e6f6cu,0x00060006u,0x0000004du,0x00000005u,0x746f6870u,0x6163536fu,0x0000656cu,0x00050006u,0x0000004du,0x00000006u,0x30646170u,0x00000000u,0x00050006u,0x0000004du,0x00000007u,0x31646170u,0x00000000u,0x00050006u,0x0000004du,0x00000008u,0x52647568u,0x00746365u,0x00030005u,0x0000004fu,0x00000000u,0x00040005u,0x0000008cu,0x72754375u,0x00000072u,0x00030005u,0x00000098u,0x00676264u,0x00030005u,0x0000009fu,0x00007366u,0x00040005u,0x000000a9u,0x64774273u,0x00000000u,0x00050005u,0x000000aau,0x6f6c4675u,0x64774277u,0x00000000u,0x00040005u,0x000000aeu,0x64774673u,0x00000000u,0x00050005u,0x000000afu,0x6f6c4675u,0x64774677u,0x00000000u,0x00030005u,0x000000b3u,0x00313046u,0x00030005u,0x000000bau,0x00303146u,0x00040005u,0x000000c1u,0x31306663u,0x00000000u,0x00040005u,0x000000c5u,0x30316663u,0x00000000u,0x00030005u,0x000000c9u,0x0031306du,0x00030005u,0x000000d5u,0x0030316du,0x00030005u,0x000000e0u,0x00004170u,0x00030005u,0x000000e8u,0x00004270u,0x00030005u,0x000000f0u,0x00003043u,0x00040005u,0x000000f1u,0x65725075u,0x00000076u,0x00030005u,0x000000f6u,0x00003143u,0x00030005u,0x000000fbu,0x00004165u,0x00040005u,0x000000fcu,0x61726170u,0x0000006du,0x00040005u,0x0000010cu,0x61726170u,0x0000006du,0x00030005u,0x00000111u,0x00004265u,0x00040005u,0x00000112u,0x61726170u,0x0000006du,0x00040005u,0x00000122u,0x61726170u,0x0000006du,0x00040005u,0x00000127u,0x76556266u,0x00000000u,0x00040005u,0x0000012bu,0x65526266u,0x00646973u,0x00040005u,0x00000138u,0x736e6f63u,0x00747369u,0x00030005u,0x00000149u,0x0000305au,0x00030005u,0x00000157u,0x0000315au,0x00040005u,0x00000163u,0x30756174u,0x00000000u,0x00040005u,0x00000167u,0x31756174u,0x00000000u,0x00040005u,0x0000016au,0x78616d7au,0x00000000u,0x00030005u,0x0000016eu,0x00003057u,0x00030005u,0x00000179u,0x00003157u,0x00040005u,0x00000183u,0x6d757357u,0x00000000u,0x00040005u,0x00000187u,0x70726177u,0x00006465u,0x00050005u,0x000001a0u,0x736f7263u,0x64616673u,0x00000065u,0x00050005u,0x000001adu,0x746f6870u,0x7272456fu,0x00000000u,0x00050005u,0x000001b1u,0x746f6870u,0x7572546fu,0x00007473u,0x00040005u,0x000001b7u,0x73757274u,0x00000074u,0x00040005u,0x000001c3u,0x76657270u,0x00000043u,0x00040005u,0x000001c8u,0x72727563u,0x00000043u,0x00050005u,0x000001cdu,0x746e6f63u,0x44746e65u,0x00666669u,0x00050005u,0x000001d2u,0x72746e75u,0x65747375u,0x00000064u,0x00040005u,0x000001d7u,0x44676962u,0x00666669u,0x00050005u,0x000001dcu,0x70616e73u,0x75436f54u,0x00007272u,0x00040005u,0x000001e5u,0x6374756fu,0x00000000u,0x00030005u,0x000001eeu,0x0067616du,0x00030005u,0x000001f2u,0x0000006eu,0x00030005u,0x000001f6u,0x00746173u,0x00040047u,0x0000001cu,0x0000000bu,0x0000001cu,0x00030047u,0x00000024u,0x00000019u,0x00040047u,0x00000024u,0x00000021u,0x00000030u,0x00040047u,0x00000024u,0x00000022u,0x00000000u,0x00030047u,0x0000004du,0x00000002u,0x00050048u,0x0000004du,0x00000000u,0x00000023u,0x00000000u,0x00050048u,0x0000004du,0x00000001u,0x00000023u,0x00000004u,0x00050048u,0x0000004du,0x00000002u,0x00000023u,0x00000008u,0x00050048u,0x0000004du,0x00000003u,0x00000023u,0x0000000cu,0x00050048u,0x0000004du,0x00000004u,0x00000023u,0x00000010u,0x00050048u,0x0000004du,0x00000005u,0x00000023u,0x00000014u,0x00050048u,0x0000004du,0x00000006u,0x00000023u,0x00000018u,0x00050048u,0x0000004du,0x00000007u,0x00000023u,0x0000001cu,0x00050048u,0x0000004du,0x00000008u,0x00000023u,0x00000020u,0x00040047u,0x0000004fu,0x00000021u,0x00000000u,0x00040047u,0x0000004fu,0x00000022u,0x00000000u,0x00040047u,0x0000008cu,0x00000021u,0x00000021u,0x00040047u,0x0000008cu,0x00000022u,0x00000000u,0x00040047u,0x000000aau,0x00000021u,0x00000023u,0x00040047u,0x000000aau,0x00000022u,0x00000000u,0x00040047u,0x000000afu,0x00000021u,0x00000022u,0x00040047u,0x000000afu,0x00000022u,0x00000000u,0x00040047u,0x000000f1u,0x00000021u,0x00000020u,0x00040047u,0x000000f1u,0Line truncated
|
||||
static const size_t DDFG_SYNTH_LEN = 3082;
|
||||
struct Blob { const uint32_t* code; size_t words; };
|
||||
enum Shader { OF3_LUMA, OF3_DOWNSAMPLE, OF3_FLOW, OF3_EXPAND, OF3_FLOW_M4, OF3_EXPAND_M4, OF3_GM_REDUCE, OF3_GM_PREWARP, OF3_FLOWREG, WFG_SYNTH, SHADER_COUNT };
|
||||
enum Shader { OF3_LUMA, OF3_DOWNSAMPLE, OF3_FLOW, OF3_EXPAND, OF3_FLOW_M4, OF3_EXPAND_M4, OF3_GM_REDUCE, OF3_GM_PREWARP, OF3_FLOWREG, DDFG_SYNTH, SHADER_COUNT };
|
||||
static inline Blob blob(Shader s) {
|
||||
switch (s) {
|
||||
case OF3_LUMA: return { OF3_LUMA_SPV, OF3_LUMA_LEN };
|
||||
@@ -35,7 +35,7 @@ static inline Blob blob(Shader s) {
|
||||
case OF3_GM_REDUCE: return { OF3_GM_REDUCE_SPV, OF3_GM_REDUCE_LEN };
|
||||
case OF3_GM_PREWARP: return { OF3_GM_PREWARP_SPV, OF3_GM_PREWARP_LEN };
|
||||
case OF3_FLOWREG: return { OF3_FLOWREG_SPV, OF3_FLOWREG_LEN };
|
||||
case WFG_SYNTH: return { WFG_SYNTH_SPV, WFG_SYNTH_LEN };
|
||||
case DDFG_SYNTH: return { DDFG_SYNTH_SPV, DDFG_SYNTH_LEN };
|
||||
default: return { nullptr, 0 };
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -4,9 +4,9 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
namespace winfg {
|
||||
namespace ddfg {
|
||||
|
||||
#define VKOK(x) do { if ((x) != VK_SUCCESS) { WFG_LOGE("vk fail: %s", #x); return false; } } while (0)
|
||||
#define VKOK(x) do { if ((x) != VK_SUCCESS) { DDFG_LOGE("vk fail: %s", #x); return false; } } while (0)
|
||||
|
||||
uint32_t FrameGen::findMemType(uint32_t bits, VkMemoryPropertyFlags props) const {
|
||||
for (uint32_t i = 0; i < memProps_.memoryTypeCount; ++i)
|
||||
@@ -153,9 +153,9 @@ bool FrameGen::init(const DeviceDispatch* dd, const InstanceDispatch* id,
|
||||
if (!makePipe(pGmReduce_, embedded::OF3_GM_REDUCE, unused)) return false;
|
||||
if (!makePipe(pGmPrewarp_, embedded::OF3_GM_PREWARP, unused)) return false;
|
||||
if (!makePipe(pFlowReg_, embedded::OF3_FLOWREG, unused)) return false;
|
||||
if (!makePipe(pSynth_, embedded::WFG_SYNTH, unused)) return false;
|
||||
if (!makeGmBuffers()) { WFG_LOGE("framegen: GM SSBO alloc failed - C1 disabled"); }
|
||||
WFG_LOGI("framegen init ok (queueFamily=%u, 10 pipelines, model=%d, C1 gm=%d, C2 flow_reg=%d iters=%d)",
|
||||
if (!makePipe(pSynth_, embedded::DDFG_SYNTH, unused)) return false;
|
||||
if (!makeGmBuffers()) { DDFG_LOGE("framegen: GM SSBO alloc failed - C1 disabled"); }
|
||||
DDFG_LOGI("framegen init ok (queueFamily=%u, 10 pipelines, model=%d, C1 gm=%d, C2 flow_reg=%d iters=%d)",
|
||||
queueFamily_, cfg_.model, cfg_.gmMode, cfg_.frMode, cfg_.frIters);
|
||||
return true;
|
||||
}
|
||||
@@ -194,7 +194,7 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat , bool force) {
|
||||
if (!makeImage(flowRegA_, levelExtent(extent, flowFinest_), VK_FORMAT_R16G16B16A16_SFLOAT, regUsage)) return false;
|
||||
if (!makeImage(flowRegB_, levelExtent(extent, flowFinest_), VK_FORMAT_R16G16B16A16_SFLOAT, regUsage)) return false;
|
||||
ready_ = true;
|
||||
WFG_LOGI("framegen resized to %ux%u (%d pyramid levels, kFlowFinest=%d perf_preset=%d)",
|
||||
DDFG_LOGI("framegen resized to %ux%u (%d pyramid levels, kFlowFinest=%d perf_preset=%d)",
|
||||
extent.width, extent.height, kLevels, flowFinest_, cfg_.perfPreset);
|
||||
return true;
|
||||
}
|
||||
+4
-2
@@ -3,7 +3,7 @@
|
||||
#include "config.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace winfg {
|
||||
namespace ddfg {
|
||||
|
||||
class FrameGen {
|
||||
public:
|
||||
@@ -12,7 +12,8 @@ public:
|
||||
void configure(const Config& c);
|
||||
bool onResize(VkExtent2D extent, VkFormat colorFormat, bool force = false);
|
||||
void record(VkCommandBuffer cmd, VkImageView prevView, VkImageView currView,
|
||||
VkImageView outView, float alpha, uint32_t gmSlot = 0);
|
||||
VkImageView outView, float alpha, uint32_t gmSlot = 0,
|
||||
uint32_t synthSlot = 0, bool reuseFlow = false);
|
||||
void destroy();
|
||||
bool valid() const { return device_ != VK_NULL_HANDLE && ready_; }
|
||||
const Config& config() const { return cfg_; }
|
||||
@@ -25,6 +26,7 @@ public:
|
||||
|
||||
|
||||
static constexpr int kGmSlots = 3;
|
||||
static constexpr int kSynthSlots = 3;
|
||||
static constexpr int kGmLevel = 3;
|
||||
static constexpr int kGmThreads = 512;
|
||||
static constexpr int kGmStride = 32;
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#if defined(__ANDROID__)
|
||||
#include <android/log.h>
|
||||
#define DDFG_LOGI(...) __android_log_print(ANDROID_LOG_INFO, "dd-fg", __VA_ARGS__)
|
||||
#define DDFG_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "dd-fg", __VA_ARGS__)
|
||||
#else
|
||||
#include <cstdio>
|
||||
#define DDFG_LOGI(...) do { std::fprintf(stderr, "[dd-fg] " __VA_ARGS__); std::fprintf(stderr, "\n"); } while(0)
|
||||
#define DDFG_LOGE(...) do { std::fprintf(stderr, "[dd-fg][E] " __VA_ARGS__); std::fprintf(stderr, "\n"); } while(0)
|
||||
#endif
|
||||
|
||||
#define DDFG_LOGD(cond, ...) do { if (cond) DDFG_LOGI(__VA_ARGS__); } while(0)
|
||||
+159
-142
@@ -1,31 +1,34 @@
|
||||
|
||||
namespace {
|
||||
struct ScratchState {
|
||||
VkBuffer synthUbo = VK_NULL_HANDLE; VkDeviceMemory synthUboMem = VK_NULL_HANDLE; void* synthUboPtr = nullptr;
|
||||
VkBuffer synthUbo[ddfg::FrameGen::kSynthSlots]{};
|
||||
VkDeviceMemory synthUboMem[ddfg::FrameGen::kSynthSlots]{};
|
||||
void* synthUboPtr[ddfg::FrameGen::kSynthSlots]{};
|
||||
std::vector<VkBuffer> flowUbo; std::vector<VkDeviceMemory> flowUboMem; std::vector<void*> flowUboPtr;
|
||||
VkBuffer expandUbo = VK_NULL_HANDLE; VkDeviceMemory expandUboMem = VK_NULL_HANDLE; void* expandUboPtr = nullptr;
|
||||
VkBuffer flowRegUbo = VK_NULL_HANDLE; VkDeviceMemory flowRegUboMem = VK_NULL_HANDLE; void* flowRegUboPtr = nullptr;
|
||||
VkBuffer gmUbo[winfg::FrameGen::kGmSlots]{};
|
||||
VkDeviceMemory gmUboMem[winfg::FrameGen::kGmSlots]{};
|
||||
void* gmUboPtr[winfg::FrameGen::kGmSlots]{};
|
||||
VkBuffer gmUbo[ddfg::FrameGen::kGmSlots]{};
|
||||
VkDeviceMemory gmUboMem[ddfg::FrameGen::kGmSlots]{};
|
||||
void* gmUboPtr[ddfg::FrameGen::kGmSlots]{};
|
||||
VkDescriptorSet sLumaCurr = VK_NULL_HANDLE, sLumaPrev = VK_NULL_HANDLE;
|
||||
std::vector<VkDescriptorSet> sDownA, sDownB, sFlow;
|
||||
VkDescriptorSet sExpand[winfg::FrameGen::kGmSlots]{};
|
||||
VkDescriptorSet sExpand[ddfg::FrameGen::kGmSlots]{};
|
||||
|
||||
|
||||
|
||||
VkDescriptorSet sFlowReg0 = VK_NULL_HANDLE, sFlowRegAB = VK_NULL_HANDLE, sFlowRegBA = VK_NULL_HANDLE;
|
||||
VkDescriptorSet sSynth = VK_NULL_HANDLE;
|
||||
VkDescriptorSet sGmReduce[winfg::FrameGen::kGmSlots]{};
|
||||
std::vector<VkDescriptorSet> sGmPrewarp[winfg::FrameGen::kGmSlots];
|
||||
VkDescriptorSet sSynth[ddfg::FrameGen::kSynthSlots]{};
|
||||
VkDescriptorSet sGmReduce[ddfg::FrameGen::kGmSlots]{};
|
||||
std::vector<VkDescriptorSet> sGmPrewarp[ddfg::FrameGen::kGmSlots];
|
||||
VkExtent2D builtFor{0,0};
|
||||
bool built = false;
|
||||
bool flowInited = false;
|
||||
bool flowReady = false;
|
||||
};
|
||||
ScratchState g_scratch;
|
||||
}
|
||||
|
||||
static bool makeUbo(const winfg::DeviceDispatch* dd, VkDevice dev,
|
||||
static bool makeUbo(const ddfg::DeviceDispatch* dd, VkDevice dev,
|
||||
const VkPhysicalDeviceMemoryProperties& mp,
|
||||
VkDeviceSize size, VkBuffer& buf, VkDeviceMemory& mem, void*& ptr) {
|
||||
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
@@ -41,21 +44,21 @@ static bool makeUbo(const winfg::DeviceDispatch* dd, VkDevice dev,
|
||||
return dd->MapMemory(dev, mem, 0, size, 0, &ptr) == VK_SUCCESS;
|
||||
}
|
||||
|
||||
static VkDescriptorSet allocSet(const winfg::DeviceDispatch* dd, VkDevice dev,
|
||||
static VkDescriptorSet allocSet(const ddfg::DeviceDispatch* dd, VkDevice dev,
|
||||
VkDescriptorPool pool, VkDescriptorSetLayout layout) {
|
||||
VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
||||
ai.descriptorPool = pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &layout;
|
||||
VkDescriptorSet s = VK_NULL_HANDLE; dd->AllocateDescriptorSets(dev, &ai, &s); return s;
|
||||
}
|
||||
|
||||
static void wImg(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
|
||||
static void wImg(const ddfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
|
||||
VkDescriptorType type, VkImageView view, VkSampler samp, VkImageLayout layout) {
|
||||
VkDescriptorImageInfo ii{}; ii.imageView = view; ii.sampler = samp; ii.imageLayout = layout;
|
||||
VkWriteDescriptorSet w{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
w.dstSet = set; w.dstBinding = binding; w.descriptorCount = 1; w.descriptorType = type; w.pImageInfo = ⅈ
|
||||
dd->UpdateDescriptorSets(dev, 1, &w, 0, nullptr);
|
||||
}
|
||||
static void wUbo(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
|
||||
static void wUbo(const ddfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
|
||||
VkBuffer buf, VkDeviceSize range) {
|
||||
VkDescriptorBufferInfo bi{}; bi.buffer = buf; bi.range = range;
|
||||
VkWriteDescriptorSet w{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
@@ -63,7 +66,7 @@ static void wUbo(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet
|
||||
w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &bi;
|
||||
dd->UpdateDescriptorSets(dev, 1, &w, 0, nullptr);
|
||||
}
|
||||
static void wSsbo(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
|
||||
static void wSsbo(const ddfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
|
||||
VkBuffer buf, VkDeviceSize range) {
|
||||
VkDescriptorBufferInfo bi{}; bi.buffer = buf; bi.range = range;
|
||||
VkWriteDescriptorSet w{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
||||
@@ -117,7 +120,7 @@ static void mulAff(const double A[3][3], const double B[3][3], double out[3][3])
|
||||
out[2][0]=0.0; out[2][1]=0.0; out[2][2]=1.0;
|
||||
}
|
||||
|
||||
namespace winfg {
|
||||
namespace ddfg {
|
||||
|
||||
static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
const VkPhysicalDeviceMemoryProperties& mp, VkDescriptorPool pool,
|
||||
@@ -140,7 +143,7 @@ static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorP
|
||||
b = VK_NULL_HANDLE; m = VK_NULL_HANDLE; p = nullptr;
|
||||
};
|
||||
auto& S = g_scratch;
|
||||
freeUbo(S.synthUbo, S.synthUboMem, S.synthUboPtr);
|
||||
for (int s = 0; s < FrameGen::kSynthSlots; ++s) freeUbo(S.synthUbo[s], S.synthUboMem[s], S.synthUboPtr[s]);
|
||||
freeUbo(S.expandUbo, S.expandUboMem, S.expandUboPtr);
|
||||
freeUbo(S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr);
|
||||
for (size_t i = 0; i < S.flowUbo.size(); ++i) freeUbo(S.flowUbo[i], S.flowUboMem[i], S.flowUboPtr[i]);
|
||||
@@ -154,7 +157,7 @@ void FrameGen::configure(const Config& c) {
|
||||
int want = flowFinestForPreset(cfg_.perfPreset);
|
||||
const int maxFinest = kLevels - 2;
|
||||
if (want > maxFinest) {
|
||||
WFG_LOGE("perf-preset=%d wants kFlowFinest=%d but only %d pyramid levels exist "
|
||||
DDFG_LOGE("perf-preset=%d wants kFlowFinest=%d but only %d pyramid levels exist "
|
||||
"(need >=2 solved) - clamping to %d", cfg_.perfPreset, want, kLevels, maxFinest);
|
||||
want = maxFinest;
|
||||
}
|
||||
@@ -163,7 +166,7 @@ void FrameGen::configure(const Config& c) {
|
||||
const int prevFinest = flowFinest_;
|
||||
flowFinest_ = want;
|
||||
const bool live = (flowFinest_ != prevFinest) && ready_;
|
||||
WFG_LOGI("perf-preset=%d -> kFlowFinest=%d (was %d) live-rebuild=%d ready=%d",
|
||||
DDFG_LOGI("perf-preset=%d -> kFlowFinest=%d (was %d) live-rebuild=%d ready=%d",
|
||||
cfg_.perfPreset, flowFinest_, prevFinest, live ? 1 : 0, ready_ ? 1 : 0);
|
||||
|
||||
if (!live) return;
|
||||
@@ -174,12 +177,12 @@ void FrameGen::configure(const Config& c) {
|
||||
dd_->DeviceWaitIdle(device_);
|
||||
destroyScratch(dd_, device_, descPool_);
|
||||
if (!onResize(extent_, VK_FORMAT_UNDEFINED, true))
|
||||
WFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u - FG disabled until next resize",
|
||||
DDFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u - FG disabled until next resize",
|
||||
extent_.width, extent_.height);
|
||||
for (int i = 0; i < 6; ++i) gmRun_[i] = 0.0;
|
||||
for (int s = 0; s < kGmSlots; ++s) { gmSlotValid_[s] = false; for (int i = 0; i < 6; ++i) gmLinAt_[s][i] = 0.0; }
|
||||
gmHaveEst_ = false; gmEngaged_ = false; gmStableCount_ = 0; gmLastTransPx_ = 0.0f;
|
||||
WFG_LOGI("perf-preset live rebuild DONE (kFlowFinest=%d, flow predictor reset)", flowFinest_);
|
||||
DDFG_LOGI("perf-preset live rebuild DONE (kFlowFinest=%d, flow predictor reset)", flowFinest_);
|
||||
}
|
||||
|
||||
bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) {
|
||||
@@ -249,9 +252,10 @@ bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) {
|
||||
}
|
||||
|
||||
void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView currView,
|
||||
VkImageView outView, float alpha, uint32_t gmSlot) {
|
||||
VkImageView outView, float alpha, uint32_t gmSlot, uint32_t synthSlot, bool reuseFlow) {
|
||||
if (!valid()) return;
|
||||
if (gmSlot >= (uint32_t)kGmSlots) gmSlot = 0;
|
||||
if (synthSlot >= (uint32_t)kSynthSlots) synthSlot = 0;
|
||||
const bool m4 = (cfg_.model == 4);
|
||||
const Pipe& flowP = m4 ? pFlowM4_ : pFlow_;
|
||||
const Pipe& expandP = m4 ? pExpandM4_ : pExpand_;
|
||||
@@ -263,35 +267,42 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
|
||||
pFlowReg_, flowRegA_, flowRegB_, gmSsbo_, kLevels, cfg_);
|
||||
}
|
||||
|
||||
bool gmEngaged = updateGlobalMotion(gmSlot, extent_);
|
||||
if (g_scratch.gmUboPtr[gmSlot]) {
|
||||
GMUBO g{};
|
||||
g.lin[0]=(float)gmRun_[0]; g.lin[1]=(float)gmRun_[1]; g.lin[2]=(float)gmRun_[2]; g.lin[3]=(float)gmRun_[3];
|
||||
g.linT[0]=(float)gmRun_[4]; g.linT[1]=(float)gmRun_[5]; g.linT[2]=gmEngaged?1.0f:0.0f; g.linT[3]=0.0f;
|
||||
if (gmEngaged) {
|
||||
g.app[0]=(float)gmRun_[0]; g.app[1]=(float)gmRun_[1]; g.app[2]=(float)gmRun_[2]; g.app[3]=(float)gmRun_[3];
|
||||
g.appT[0]=(float)gmRun_[4]; g.appT[1]=(float)gmRun_[5];
|
||||
// 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_);
|
||||
if (g_scratch.gmUboPtr[gmSlot]) {
|
||||
GMUBO g{};
|
||||
g.lin[0]=(float)gmRun_[0]; g.lin[1]=(float)gmRun_[1]; g.lin[2]=(float)gmRun_[2]; g.lin[3]=(float)gmRun_[3];
|
||||
g.linT[0]=(float)gmRun_[4]; g.linT[1]=(float)gmRun_[5]; g.linT[2]=gmEngaged?1.0f:0.0f; g.linT[3]=0.0f;
|
||||
if (gmEngaged) {
|
||||
g.app[0]=(float)gmRun_[0]; g.app[1]=(float)gmRun_[1]; g.app[2]=(float)gmRun_[2]; g.app[3]=(float)gmRun_[3];
|
||||
g.appT[0]=(float)gmRun_[4]; g.appT[1]=(float)gmRun_[5];
|
||||
}
|
||||
std::memcpy(g_scratch.gmUboPtr[gmSlot], &g, sizeof(g));
|
||||
}
|
||||
std::memcpy(g_scratch.gmUboPtr[gmSlot], &g, sizeof(g));
|
||||
}
|
||||
{
|
||||
static int lastEng = -1;
|
||||
if (gmEngaged != (lastEng == 1) || (gmFrames_ % 300ull) == 0) {
|
||||
WFG_LOGI("C1 global-motion: engaged=%d mode=%d stable=%d transPx=%.1f (model=%d)",
|
||||
gmEngaged ? 1 : 0, cfg_.gmMode, gmStableCount_, gmLastTransPx_, cfg_.model);
|
||||
lastEng = gmEngaged ? 1 : 0;
|
||||
{
|
||||
static int lastEng = -1;
|
||||
if (gmEngaged != (lastEng == 1) || (gmFrames_ % 300ull) == 0) {
|
||||
DDFG_LOGI("C1 global-motion: engaged=%d mode=%d stable=%d transPx=%.1f (model=%d)",
|
||||
gmEngaged ? 1 : 0, cfg_.gmMode, gmStableCount_, gmLastTransPx_, cfg_.model);
|
||||
lastEng = gmEngaged ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
wImg(dd_, device_, g_scratch.sLumaCurr, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, currView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
wImg(dd_, device_, g_scratch.sLumaPrev, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, prevView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
}
|
||||
|
||||
SynthUBO su{cfg_.flowScale, alpha, cfg_.beta, cfg_.lambda, cfg_.epsilon, cfg_.photoScale, 0, 0,
|
||||
cfg_.hudX0, cfg_.hudY0, cfg_.hudX1, cfg_.hudY1};
|
||||
if (g_scratch.synthUboPtr) std::memcpy(g_scratch.synthUboPtr, &su, sizeof(su));
|
||||
if (g_scratch.synthUboPtr[synthSlot]) std::memcpy(g_scratch.synthUboPtr[synthSlot], &su, sizeof(su));
|
||||
|
||||
wImg(dd_, device_, g_scratch.sLumaCurr, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, currView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
wImg(dd_, device_, g_scratch.sLumaPrev, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, prevView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
wImg(dd_, device_, g_scratch.sSynth, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, prevView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
wImg(dd_, device_, g_scratch.sSynth, 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, currView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
wImg(dd_, device_, g_scratch.sSynth, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, outView, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL);
|
||||
const VkDescriptorSet synthSet = g_scratch.sSynth[synthSlot];
|
||||
wImg(dd_, device_, synthSet, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, prevView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
wImg(dd_, device_, synthSet, 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, currView, linear_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
wImg(dd_, device_, synthSet, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, outView, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_GENERAL);
|
||||
|
||||
auto disp = [&](const Pipe& p, VkDescriptorSet set, VkExtent2D e) {
|
||||
dd_->CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, p.pipeline);
|
||||
@@ -300,106 +311,109 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
|
||||
};
|
||||
auto bar = [&](FrameGen::Img& i){ barrier(cmd, i.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL); };
|
||||
|
||||
for (auto& i : pyrA_) barrier(cmd, i.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
for (auto& i : pyrB_) barrier(cmd, i.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
for (int l = flowFinest_; l < kLevels; ++l) barrier(cmd, pyrAs_[l].image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
barrier(cmd, flowExpA_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
barrier(cmd, flowExpB_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
barrier(cmd, flowRegA_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
barrier(cmd, flowRegB_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
{
|
||||
VkClearColorValue zero{};
|
||||
VkImageSubresourceRange rng{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
if (!reuseFlow) {
|
||||
for (auto& i : pyrA_) barrier(cmd, i.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
for (auto& i : pyrB_) barrier(cmd, i.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
for (int l = flowFinest_; l < kLevels; ++l) barrier(cmd, pyrAs_[l].image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
barrier(cmd, flowExpA_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
barrier(cmd, flowExpB_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
barrier(cmd, flowRegA_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
barrier(cmd, flowRegB_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL);
|
||||
{
|
||||
VkClearColorValue zero{};
|
||||
VkImageSubresourceRange rng{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
for (int l = flowFinest_; l < kLevels; ++l) {
|
||||
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
b.srcAccessMask = 0; b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
b.image = flowLvl_[l].image; b.subresourceRange = rng;
|
||||
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
dd_->CmdClearColorImage(cmd, flowLvl_[l].image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, &rng);
|
||||
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; b.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
}
|
||||
}
|
||||
|
||||
disp(pLuma_, g_scratch.sLumaCurr, levelExtent(extent_, 0));
|
||||
disp(pLuma_, g_scratch.sLumaPrev, levelExtent(extent_, 0));
|
||||
bar(pyrA_[0]); bar(pyrB_[0]);
|
||||
for (int l = 1; l < kLevels; ++l) {
|
||||
disp(pDown_, g_scratch.sDownA[l - 1], levelExtent(extent_, l));
|
||||
disp(pDown_, g_scratch.sDownB[l - 1], levelExtent(extent_, l));
|
||||
bar(pyrA_[l]); bar(pyrB_[l]);
|
||||
}
|
||||
if (gmBuffersReady_ && g_scratch.sGmReduce[gmSlot]) {
|
||||
dd_->CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pGmReduce_.pipeline);
|
||||
dd_->CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pGmReduce_.layout, 0, 1, &g_scratch.sGmReduce[gmSlot], 0, nullptr);
|
||||
dd_->CmdDispatch(cmd, (uint32_t)(kGmThreads / 64), 1, 1);
|
||||
VkMemoryBarrier mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
mb.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; mb.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0, 1, &mb, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
for (int l = flowFinest_; l < kLevels; ++l) {
|
||||
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
b.srcAccessMask = 0; b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
b.image = flowLvl_[l].image; b.subresourceRange = rng;
|
||||
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
dd_->CmdClearColorImage(cmd, flowLvl_[l].image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, &rng);
|
||||
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; b.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b);
|
||||
disp(pGmPrewarp_, g_scratch.sGmPrewarp[gmSlot][l], levelExtent(extent_, l));
|
||||
bar(pyrAs_[l]);
|
||||
}
|
||||
for (int l = kLevels - 1; l >= flowFinest_; --l) {
|
||||
disp(flowP, g_scratch.sFlow[l], levelExtent(extent_, l));
|
||||
bar(flowLvl_[l]);
|
||||
}
|
||||
}
|
||||
|
||||
disp(pLuma_, g_scratch.sLumaCurr, levelExtent(extent_, 0));
|
||||
disp(pLuma_, g_scratch.sLumaPrev, levelExtent(extent_, 0));
|
||||
bar(pyrA_[0]); bar(pyrB_[0]);
|
||||
for (int l = 1; l < kLevels; ++l) {
|
||||
disp(pDown_, g_scratch.sDownA[l - 1], levelExtent(extent_, l));
|
||||
disp(pDown_, g_scratch.sDownB[l - 1], levelExtent(extent_, l));
|
||||
bar(pyrA_[l]); bar(pyrB_[l]);
|
||||
}
|
||||
if (gmBuffersReady_ && g_scratch.sGmReduce[gmSlot]) {
|
||||
dd_->CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pGmReduce_.pipeline);
|
||||
dd_->CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pGmReduce_.layout, 0, 1, &g_scratch.sGmReduce[gmSlot], 0, nullptr);
|
||||
dd_->CmdDispatch(cmd, (uint32_t)(kGmThreads / 64), 1, 1);
|
||||
VkMemoryBarrier mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
mb.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; mb.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0, 1, &mb, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
for (int l = flowFinest_; l < kLevels; ++l) {
|
||||
disp(pGmPrewarp_, g_scratch.sGmPrewarp[gmSlot][l], levelExtent(extent_, l));
|
||||
bar(pyrAs_[l]);
|
||||
}
|
||||
for (int l = kLevels - 1; l >= flowFinest_; --l) {
|
||||
disp(flowP, g_scratch.sFlow[l], levelExtent(extent_, l));
|
||||
bar(flowLvl_[l]);
|
||||
}
|
||||
const bool frEngaged = (cfg_.frMode != 2) && (cfg_.frIters > 0);
|
||||
if (frEngaged) {
|
||||
const int F = flowFinest_;
|
||||
const VkExtent2D fe = levelExtent(extent_, F);
|
||||
if (g_scratch.flowRegUboPtr) {
|
||||
FlowRegUBO fu{cfg_.frDt, cfg_.frLambda, cfg_.frEdge, cfg_.frEps};
|
||||
std::memcpy(g_scratch.flowRegUboPtr, &fu, sizeof(fu));
|
||||
}
|
||||
FrameGen::Img* last = nullptr;
|
||||
for (int it = 0; it < cfg_.frIters; ++it) {
|
||||
VkDescriptorSet set; FrameGen::Img* outImg;
|
||||
if (it == 0) { set = g_scratch.sFlowReg0; outImg = &flowRegA_; }
|
||||
else if (it % 2 == 1) { set = g_scratch.sFlowRegAB; outImg = &flowRegB_; }
|
||||
else { set = g_scratch.sFlowRegBA; outImg = &flowRegA_; }
|
||||
dd_->CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pFlowReg_.pipeline);
|
||||
dd_->CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pFlowReg_.layout, 0, 1, &set, 0, nullptr);
|
||||
dd_->CmdDispatch(cmd, (fe.width + 15) / 16, (fe.height + 15) / 16, 1);
|
||||
bar(*outImg);
|
||||
last = outImg;
|
||||
}
|
||||
VkMemoryBarrier mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
mb.srcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
||||
mb.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 1, &mb, 0, nullptr, 0, nullptr);
|
||||
VkImageCopy region{};
|
||||
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.extent = {fe.width, fe.height, 1};
|
||||
dd_->CmdCopyImage(cmd, last->image, VK_IMAGE_LAYOUT_GENERAL,
|
||||
flowLvl_[F].image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion);
|
||||
VkMemoryBarrier mb2{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
mb2.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
mb2.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
0, 1, &mb2, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
{
|
||||
static int lastFr = -1;
|
||||
int fe = frEngaged ? 1 : 0;
|
||||
if (fe != lastFr || (gmFrames_ % 300ull) == 0) {
|
||||
DDFG_LOGI("C2 flow-reg: engaged=%d mode=%d iters=%d lambda=%.2f dt=%.2f edge=%.1f eps=%.3f (model=%d)",
|
||||
fe, cfg_.frMode, cfg_.frIters, cfg_.frLambda, cfg_.frDt, cfg_.frEdge, cfg_.frEps, cfg_.model);
|
||||
lastFr = fe;
|
||||
}
|
||||
}
|
||||
|
||||
const bool frEngaged = (cfg_.frMode != 2) && (cfg_.frIters > 0);
|
||||
if (frEngaged) {
|
||||
const int F = flowFinest_;
|
||||
const VkExtent2D fe = levelExtent(extent_, F);
|
||||
if (g_scratch.flowRegUboPtr) {
|
||||
FlowRegUBO fu{cfg_.frDt, cfg_.frLambda, cfg_.frEdge, cfg_.frEps};
|
||||
std::memcpy(g_scratch.flowRegUboPtr, &fu, sizeof(fu));
|
||||
}
|
||||
FrameGen::Img* last = nullptr;
|
||||
for (int it = 0; it < cfg_.frIters; ++it) {
|
||||
VkDescriptorSet set; FrameGen::Img* outImg;
|
||||
if (it == 0) { set = g_scratch.sFlowReg0; outImg = &flowRegA_; }
|
||||
else if (it % 2 == 1) { set = g_scratch.sFlowRegAB; outImg = &flowRegB_; }
|
||||
else { set = g_scratch.sFlowRegBA; outImg = &flowRegA_; }
|
||||
dd_->CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pFlowReg_.pipeline);
|
||||
dd_->CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pFlowReg_.layout, 0, 1, &set, 0, nullptr);
|
||||
dd_->CmdDispatch(cmd, (fe.width + 15) / 16, (fe.height + 15) / 16, 1);
|
||||
bar(*outImg);
|
||||
last = outImg;
|
||||
}
|
||||
VkMemoryBarrier mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
mb.srcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
||||
mb.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 1, &mb, 0, nullptr, 0, nullptr);
|
||||
VkImageCopy region{};
|
||||
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.extent = {fe.width, fe.height, 1};
|
||||
dd_->CmdCopyImage(cmd, last->image, VK_IMAGE_LAYOUT_GENERAL,
|
||||
flowLvl_[F].image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion);
|
||||
VkMemoryBarrier mb2{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
mb2.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
mb2.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
0, 1, &mb2, 0, nullptr, 0, nullptr);
|
||||
disp(expandP, g_scratch.sExpand[gmSlot], levelExtent(extent_, 0));
|
||||
bar(flowExpA_); bar(flowExpB_);
|
||||
g_scratch.flowReady = true;
|
||||
}
|
||||
{
|
||||
static int lastFr = -1;
|
||||
int fe = frEngaged ? 1 : 0;
|
||||
if (fe != lastFr || (gmFrames_ % 300ull) == 0) {
|
||||
WFG_LOGI("C2 flow-reg: engaged=%d mode=%d iters=%d lambda=%.2f dt=%.2f edge=%.1f eps=%.3f (model=%d)",
|
||||
fe, cfg_.frMode, cfg_.frIters, cfg_.frLambda, cfg_.frDt, cfg_.frEdge, cfg_.frEps, cfg_.model);
|
||||
lastFr = fe;
|
||||
}
|
||||
}
|
||||
|
||||
disp(expandP, g_scratch.sExpand[gmSlot], levelExtent(extent_, 0));
|
||||
bar(flowExpA_); bar(flowExpB_);
|
||||
disp(pSynth_, g_scratch.sSynth, levelExtent(extent_, 0));
|
||||
disp(pSynth_, synthSet, levelExtent(extent_, 0));
|
||||
}
|
||||
|
||||
static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
@@ -418,7 +432,8 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
S = ScratchState{};
|
||||
const auto GEN = VK_IMAGE_LAYOUT_GENERAL;
|
||||
|
||||
makeUbo(dd, dev, mp, sizeof(SynthUBO), S.synthUbo, S.synthUboMem, S.synthUboPtr);
|
||||
for (int s = 0; s < FrameGen::kSynthSlots; ++s)
|
||||
makeUbo(dd, dev, mp, sizeof(SynthUBO), S.synthUbo[s], S.synthUboMem[s], S.synthUboPtr[s]);
|
||||
makeUbo(dd, dev, mp, sizeof(FlowUBO), S.expandUbo, S.expandUboMem, S.expandUboPtr);
|
||||
if (S.expandUboPtr) { FlowUBO u{cfg.flowScale, 0, 0.6f, 1.6f}; std::memcpy(S.expandUboPtr, &u, sizeof(u)); }
|
||||
makeUbo(dd, dev, mp, sizeof(FlowRegUBO), S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr);
|
||||
@@ -509,10 +524,12 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
wImg(dd, dev, S.sFlowRegBA, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegA.view, VK_NULL_HANDLE, GEN);
|
||||
}
|
||||
|
||||
S.sSynth = allocSet(dd, dev, pool, pSynth.setLayout);
|
||||
wUbo(dd, dev, S.sSynth, 0, S.synthUbo, sizeof(SynthUBO));
|
||||
wImg(dd, dev, S.sSynth, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expA.view, samp, GEN);
|
||||
wImg(dd, dev, S.sSynth, 35, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expB.view, samp, GEN);
|
||||
for (int s = 0; s < FrameGen::kSynthSlots; ++s) {
|
||||
S.sSynth[s] = allocSet(dd, dev, pool, pSynth.setLayout);
|
||||
wUbo(dd, dev, S.sSynth[s], 0, S.synthUbo[s], sizeof(SynthUBO));
|
||||
wImg(dd, dev, S.sSynth[s], 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expA.view, samp, GEN);
|
||||
wImg(dd, dev, S.sSynth[s], 35, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, expB.view, samp, GEN);
|
||||
}
|
||||
|
||||
S.builtFor = ext; S.built = true;
|
||||
return true;
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
#include <vulkan/vk_layer.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace winfg {
|
||||
namespace ddfg {
|
||||
|
||||
static inline void* dispatch_key(void* handle) { return *reinterpret_cast<void**>(handle); }
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
#if defined(__ANDROID__)
|
||||
#include <android/log.h>
|
||||
#define WFG_LOGI(...) __android_log_print(ANDROID_LOG_INFO, "win-fg", __VA_ARGS__)
|
||||
#define WFG_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "win-fg", __VA_ARGS__)
|
||||
#else
|
||||
#include <cstdio>
|
||||
#define WFG_LOGI(...) do { std::fprintf(stderr, "[win-fg] " __VA_ARGS__); std::fprintf(stderr, "\n"); } while(0)
|
||||
#define WFG_LOGE(...) do { std::fprintf(stderr, "[win-fg][E] " __VA_ARGS__); std::fprintf(stderr, "\n"); } while(0)
|
||||
#endif
|
||||
|
||||
#define WFG_LOGD(cond, ...) do { if (cond) WFG_LOGI(__VA_ARGS__); } while(0)
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
#include "vk_dispatch.hpp"
|
||||
struct VkTable;
|
||||
bool winfgVkdInit(const VkTable& table, winfg::DeviceDispatch& dd, winfg::InstanceDispatch& id);
|
||||
@@ -1,5 +1,8 @@
|
||||
#include <errno.h>
|
||||
#include <jni.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_input_FakeInputWriter_nativeStoreFence(JNIEnv *env, jclass clazz) {
|
||||
@@ -7,3 +10,10 @@ Java_com_droiddeck_launcher_input_FakeInputWriter_nativeStoreFence(JNIEnv *env,
|
||||
(void)clazz;
|
||||
atomic_thread_fence(memory_order_release);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_session_ThreadSignals_tgkill(JNIEnv *env, jclass clazz, jint tgid, jint tid, jint signal) {
|
||||
(void)env;
|
||||
(void)clazz;
|
||||
return syscall(SYS_tgkill, tgid, tid, signal) == 0 ? 0 : errno;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#ifndef DROIDDECK_VENC_PROTO_H
|
||||
#define DROIDDECK_VENC_PROTO_H
|
||||
|
||||
#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 };
|
||||
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 };
|
||||
|
||||
struct venc_config {
|
||||
uint32_t type;
|
||||
uint32_t codec;
|
||||
uint32_t width, height, stride, rows;
|
||||
uint32_t fps_num, fps_den;
|
||||
uint32_t bitrate, peak_bitrate, vbr;
|
||||
uint32_t gop;
|
||||
int32_t profile, level;
|
||||
uint32_t inputs, outputs;
|
||||
uint32_t input_size, output_size;
|
||||
};
|
||||
|
||||
struct venc_frame {
|
||||
uint32_t type;
|
||||
uint32_t input, output;
|
||||
uint32_t bytes;
|
||||
uint32_t keyframe;
|
||||
int64_t pts_us;
|
||||
};
|
||||
|
||||
struct venc_done {
|
||||
uint32_t type;
|
||||
int32_t status;
|
||||
uint32_t bytes;
|
||||
uint32_t keyframe;
|
||||
};
|
||||
|
||||
struct venc_bitrate {
|
||||
uint32_t type;
|
||||
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;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,346 @@
|
||||
#include <android/log.h>
|
||||
#include <dlfcn.h>
|
||||
#include <jni.h>
|
||||
#include <media/NdkMediaCodec.h>
|
||||
#include <media/NdkMediaFormat.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "venc_proto.h"
|
||||
|
||||
#define LOG(...) __android_log_print(ANDROID_LOG_INFO, "DroidDeckVenc", __VA_ARGS__)
|
||||
|
||||
namespace {
|
||||
|
||||
struct Mapping {
|
||||
uint8_t *data = nullptr;
|
||||
size_t size = 0;
|
||||
};
|
||||
|
||||
constexpr int32_t kColorFormatYuv420SemiPlanar = 21;
|
||||
constexpr uint32_t kBufferFlagKeyFrame = 1;
|
||||
constexpr uint32_t kBufferFlagCodecConfig = 2;
|
||||
constexpr int kTouchWidth = 1920, kTouchHeight = 1080; // InputSpace.kt
|
||||
constexpr int kRemoteTouchId = 31; // the compositor's last touch slot (MAX_TOUCH_POINTS 32)
|
||||
|
||||
int32_t avc_profile(int32_t v4l2) {
|
||||
switch (v4l2) {
|
||||
case 0: return 1;
|
||||
case 1: return 0x10000;
|
||||
case 2: return 2;
|
||||
case 3: return 4;
|
||||
case 4: return 8;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class Session {
|
||||
public:
|
||||
explicit Session(int fd) : fd_(fd) {}
|
||||
~Session() {
|
||||
release();
|
||||
close(fd_);
|
||||
}
|
||||
|
||||
void run() {
|
||||
for (;;) {
|
||||
uint8_t buf[256];
|
||||
int fds[VENC_MAX_BUFFERS * 2];
|
||||
int nfds = 0;
|
||||
ssize_t n = receive(buf, sizeof buf, fds, &nfds);
|
||||
if (n <= 0) return;
|
||||
uint32_t type;
|
||||
memcpy(&type, buf, sizeof type);
|
||||
if (type == VENC_CONFIG && n == (ssize_t)sizeof(venc_config)) {
|
||||
venc_config c;
|
||||
memcpy(&c, buf, sizeof c);
|
||||
bool ok = configure(c, fds, nfds);
|
||||
for (int i = 0; i < nfds; i++) close(fds[i]);
|
||||
reply(ok ? 0 : -1, 0, false);
|
||||
} else if (type == VENC_FRAME && n == (ssize_t)sizeof(venc_frame)) {
|
||||
venc_frame f;
|
||||
memcpy(&f, buf, sizeof f);
|
||||
uint32_t bytes = 0;
|
||||
bool key = false;
|
||||
bool ok = encode(f, &bytes, &key);
|
||||
reply(ok ? 0 : -1, bytes, key);
|
||||
} else if (type == VENC_TOUCH && n == (ssize_t)sizeof(venc_touch)) {
|
||||
venc_touch t;
|
||||
memcpy(&t, buf, sizeof t);
|
||||
touch(t);
|
||||
for (int i = 0; i < nfds; i++) close(fds[i]);
|
||||
} else if (type == VENC_BITRATE && n == (ssize_t)sizeof(venc_bitrate)) {
|
||||
venc_bitrate b;
|
||||
memcpy(&b, buf, sizeof b);
|
||||
set_bitrate(b.bitrate);
|
||||
for (int i = 0; i < nfds; i++) close(fds[i]);
|
||||
} else {
|
||||
LOG("unexpected message %u of %zd bytes; closing", type, n);
|
||||
for (int i = 0; i < nfds; i++) close(fds[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
ssize_t receive(uint8_t *buf, size_t len, int *fds, int *nfds) {
|
||||
char control[CMSG_SPACE(sizeof(int) * VENC_MAX_BUFFERS * 2)];
|
||||
iovec iov = {buf, len};
|
||||
msghdr m = {};
|
||||
m.msg_iov = &iov;
|
||||
m.msg_iovlen = 1;
|
||||
m.msg_control = control;
|
||||
m.msg_controllen = sizeof control;
|
||||
ssize_t n = recvmsg(fd_, &m, MSG_CMSG_CLOEXEC);
|
||||
for (cmsghdr *c = CMSG_FIRSTHDR(&m); n > 0 && c; c = CMSG_NXTHDR(&m, c)) {
|
||||
if (c->cmsg_level != SOL_SOCKET || c->cmsg_type != SCM_RIGHTS) continue;
|
||||
int count = (int)((c->cmsg_len - CMSG_LEN(0)) / sizeof(int));
|
||||
memcpy(fds, CMSG_DATA(c), sizeof(int) * std::min(count, VENC_MAX_BUFFERS * 2));
|
||||
*nfds = std::min(count, VENC_MAX_BUFFERS * 2);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void reply(int32_t status, uint32_t bytes, bool key) {
|
||||
venc_done d = {VENC_DONE, status, bytes, key ? 1u : 0u};
|
||||
send(fd_, &d, sizeof d, MSG_NOSIGNAL);
|
||||
}
|
||||
|
||||
void release() {
|
||||
if (codec_) {
|
||||
AMediaCodec_stop(codec_);
|
||||
AMediaCodec_delete(codec_);
|
||||
codec_ = nullptr;
|
||||
}
|
||||
for (auto &m : inputs_) munmap(m.data, m.size);
|
||||
for (auto &m : outputs_) munmap(m.data, m.size);
|
||||
inputs_.clear();
|
||||
outputs_.clear();
|
||||
header_.clear();
|
||||
}
|
||||
|
||||
AMediaCodec *create(const venc_config &c, bool with_profile) {
|
||||
const char *mime = c.codec == VENC_HEVC ? "video/hevc" : "video/avc";
|
||||
AMediaCodec *codec = AMediaCodec_createEncoderByType(mime);
|
||||
if (!codec) return nullptr;
|
||||
float fps = c.fps_num ? (float)c.fps_den / (float)c.fps_num : 60.0f;
|
||||
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_BIT_RATE, (int32_t)std::max(c.bitrate, 500000u));
|
||||
AMediaFormat_setInt32(f, "bitrate-mode", c.vbr ? 1 : 2);
|
||||
AMediaFormat_setFloat(f, AMEDIAFORMAT_KEY_FRAME_RATE, fps);
|
||||
AMediaFormat_setFloat(f, "operating-rate", fps);
|
||||
AMediaFormat_setInt32(f, "priority", 0);
|
||||
AMediaFormat_setInt32(f, "latency", 1);
|
||||
AMediaFormat_setInt32(f, "max-bframes", 0);
|
||||
AMediaFormat_setInt32(f, "prepend-sps-pps-to-idr-frames", 1);
|
||||
float gop_seconds = c.gop && c.gop < 0xffff ? (float)c.gop / fps : 3600.0f;
|
||||
AMediaFormat_setFloat(f, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, std::max(gop_seconds, 0.1f));
|
||||
if (with_profile) {
|
||||
int32_t profile = c.codec == VENC_HEVC ? (c.profile == 2 ? 2 : 1) : avc_profile(c.profile);
|
||||
if (profile) AMediaFormat_setInt32(f, "profile", profile);
|
||||
}
|
||||
media_status_t s = AMediaCodec_configure(codec, f, nullptr, nullptr, AMEDIACODEC_CONFIGURE_FLAG_ENCODE);
|
||||
AMediaFormat_delete(f);
|
||||
if (s == AMEDIA_OK) s = AMediaCodec_start(codec);
|
||||
if (s != AMEDIA_OK) {
|
||||
AMediaCodec_delete(codec);
|
||||
return nullptr;
|
||||
}
|
||||
return codec;
|
||||
}
|
||||
|
||||
bool configure(const venc_config &c, const int *fds, int nfds) {
|
||||
release();
|
||||
if (c.inputs + c.outputs != (uint32_t)nfds || c.width < 64 || c.height < 64 || c.stride < c.width ||
|
||||
c.rows < c.height || (uint64_t)c.stride * c.rows * 3 / 2 > c.input_size) {
|
||||
LOG("bad config: %u+%u buffers for %d fds, %ux%u stride %u", c.inputs, c.outputs, nfds, c.width, c.height,
|
||||
c.stride);
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < nfds; i++) {
|
||||
bool input = (uint32_t)i < c.inputs;
|
||||
size_t size = input ? c.input_size : c.output_size;
|
||||
void *p = mmap(nullptr, size, input ? PROT_READ : PROT_READ | PROT_WRITE, MAP_SHARED, fds[i], 0);
|
||||
if (p == MAP_FAILED) {
|
||||
LOG("could not map buffer %d: %s", i, strerror(errno));
|
||||
release();
|
||||
return false;
|
||||
}
|
||||
(input ? inputs_ : outputs_).push_back({(uint8_t *)p, size});
|
||||
}
|
||||
codec_ = create(c, true);
|
||||
if (!codec_) codec_ = create(c, false);
|
||||
if (!codec_) {
|
||||
LOG("no %s encoder for %ux%u", c.codec == VENC_HEVC ? "HEVC" : "H.264", c.width, c.height);
|
||||
release();
|
||||
return false;
|
||||
}
|
||||
config_ = c;
|
||||
dst_stride_ = (int32_t)c.width;
|
||||
dst_rows_ = (int32_t)c.height;
|
||||
using InputFormat = AMediaFormat *(*)(AMediaCodec *);
|
||||
static auto input_format = (InputFormat)dlsym(RTLD_DEFAULT, "AMediaCodec_getInputFormat");
|
||||
if (AMediaFormat *in = input_format ? input_format(codec_) : nullptr) {
|
||||
int32_t v;
|
||||
if (AMediaFormat_getInt32(in, "stride", &v) && v >= (int32_t)c.width) dst_stride_ = v;
|
||||
if (AMediaFormat_getInt32(in, "slice-height", &v) && v >= (int32_t)c.height) dst_rows_ = v;
|
||||
AMediaFormat_delete(in);
|
||||
}
|
||||
LOG("%s %ux%u at %.2f fps, %u kbit/s %s, input stride %d rows %d", c.codec == VENC_HEVC ? "HEVC" : "H.264",
|
||||
c.width, c.height, c.fps_num ? (double)c.fps_den / c.fps_num : 0.0, c.bitrate / 1000,
|
||||
c.vbr ? "VBR" : "CBR", dst_stride_, dst_rows_);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void touch(const venc_touch &t) {
|
||||
using SendTouch = void (*)(int, int, int, int);
|
||||
static SendTouch send_touch = [] {
|
||||
void *wayland = dlopen("libdroiddeckwayland.so", RTLD_NOW | RTLD_NOLOAD);
|
||||
auto fn = wayland ? (SendTouch)dlsym(wayland, "droiddeck_wayland_send_touch") : nullptr;
|
||||
if (!fn) LOG("remote touch unavailable: %s", dlerror());
|
||||
return fn;
|
||||
}();
|
||||
if (!send_touch || t.action > VENC_TOUCH_UP) return;
|
||||
int x = (int)((uint64_t)std::min(t.x, 65535u) * (kTouchWidth - 1) / 65535);
|
||||
int y = (int)((uint64_t)std::min(t.y, 65535u) * (kTouchHeight - 1) / 65535);
|
||||
send_touch((int)t.action, kRemoteTouchId, x, y);
|
||||
}
|
||||
|
||||
void set_bitrate(uint32_t bitrate) {
|
||||
if (!codec_) return;
|
||||
AMediaFormat *p = AMediaFormat_new();
|
||||
AMediaFormat_setInt32(p, "video-bitrate", (int32_t)std::max(bitrate, 500000u));
|
||||
AMediaCodec_setParameters(codec_, p);
|
||||
AMediaFormat_delete(p);
|
||||
}
|
||||
|
||||
bool encode(const venc_frame &f, uint32_t *bytes, bool *key) {
|
||||
if (!codec_ || f.input >= inputs_.size() || f.output >= outputs_.size()) return false;
|
||||
if (f.keyframe) {
|
||||
AMediaFormat *p = AMediaFormat_new();
|
||||
AMediaFormat_setInt32(p, "request-sync", 0);
|
||||
AMediaCodec_setParameters(codec_, p);
|
||||
AMediaFormat_delete(p);
|
||||
}
|
||||
ssize_t in = AMediaCodec_dequeueInputBuffer(codec_, 100000);
|
||||
if (in < 0) return false;
|
||||
size_t capacity = 0;
|
||||
uint8_t *dst = AMediaCodec_getInputBuffer(codec_, (size_t)in, &capacity);
|
||||
size_t need = (size_t)dst_stride_ * dst_rows_ + (size_t)dst_stride_ * ((config_.height + 1) / 2);
|
||||
if (!dst || capacity < need) return false;
|
||||
const uint8_t *src = inputs_[f.input].data;
|
||||
for (uint32_t y = 0; y < config_.height; y++)
|
||||
memcpy(dst + (size_t)y * dst_stride_, src + (size_t)y * config_.stride, config_.width);
|
||||
const uint8_t *src_uv = src + (size_t)config_.stride * config_.rows;
|
||||
uint8_t *dst_uv = dst + (size_t)dst_stride_ * dst_rows_;
|
||||
for (uint32_t y = 0; y < (config_.height + 1) / 2; y++)
|
||||
memcpy(dst_uv + (size_t)y * dst_stride_, src_uv + (size_t)y * config_.stride, config_.width);
|
||||
if (AMediaCodec_queueInputBuffer(codec_, (size_t)in, 0, need, (uint64_t)std::max<int64_t>(f.pts_us, 0), 0) !=
|
||||
AMEDIA_OK)
|
||||
return false;
|
||||
Mapping &out = outputs_[f.output];
|
||||
for (int tries = 0; tries < 20; tries++) {
|
||||
AMediaCodecBufferInfo info;
|
||||
ssize_t o = AMediaCodec_dequeueOutputBuffer(codec_, &info, 20000);
|
||||
if (o == AMEDIACODEC_INFO_TRY_AGAIN_LATER || o == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED ||
|
||||
o == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED)
|
||||
continue;
|
||||
if (o < 0) return false;
|
||||
size_t size = 0;
|
||||
uint8_t *data = AMediaCodec_getOutputBuffer(codec_, (size_t)o, &size);
|
||||
const uint8_t *payload = data ? data + info.offset : nullptr;
|
||||
size_t length = data ? (size_t)info.size : 0;
|
||||
if (info.flags & kBufferFlagCodecConfig) {
|
||||
header_.assign(payload, payload + length);
|
||||
AMediaCodec_releaseOutputBuffer(codec_, (size_t)o, false);
|
||||
continue;
|
||||
}
|
||||
*key = (info.flags & kBufferFlagKeyFrame) != 0;
|
||||
bool prepend = *key && !header_.empty() &&
|
||||
(length < header_.size() || memcmp(payload, header_.data(), header_.size()) != 0);
|
||||
size_t total = length + (prepend ? header_.size() : 0);
|
||||
bool fits = payload && total <= out.size;
|
||||
if (fits) {
|
||||
if (prepend) memcpy(out.data, header_.data(), header_.size());
|
||||
memcpy(out.data + (prepend ? header_.size() : 0), payload, length);
|
||||
*bytes = (uint32_t)total;
|
||||
}
|
||||
AMediaCodec_releaseOutputBuffer(codec_, (size_t)o, false);
|
||||
return fits;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int fd_;
|
||||
AMediaCodec *codec_ = nullptr;
|
||||
venc_config config_ = {};
|
||||
int32_t dst_stride_ = 0, dst_rows_ = 0;
|
||||
std::vector<Mapping> inputs_, outputs_;
|
||||
std::vector<uint8_t> header_;
|
||||
};
|
||||
|
||||
std::atomic<int> g_listener{-1};
|
||||
|
||||
void serve(int listener) {
|
||||
for (;;) {
|
||||
int fd = accept4(listener, nullptr, nullptr, SOCK_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
return;
|
||||
}
|
||||
std::thread([fd] {
|
||||
Session s(fd);
|
||||
s.run();
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_com_droiddeck_launcher_session_VideoEncodeBridge_nativeStart(JNIEnv *env, jobject, jstring jpath) {
|
||||
if (g_listener.load() >= 0) return JNI_TRUE;
|
||||
const char *path = env->GetStringUTFChars(jpath, nullptr);
|
||||
sockaddr_un addr = {};
|
||||
addr.sun_family = AF_UNIX;
|
||||
bool fits = strlen(path) < sizeof addr.sun_path;
|
||||
if (fits) strcpy(addr.sun_path, path);
|
||||
int fd = fits ? socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0) : -1;
|
||||
if (fd >= 0) {
|
||||
unlink(path);
|
||||
if (bind(fd, (sockaddr *)&addr, sizeof addr) != 0 || listen(fd, 4) != 0) {
|
||||
LOG("could not listen on %s: %s", path, strerror(errno));
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
}
|
||||
env->ReleaseStringUTFChars(jpath, path);
|
||||
if (fd < 0) return JNI_FALSE;
|
||||
g_listener.store(fd);
|
||||
std::thread(serve, fd).detach();
|
||||
return JNI_TRUE;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_session_VideoEncodeBridge_nativeStop(JNIEnv *, jobject) {
|
||||
int fd = g_listener.exchange(-1);
|
||||
if (fd >= 0) {
|
||||
shutdown(fd, SHUT_RDWR);
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,10 @@ target_include_directories(droiddeckwayland PRIVATE
|
||||
)
|
||||
|
||||
set(DROIDDECK_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg)
|
||||
set(DROIDDECK_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/winfg)
|
||||
set(DROIDDECK_FRAMEGEN_DDFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/ddfg)
|
||||
if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
AND EXISTS ${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp)
|
||||
message(STATUS "droiddeckwayland: frame-generation engines built in (LSFG Native, Win-FG Native)")
|
||||
AND EXISTS ${DROIDDECK_FRAMEGEN_DDFG_DIR}/ddfg_engine.cpp)
|
||||
message(STATUS "droiddeckwayland: frame-generation engines built in (LSFG Native, DD-FG Native)")
|
||||
target_sources(droiddeckwayland PRIVATE
|
||||
src/framegen_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_probe.cpp
|
||||
@@ -68,9 +68,9 @@ if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_governor.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_jni.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/framegen.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_vkd.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_DDFG_DIR}/framegen.cpp
|
||||
${DROIDDECK_FRAMEGEN_DDFG_DIR}/ddfg_vkd.cpp
|
||||
${DROIDDECK_FRAMEGEN_DDFG_DIR}/ddfg_engine.cpp
|
||||
)
|
||||
target_include_directories(droiddeckwayland PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
@@ -79,8 +79,8 @@ if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
target_compile_options(droiddeckwayland PRIVATE
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17 -fvisibility=hidden>)
|
||||
target_compile_definitions(droiddeckwayland PRIVATE
|
||||
WINFG_UPSTREAM="${WINFG_UPSTREAM}"
|
||||
WINFG_CHAIN_HASH="${WINFG_CHAIN_HASH}"
|
||||
DDFG_UPSTREAM="${DDFG_UPSTREAM}"
|
||||
DDFG_CHAIN_HASH="${DDFG_CHAIN_HASH}"
|
||||
)
|
||||
target_link_libraries(droiddeckwayland dxbc atomic)
|
||||
else()
|
||||
|
||||
@@ -228,6 +228,8 @@ struct surface {
|
||||
enum surface_role role;
|
||||
struct wl_resource *idle_buffer;
|
||||
struct wl_listener idle_buffer_destroy;
|
||||
uint32_t *idle_px;
|
||||
int idle_w, idle_h;
|
||||
struct wl_resource *xdg_surface, *xdg_toplevel;
|
||||
struct wl_resource *viewport;
|
||||
|
||||
@@ -979,8 +981,30 @@ static void cursor_publish_buffer(struct surface *s, struct wl_resource *buffer)
|
||||
g_cursor_shown = s;
|
||||
}
|
||||
|
||||
static void surface_forget_idle_px(struct surface *s) {
|
||||
free(s->idle_px);
|
||||
s->idle_px = NULL;
|
||||
s->idle_w = s->idle_h = 0;
|
||||
}
|
||||
|
||||
static void on_idle_buffer_destroyed(struct wl_listener *l, void *data) {
|
||||
struct surface *s = wl_container_of(l, s, idle_buffer_destroy);
|
||||
struct wl_shm_buffer *shm = wl_shm_buffer_get(s->idle_buffer);
|
||||
if (shm) {
|
||||
int32_t w = wl_shm_buffer_get_width(shm), h = wl_shm_buffer_get_height(shm);
|
||||
int32_t stride = wl_shm_buffer_get_stride(shm);
|
||||
uint32_t *px = w > 0 && h > 0 && (int64_t)w * h <= CURSOR_MAX_PX && (int64_t)stride >= (int64_t)w * 4
|
||||
? realloc(s->idle_px, (size_t)w * h * 4) : NULL;
|
||||
if (px) {
|
||||
wl_shm_buffer_begin_access(shm);
|
||||
const uint8_t *src = wl_shm_buffer_get_data(shm);
|
||||
for (int y = 0; y < h; y++) memcpy(&px[(size_t)y * w], src + (size_t)y * stride, (size_t)w * 4);
|
||||
wl_shm_buffer_end_access(shm);
|
||||
s->idle_px = px;
|
||||
s->idle_w = w;
|
||||
s->idle_h = h;
|
||||
}
|
||||
}
|
||||
wl_list_remove(&s->idle_buffer_destroy.link);
|
||||
wl_list_init(&s->idle_buffer_destroy.link);
|
||||
s->idle_buffer = NULL;
|
||||
@@ -1007,6 +1031,7 @@ static struct wl_resource *surface_take_idle(struct surface *s) {
|
||||
}
|
||||
|
||||
static void surface_drop_idle(struct surface *s) {
|
||||
surface_forget_idle_px(s);
|
||||
struct wl_resource *buffer = surface_take_idle(s);
|
||||
if (buffer) send_buffer_release(buffer);
|
||||
}
|
||||
@@ -1136,6 +1161,7 @@ static void surface_resource_destroy(struct wl_resource *r) {
|
||||
if (g_cursor_surface == s) { g_cursor_surface = NULL; cursor_publish_hidden(); }
|
||||
if (g_cursor_shown == s) g_cursor_shown = NULL;
|
||||
surface_take_idle(s);
|
||||
surface_forget_idle_px(s);
|
||||
if (g_grab == s) g_grab = NULL;
|
||||
if (g_key_target == s) g_key_target = NULL;
|
||||
if (g_ime_click == s) g_ime_click = NULL;
|
||||
@@ -2123,6 +2149,9 @@ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint3
|
||||
send_buffer_release(buffer);
|
||||
} else if (g_cursor_shown == s) {
|
||||
cursor_publish_shown();
|
||||
} else if (s->idle_px) {
|
||||
cursor_publish_pixels((const uint8_t *)s->idle_px, s->idle_w, s->idle_h, (size_t)s->idle_w * 4, hx, hy);
|
||||
g_cursor_shown = s;
|
||||
}
|
||||
}
|
||||
static void pointer_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
|
||||
@@ -2674,7 +2703,11 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int
|
||||
uint32_t t = now_ms();
|
||||
|
||||
pointer_focus(target->resource, fx, fy);
|
||||
if (!(k && k->is_lock))
|
||||
// A button-only input must not replay a cached absolute position. A game
|
||||
// may have warped its cursor since the last motion (before taking lock,
|
||||
// or while releasing it), so that replay can become an unintended pan.
|
||||
// Absolute touch events still carry their position, including on clicks.
|
||||
if (!(k && k->is_lock) && (!relative || dx != 0 || dy != 0))
|
||||
for_each_pointer_of(client, sp) wl_pointer_send_motion(sp->ptr, t, fx, fy);
|
||||
send_relative_motion(client, dx, dy);
|
||||
for_each_pointer_of(client, sp) {
|
||||
|
||||
@@ -86,7 +86,7 @@ static int64_t now_ns(void) {
|
||||
|
||||
|
||||
void vkp_framegen_set_engine(int kind) {
|
||||
if (kind != VKP_FG_ENGINE_LSFG && kind != VKP_FG_ENGINE_WINFG) kind = VKP_FG_ENGINE_LSFG;
|
||||
if (kind != VKP_FG_ENGINE_LSFG && kind != VKP_FG_ENGINE_DDFG) kind = VKP_FG_ENGINE_LSFG;
|
||||
atomic_store(&g_kind, kind);
|
||||
atomic_store(&g_failed_kind, -1);
|
||||
atomic_store(&g_cfg_dirty, 1);
|
||||
@@ -125,7 +125,7 @@ void vkp_framegen_set_tuning(float flow_scale, float refresh_hz) {
|
||||
atomic_store(&g_cfg_dirty, 1);
|
||||
}
|
||||
|
||||
void vkp_framegen_set_winfg_tuning(int model, int perf_preset) {
|
||||
void vkp_framegen_set_ddfg_tuning(int model, int perf_preset) {
|
||||
atomic_store(&g_model, model == 3 ? 3 : 4);
|
||||
atomic_store(&g_preset, perf_preset < 0 ? 0 : perf_preset > 2 ? 2 : perf_preset);
|
||||
atomic_store(&g_cfg_dirty, 1);
|
||||
@@ -180,9 +180,9 @@ void vkp_framegen_device_ready(VkDevice dev, VkQueue queue, uint32_t qfam, int f
|
||||
g_vk.GetPhysicalDeviceMemoryProperties(g_pd, &g_memprops);
|
||||
fge_device_ready(g_gipa, g_inst, g_pd, dev, queue, qfam, features_enabled);
|
||||
g_dev_ready = vk.CreateImage && vk.CreateImageView && vk.CmdPipelineBarrier;
|
||||
FGLOG("engines ready on the compositor's device: LSFG Native %s (%s), Win-FG Native %s",
|
||||
FGLOG("engines ready on the compositor's device: LSFG Native %s (%s), DD-FG Native %s",
|
||||
fge_caps_ok(VKP_FG_ENGINE_LSFG) ? "available" : "unavailable", fge_caps_reason(),
|
||||
fge_caps_ok(VKP_FG_ENGINE_WINFG) ? "available" : "unavailable");
|
||||
fge_caps_ok(VKP_FG_ENGINE_DDFG) ? "available" : "unavailable");
|
||||
}
|
||||
|
||||
void vkp_framegen_device_lost(void) { g_dev_lost = 1; }
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
#endif
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
enum { VKP_FG_ENGINE_LSFG = 0, VKP_FG_ENGINE_WINFG = 1 };
|
||||
enum { VKP_FG_ENGINE_LSFG = 0, VKP_FG_ENGINE_DDFG = 1 };
|
||||
#define VKP_FG_MAX_GENERATIONS 3
|
||||
|
||||
void vkp_framegen_set_engine(int kind);
|
||||
void vkp_framegen_set_armed(int armed, int multiplier, int target_fps);
|
||||
void vkp_framegen_set_lsfg_cache_path(const char *path);
|
||||
void vkp_framegen_set_tuning(float flow_scale, float refresh_hz);
|
||||
void vkp_framegen_set_winfg_tuning(int model, int perf_preset);
|
||||
void vkp_framegen_set_ddfg_tuning(int model, int perf_preset);
|
||||
int vkp_framegen_problem(void);
|
||||
const char *vkp_framegen_caps_reason(void);
|
||||
void vkp_framegen_stats(float out[6]);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "lsfg/lsfg_engine.h"
|
||||
#include "lsfg/lsfg_probe.h"
|
||||
#include "lsfg/lsfg_vkd.h"
|
||||
#include "winfg/winfg_engine.h"
|
||||
#include "ddfg/ddfg_engine.h"
|
||||
|
||||
#define ELOG(...) __android_log_print(ANDROID_LOG_INFO, "DroidDeckWayland", "framegen: " __VA_ARGS__)
|
||||
#define ELOGE(...) __android_log_print(ANDROID_LOG_ERROR, "DroidDeckWayland", "framegen: " __VA_ARGS__)
|
||||
@@ -30,7 +30,7 @@ uint32_t g_qfam = 0;
|
||||
bool g_probed = false, g_ready = false;
|
||||
|
||||
std::unique_ptr<lsfg::Engine> g_lsfg;
|
||||
std::unique_ptr<winfg::Engine> g_winfg;
|
||||
std::unique_ptr<ddfg::Engine> g_ddfg;
|
||||
int g_kind = -1;
|
||||
char g_build[96] = "";
|
||||
|
||||
@@ -143,15 +143,15 @@ int fge_start(int kind, const char *cache_path) {
|
||||
fge_stop();
|
||||
if (!g_ready) return -1;
|
||||
if (kind == 1) {
|
||||
auto e = std::make_unique<winfg::Engine>();
|
||||
auto e = std::make_unique<ddfg::Engine>();
|
||||
if (!e->init(g_table, g_pd, g_dev, g_qfam, g_queue)) {
|
||||
ELOGE("winfg-native: engine init failed");
|
||||
ELOGE("ddfg-native: engine init failed");
|
||||
return -1;
|
||||
}
|
||||
g_winfg = std::move(e);
|
||||
g_ddfg = std::move(e);
|
||||
g_kind = 1;
|
||||
#if defined(WINFG_UPSTREAM) && defined(WINFG_CHAIN_HASH)
|
||||
std::snprintf(g_build, sizeof(g_build), "chain %s, src %s", WINFG_UPSTREAM, WINFG_CHAIN_HASH);
|
||||
#if defined(DDFG_UPSTREAM) && defined(DDFG_CHAIN_HASH)
|
||||
std::snprintf(g_build, sizeof(g_build), "chain %s, src %s", DDFG_UPSTREAM, DDFG_CHAIN_HASH);
|
||||
#else
|
||||
std::snprintf(g_build, sizeof(g_build), "embedded chain");
|
||||
#endif
|
||||
@@ -176,46 +176,46 @@ int fge_start(int kind, const char *cache_path) {
|
||||
}
|
||||
|
||||
void fge_stop(void) {
|
||||
if ((g_lsfg || g_winfg) && g_table.DeviceWaitIdle && g_dev) g_table.DeviceWaitIdle(g_dev);
|
||||
if ((g_lsfg || g_ddfg) && g_table.DeviceWaitIdle && g_dev) g_table.DeviceWaitIdle(g_dev);
|
||||
g_lsfg.reset();
|
||||
g_winfg.reset();
|
||||
g_ddfg.reset();
|
||||
g_kind = -1;
|
||||
}
|
||||
|
||||
int fge_unavailable(void) {
|
||||
if (g_kind == 1) return g_winfg && g_winfg->unavailable() ? 1 : 0;
|
||||
if (g_kind == 1) return g_ddfg && g_ddfg->unavailable() ? 1 : 0;
|
||||
return g_lsfg && g_lsfg->unavailable() ? 1 : 0;
|
||||
}
|
||||
|
||||
void fge_configure(uint32_t multiplier, uint32_t target_fps, float flow_scale, float refresh_hz,
|
||||
int model, int perf_preset) {
|
||||
if (multiplier < 2) multiplier = 2;
|
||||
if (g_kind == 1 && g_winfg) g_winfg->configure(multiplier, model, perf_preset, flow_scale);
|
||||
if (g_kind == 1 && g_ddfg) g_ddfg->configure(multiplier, model, perf_preset, flow_scale);
|
||||
else if (g_lsfg) g_lsfg->configure(multiplier, target_fps, flow_scale, refresh_hz);
|
||||
}
|
||||
|
||||
int fge_prepare(uint32_t w, uint32_t h, VkFormat fmt) {
|
||||
if (g_kind == 1) return g_winfg && g_winfg->prepare(w, h, fmt) ? 1 : 0;
|
||||
if (g_kind == 1) return g_ddfg && g_ddfg->prepare(w, h, fmt) ? 1 : 0;
|
||||
if (!g_lsfg) return 0;
|
||||
g_lsfg->setGuestExtent(w, h);
|
||||
return g_lsfg->prepare(w, h, fmt) ? 1 : 0;
|
||||
}
|
||||
|
||||
uint32_t fge_plan(uint32_t capacity, uint64_t source_frames, float presented_rate) {
|
||||
if (g_kind == 1) return g_winfg ? g_winfg->plan(capacity) : 0;
|
||||
if (g_kind == 1) return g_ddfg ? g_ddfg->plan(capacity) : 0;
|
||||
if (!g_lsfg) return 0;
|
||||
g_lsfg->setPresentedRate(presented_rate);
|
||||
return g_lsfg->plan(capacity, source_frames);
|
||||
}
|
||||
|
||||
void fge_process(VkCommandBuffer cmd, VkImage source, uint32_t w, uint32_t h, uint32_t gens) {
|
||||
if (g_kind == 1) { if (g_winfg) g_winfg->process(cmd, source, w, h, gens); return; }
|
||||
if (g_kind == 1) { if (g_ddfg) g_ddfg->process(cmd, source, w, h, gens); return; }
|
||||
if (g_lsfg) g_lsfg->process(cmd, source, w, h, gens);
|
||||
}
|
||||
|
||||
void fge_generate(VkCommandBuffer cmd, uint32_t g, uint32_t count, VkImage img, VkImageView view,
|
||||
uint32_t w, uint32_t h) {
|
||||
if (g_kind == 1) { if (g_winfg) g_winfg->generateInto(cmd, g, count, img, view, w, h); return; }
|
||||
if (g_kind == 1) { if (g_ddfg) g_ddfg->generateInto(cmd, g, count, img, view, w, h); return; }
|
||||
if (g_lsfg) g_lsfg->generateInto(cmd, g, g, img, view, w, h);
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ void fge_telemetry(float *accepted, float *source_rate, float *thermal) {
|
||||
}
|
||||
}
|
||||
|
||||
const char *fge_engine_name(int kind) { return kind == 1 ? "Win-FG Native" : "LSFG Native"; }
|
||||
const char *fge_engine_name(int kind) { return kind == 1 ? "DD-FG Native" : "LSFG Native"; }
|
||||
const char *fge_build_info(void) { return g_build; }
|
||||
|
||||
}
|
||||
@@ -34,5 +34,5 @@ void fge_forget_targets(void) {}
|
||||
void fge_telemetry(float *accepted, float *source_rate, float *thermal) {
|
||||
*accepted = 0.0f; *source_rate = 0.0f; *thermal = -1.0f;
|
||||
}
|
||||
const char *fge_engine_name(int kind) { return kind == 1 ? "Win-FG Native" : "LSFG Native"; }
|
||||
const char *fge_engine_name(int kind) { return kind == 1 ? "DD-FG Native" : "LSFG Native"; }
|
||||
const char *fge_build_info(void) { return "stub"; }
|
||||
@@ -31,10 +31,10 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFrameGenTuning(JN
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetWinFgTuning(JNIEnv *env, jclass clazz,
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetDdFgTuning(JNIEnv *env, jclass clazz,
|
||||
jint model, jint perfPreset) {
|
||||
(void)env; (void)clazz;
|
||||
vkp_framegen_set_winfg_tuning((int)model, (int)perfPreset);
|
||||
vkp_framegen_set_ddfg_tuning((int)model, (int)perfPreset);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
|
||||
@@ -16,16 +16,22 @@ class App : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
com.droiddeck.launcher.ui.Themes.loadGradient(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
|
||||
if (process == "$packageName:xboxlogin") {
|
||||
if (android.os.Build.VERSION.SDK_INT >= 28) android.webkit.WebView.setDataDirectorySuffix("xbox")
|
||||
return
|
||||
}
|
||||
if (process == "$packageName:vr_recovery" || process == "$packageName:rgb_diagnostic") return
|
||||
CrashHandler.install(this)
|
||||
com.droiddeck.launcher.session.QuestExperimental.initialize(this)
|
||||
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
|
||||
com.droiddeck.launcher.update.AppUpdates.init(this)
|
||||
com.droiddeck.launcher.core.DownloadSource.init(this)
|
||||
prefetchAddonPacks()
|
||||
GpuClockPin.clearLeftover(this)
|
||||
com.droiddeck.launcher.glasses.Glasses.init(this)
|
||||
com.droiddeck.launcher.glasses.GlassesMenuMirror.initialize(this)
|
||||
@@ -35,5 +41,24 @@ class App : Application() {
|
||||
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.xbox.XboxBackend)
|
||||
}
|
||||
|
||||
/** 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({
|
||||
runCatching {
|
||||
if (com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(app).isDirectory) {
|
||||
com.droiddeck.launcher.core.AddonPack.PROOT.ensure(app)
|
||||
com.droiddeck.launcher.core.AddonPack.DIRECTAUDIO.ensure(app)
|
||||
}
|
||||
if (com.droiddeck.launcher.stores.gog.GogComet.enabled(app)) com.droiddeck.launcher.core.AddonPack.COMET.ensure(app)
|
||||
if (com.droiddeck.launcher.stores.StoreAccounts.signedInAs(app, com.droiddeck.launcher.stores.Store.XBOX) != null)
|
||||
com.droiddeck.launcher.core.AddonPack.XBOX.ensure(app)
|
||||
com.droiddeck.launcher.handtracking.HandTracking.load(app)
|
||||
}.onFailure { android.util.Log.w("App", "add-on pack prefetch failed", it) }
|
||||
}, "addon-packs").start()
|
||||
}
|
||||
}
|
||||
@@ -11,29 +11,46 @@ import java.io.File
|
||||
|
||||
internal class DeckyMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) {
|
||||
var deckyInstalled by mutableStateOf<String?>(null)
|
||||
var deckyReleases by mutableStateOf<List<DeckyManager.Release>>(emptyList())
|
||||
var deckyLatest by mutableStateOf<DeckyManager.Release?>(null)
|
||||
var deckyBranch by mutableIntStateOf(DeckyManager.BRANCH_STABLE)
|
||||
private var deckyChannels = DeckyManager.ReleaseChannels(emptyList(), emptyList())
|
||||
var deckyChecking by mutableStateOf(false)
|
||||
var deckyStage by mutableStateOf<String?>(null)
|
||||
var deckyPercent by mutableIntStateOf(-1)
|
||||
var deckyDownloadingBinary by mutableStateOf(false)
|
||||
var deckySupervisor by mutableStateOf(false)
|
||||
var deckyPendingZip by mutableStateOf<File?>(null)
|
||||
var deckyReleaseRequest = 0
|
||||
|
||||
fun refreshDecky() {
|
||||
val request = ++deckyReleaseRequest
|
||||
deckyBranch = DeckyManager.branch(activity)
|
||||
deckyChecking = true
|
||||
Thread({
|
||||
val channels = runCatching { DeckyManager.releaseChannels(activity) }
|
||||
.getOrElse { DeckyManager.ReleaseChannels(emptyList(), emptyList()) }
|
||||
ui.post {
|
||||
if (request == deckyReleaseRequest) {
|
||||
deckyReleases = channels.stable.ifEmpty { channels.prerelease }
|
||||
deckyChannels = channels
|
||||
deckyLatest = DeckyManager.latest(channels, deckyBranch)
|
||||
deckyChecking = false
|
||||
}
|
||||
}
|
||||
}, "decky-releases").start()
|
||||
}
|
||||
|
||||
fun switchDeckyBranch(branch: Int) {
|
||||
if (branch == deckyBranch || deckyStage != null || SessionState.running) return
|
||||
val saved = runCatching { DeckyManager.setBranch(activity, branch) }
|
||||
if (saved.isFailure) {
|
||||
android.widget.Toast.makeText(activity, R.string.decky_channel_failed, android.widget.Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
deckyBranch = branch
|
||||
deckyLatest = DeckyManager.latest(deckyChannels, branch)
|
||||
refreshDecky()
|
||||
}
|
||||
|
||||
fun installDecky(release: DeckyManager.Release) {
|
||||
if (deckyStage != null || SessionState.running) return
|
||||
deckyStage = activity.getString(R.string.store_starting); deckyPercent = -1
|
||||
@@ -49,6 +66,17 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
}
|
||||
|
||||
fun importPluginZip(archive: File) {
|
||||
if (deckyStage != null || SessionState.running || deckyInstalled == null) return
|
||||
deckyPendingZip = archive
|
||||
}
|
||||
|
||||
fun cancelPluginZip() {
|
||||
deckyPendingZip = null
|
||||
}
|
||||
|
||||
fun confirmPluginZip() {
|
||||
val archive = deckyPendingZip ?: return
|
||||
deckyPendingZip = null
|
||||
if (deckyStage != null || SessionState.running || deckyInstalled == null) return
|
||||
deckyStage = activity.getString(R.string.decky_plugin_starting); deckyPercent = -1
|
||||
Thread({
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
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
|
||||
@@ -184,7 +185,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
|
||||
val recommended = if (QuestExperimental.isSupported()) null else DriverPairs.recommendedKey(gpu, pairs)
|
||||
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) }
|
||||
@@ -212,6 +213,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
}.sortedByDescending { it.recommended }
|
||||
}
|
||||
|
||||
private fun recommendedKey(pairs: List<DriverPairs.DriverPair>) = DriverPairs.recommendedKey(
|
||||
gpu, pairs, TurnipReleases.autoPicks(activity), AutoDriverTrial.bad(activity),
|
||||
blob = AutoPicks.blobVersion(com.droiddeck.launcher.gpu.VulkanInfo.cachedOrNull()?.get("driver_version")),
|
||||
)
|
||||
|
||||
fun chooseMode(next: String) {
|
||||
pairModeOpen = next == GPU_MODE_PAIR
|
||||
setMode(auto = next == GPU_MODE_AUTO, mix = next == GPU_MODE_MIX)
|
||||
@@ -314,12 +320,13 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
Thread({
|
||||
val problem = if (force || releasesStale()) runCatching { TurnipReleases.refresh(activity) }.exceptionOrNull()?.message else null
|
||||
val holdBack = !force && com.droiddeck.launcher.frontend.AutoComponents.constrained(activity)
|
||||
runCatching { com.droiddeck.launcher.gpu.VulkanInfo.query() }
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
|
||||
val key = DriverPairs.recommendedKey(gpu, pairs)
|
||||
val key = recommendedKey(pairs)
|
||||
val pair = pairs.firstOrNull { it.key == key }
|
||||
val row = pairRows.firstOrNull { it.key == key }
|
||||
when {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.droiddeck.launcher
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
|
||||
object LauncherIcon {
|
||||
const val DARK = "dark"
|
||||
const val LIGHT = "light"
|
||||
|
||||
private val aliases = linkedMapOf(DARK to "com.droiddeck.launcher.LauncherDark", LIGHT to "com.droiddeck.launcher.LauncherLight")
|
||||
|
||||
val options: List<Pair<String, Int>> = listOf(
|
||||
DARK to R.string.setup_launcher_icon_dark,
|
||||
LIGHT to R.string.setup_launcher_icon_light,
|
||||
)
|
||||
|
||||
private fun component(context: Context, style: String) =
|
||||
ComponentName(context, aliases.getValue(style))
|
||||
|
||||
private fun enabled(context: Context, style: String): Boolean =
|
||||
when (context.packageManager.getComponentEnabledSetting(component(context, style))) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> style == DARK
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun current(context: Context): String = aliases.keys.firstOrNull { enabled(context, it) } ?: DARK
|
||||
|
||||
fun activity(context: Context): ComponentName = component(context, current(context))
|
||||
|
||||
fun set(context: Context, style: String) {
|
||||
if (style !in aliases || style == current(context)) return
|
||||
val pm = context.packageManager
|
||||
pm.setComponentEnabledSetting(component(context, style),
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
|
||||
aliases.keys.filter { it != style }.forEach {
|
||||
pm.setComponentEnabledSetting(component(context, it),
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var frameGenLabel by mutableStateOf("")
|
||||
private var lossless by mutableStateOf(Lossless.State.NONE)
|
||||
private var showRemove by mutableStateOf(false)
|
||||
private var showRuntimeActions by mutableStateOf<LinuxRuntimeInstaller.Release?>(null)
|
||||
private var pendingSwitch by mutableStateOf<Intent?>(null)
|
||||
private var switchingTo: Intent? = null
|
||||
private var switchPoll: Runnable? = null
|
||||
@@ -118,11 +119,13 @@ class MainActivity : ComponentActivity() {
|
||||
private var showNonAdreno by mutableStateOf<LinuxRuntimeInstaller.Release?>(null)
|
||||
private var glThread by mutableStateOf(true)
|
||||
private var noGlError by mutableStateOf(true)
|
||||
private var returnAfterGame by mutableStateOf(true)
|
||||
private var steamRepairQueued by mutableStateOf(false)
|
||||
private var mangoapp by mutableStateOf(true)
|
||||
private var steamController by mutableStateOf(SessionPrefs.CONTROLLER_DECK)
|
||||
private var showComponents by mutableStateOf(false)
|
||||
private var focusComponentsContent by mutableStateOf(true)
|
||||
private var componentsGameKey by mutableStateOf<String?>(null)
|
||||
private var showMapping by mutableStateOf(false)
|
||||
private var saveBusy: String? = null
|
||||
private var onSavePicked: ((File) -> Unit)? = null
|
||||
@@ -163,12 +166,13 @@ class MainActivity : ComponentActivity() {
|
||||
private var directAudio by mutableStateOf(false)
|
||||
private var picturePosition by mutableStateOf("center")
|
||||
private var stretch16x9 by mutableStateOf(false)
|
||||
private var orientation by mutableStateOf(android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE)
|
||||
private var orientation by mutableStateOf(android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
|
||||
private var launcherFullscreen by mutableStateOf(true)
|
||||
private var downloadSource by mutableStateOf(DownloadSource.MODE_AUTO)
|
||||
private var downloadMirror by mutableStateOf("")
|
||||
private var downloadPrefix by mutableStateOf("")
|
||||
private var animationsEnabled by mutableStateOf(true)
|
||||
private var launcherIcon by mutableStateOf(LauncherIcon.DARK)
|
||||
private var gameStoresEnabled by mutableStateOf(true)
|
||||
private var gameStoresSpeedTier by mutableStateOf("fast")
|
||||
private var storesOpenTab by mutableStateOf(SessionPrefs.STORES_OPEN_LIBRARY)
|
||||
@@ -493,6 +497,20 @@ class MainActivity : ComponentActivity() {
|
||||
pickSaveDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.game_file_folder), gameSyncFolder))
|
||||
}
|
||||
|
||||
private fun switchLauncherIcon(style: String): Boolean {
|
||||
val before = LauncherIcon.current(this)
|
||||
LauncherIcon.set(this, style)
|
||||
launcherIcon = LauncherIcon.current(this)
|
||||
if (launcherIcon == before) return false
|
||||
// Android finishes activities started through the alias that was just disabled.
|
||||
startActivity(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
.setComponent(LauncherIcon.activity(this))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
.putExtra(com.droiddeck.launcher.stores.download.StoreDownloadService.EXTRA_NAV, "setup:launcher"))
|
||||
finishAndRemoveTask()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun readGameIntent(request: Intent) {
|
||||
request.getStringExtra(com.droiddeck.launcher.stores.download.StoreDownloadService.EXTRA_NAV)?.let { navRequest = it; request.removeExtra(com.droiddeck.launcher.stores.download.StoreDownloadService.EXTRA_NAV) }
|
||||
shortcutPicker = request.action == Intent.ACTION_CREATE_SHORTCUT
|
||||
@@ -748,6 +766,7 @@ class MainActivity : ComponentActivity() {
|
||||
downloadPrefix = DownloadSource.custom(this)
|
||||
DownloadSource.probe()
|
||||
animationsEnabled = SessionPrefs.animationsEnabled(this)
|
||||
launcherIcon = LauncherIcon.current(this)
|
||||
gameStoresEnabled = SessionPrefs.gameStoresEnabled(this)
|
||||
gameStoresSpeedTier = SessionPrefs.gameStoresSpeedTier(this)
|
||||
storesOpenTab = SessionPrefs.storesOpenTab(this)
|
||||
@@ -759,6 +778,9 @@ class MainActivity : ComponentActivity() {
|
||||
setContent {
|
||||
DroidDeckTheme(theme, appScale = appScale) {
|
||||
com.droiddeck.launcher.ui.FocusGlideHost(androidx.compose.ui.Modifier.fillMaxSize().exposeTestTags()) {
|
||||
if (decky.deckyPendingZip != null) com.droiddeck.launcher.ui.DeckyZipWarningDialog(
|
||||
onConfirm = { decky.confirmPluginZip() }, onCancel = { decky.cancelPluginZip() },
|
||||
)
|
||||
val page: (@Composable () -> Unit)? = when {
|
||||
showPerformance -> { { PerformanceHost() } }
|
||||
showComponents -> { { ComponentsHost() } }
|
||||
@@ -810,6 +832,7 @@ class MainActivity : ComponentActivity() {
|
||||
downloadMirror = downloadMirror,
|
||||
downloadPrefix = downloadPrefix,
|
||||
animationsEnabled = animationsEnabled,
|
||||
launcherIcon = launcherIcon,
|
||||
gameStoresEnabled = gameStoresEnabled,
|
||||
gameStoresSpeedTier = gameStoresSpeedTier,
|
||||
storesOpenTab = storesOpenTab,
|
||||
@@ -853,6 +876,7 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
onSteamGameArtChanged = { appId -> refreshSteamGameArt(appId) },
|
||||
onAddedGameRemoved = { folder, appId, name -> removeAddedGame(folder, appId, name) },
|
||||
onGameRemoved = { g -> removeGame(g) },
|
||||
onAddedGamesArt = { on -> SessionPrefs.setAddedGamesArt(this, on); addedGamesArt = on; if (on) refreshAddedGames() },
|
||||
onSgdbKey = { key ->
|
||||
Thread({
|
||||
@@ -925,6 +949,7 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
onProtons = { openProtons() },
|
||||
onComponents = { focusContent -> openComponents(focusContent) },
|
||||
onGameComponents = { game -> openComponents(gameKey = game.profileKey) },
|
||||
onComponentsTab = { tab -> openComponents(focusContent = true, tab = tab) },
|
||||
onSaveImport = { sg -> importSaves(sg.name) { GameSaves.game(sg) } },
|
||||
onSaveExport = { sg, layout -> exportSaves(sg.name, layout) { GameSaves.game(sg) } },
|
||||
@@ -982,6 +1007,7 @@ class MainActivity : ComponentActivity() {
|
||||
onPageBack = { showPerformance = false; showComponents = false; showMapping = false },
|
||||
onGameStoresEnabled = { on -> SessionPrefs.setGameStoresEnabled(this, on); gameStoresEnabled = on },
|
||||
onTheme = { id -> SessionPrefs.setTheme(this, id); theme = id },
|
||||
onLauncherIcon = { style -> switchLauncherIcon(style) },
|
||||
onLanguage = { tag ->
|
||||
if (tag != com.droiddeck.launcher.core.AppLanguage.chosen(this)) {
|
||||
com.droiddeck.launcher.core.AppLanguage.setChosen(this, tag)
|
||||
@@ -1136,6 +1162,13 @@ class MainActivity : ComponentActivity() {
|
||||
onDismiss = { pendingSwitch = null },
|
||||
)
|
||||
}
|
||||
showRuntimeActions?.let { release ->
|
||||
com.droiddeck.launcher.ui.RuntimeActionsDialog(
|
||||
onRepair = { showRuntimeActions = null; if (!runtimeChangesBlocked()) install(release) },
|
||||
onRemove = { showRuntimeActions = null; showRemove = true },
|
||||
onDismiss = { showRuntimeActions = null },
|
||||
)
|
||||
}
|
||||
if (showRemove) ConfirmDialog(
|
||||
title = androidx.compose.ui.res.stringResource(R.string.main_remove_runtime_title),
|
||||
text = androidx.compose.ui.res.stringResource(R.string.main_remove_runtime_text),
|
||||
@@ -1295,6 +1328,7 @@ class MainActivity : ComponentActivity() {
|
||||
syncLossless()
|
||||
updates.onResume()
|
||||
drivers.checkOnOpen()
|
||||
resumeInterruptedPackages()
|
||||
protons.checkUpdates()
|
||||
if (!SessionState.running) Thread({
|
||||
val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList())
|
||||
@@ -1439,8 +1473,9 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun openComponents(focusContent: Boolean = true, tab: String? = null) {
|
||||
private fun openComponents(focusContent: Boolean = true, tab: String? = null, gameKey: String? = null) {
|
||||
focusComponentsContent = focusContent
|
||||
componentsGameKey = gameKey
|
||||
if (tab != null) components.compComp = tab
|
||||
showPerformance = false
|
||||
showMapping = false
|
||||
@@ -1489,6 +1524,7 @@ class MainActivity : ComponentActivity() {
|
||||
onImport = { pickComponent.launch(InAppFilePicker.buildIntent(this, WCP_EXT, getString(R.string.comp_pick_wcp))) },
|
||||
onBack = { if (!com.droiddeck.launcher.ui.Hops.back { showComponents = false }) showComponents = false },
|
||||
games = steamGames,
|
||||
initialGameKey = componentsGameKey,
|
||||
generalFexPreset = fexPreset,
|
||||
onGeneralFexPreset = { id -> SessionPrefs.setFexPreset(this, id); fexPreset = id },
|
||||
gpu = drivers.state(),
|
||||
@@ -1549,6 +1585,27 @@ class MainActivity : ComponentActivity() {
|
||||
.putExtra(SessionService.EXTRA_PROGRAM, path))
|
||||
}
|
||||
|
||||
private fun resumeInterruptedPackages() {
|
||||
if (runtimeChangesBlocked()) return
|
||||
val first = DesktopCatalog.interrupted(this).firstOrNull()
|
||||
if (first == null) {
|
||||
Thread({ DesktopCatalog.pruneDownloads(this) }, "prune-pkg").start()
|
||||
return
|
||||
}
|
||||
pkgId = first.id; pkgStage = getString(R.string.deskpkg_installing, first.name); pkgPercent = -1
|
||||
Thread({
|
||||
val problem = DesktopCatalog.resumeInterrupted(this) { stage, percent ->
|
||||
ui.post { pkgStage = stage; pkgPercent = percent }
|
||||
}
|
||||
ui.post {
|
||||
pkgStage = null; pkgId = null
|
||||
if (problem != null) android.widget.Toast.makeText(this, getString(R.string.main_pkg_problem, first.name, problem), android.widget.Toast.LENGTH_LONG).show()
|
||||
refreshPackages()
|
||||
refresh()
|
||||
}
|
||||
}, "resume-pkg").start()
|
||||
}
|
||||
|
||||
private fun installPackage(id: String) {
|
||||
val entry = catalog?.firstOrNull { it.id == id } ?: return
|
||||
if (busy || LinuxRuntimeInstaller.isBusy() || pkgStage != null || SessionState.running) return
|
||||
@@ -1599,6 +1656,7 @@ class MainActivity : ComponentActivity() {
|
||||
pipAutoEnter = pipAutoEnter,
|
||||
unfoldedControls = if (mode == SessionService.MODE_STEAM &&
|
||||
com.droiddeck.launcher.session.SessionDisplay.foldable(this)) unfoldedControls else null,
|
||||
unfoldedControlsWithController = unfoldedControlsWithController,
|
||||
oscMode = if (mode == SessionService.MODE_STEAM) oscMode else null,
|
||||
backActionsInverted = backActionsInverted,
|
||||
directAudio = if (mode == SessionService.MODE_STEAM) directAudio else null,
|
||||
@@ -1612,6 +1670,7 @@ class MainActivity : ComponentActivity() {
|
||||
forceSsbs = forceSsbs,
|
||||
syncBackend = if (mode == SessionService.MODE_STEAM) SessionPrefs.syncBackendOf(fastSync, fsyncFirst, syncFallback) else null,
|
||||
steamClientArch = if (mode == SessionService.MODE_STEAM) steamClientArch else null,
|
||||
returnAfterGame = if (mode == SessionService.MODE_STEAM) returnAfterGame else null,
|
||||
steamRepairQueued = steamRepairQueued,
|
||||
mangoapp = mangoapp,
|
||||
steamController = if (mode == SessionService.MODE_STEAM) steamController else null,
|
||||
@@ -1627,7 +1686,8 @@ class MainActivity : ComponentActivity() {
|
||||
wifiDiscoveryAsked = wifiDiscoveryAsked,
|
||||
wifiDiscoveryBlocked = wifiDiscoveryBlocked,
|
||||
deckyInstalled = if (mode == SessionService.MODE_STEAM) decky.deckyInstalled else null,
|
||||
deckyLatestRelease = if (mode == SessionService.MODE_STEAM) decky.deckyReleases.firstOrNull() else null,
|
||||
deckyLatestRelease = if (mode == SessionService.MODE_STEAM) decky.deckyLatest else null,
|
||||
deckyBranch = decky.deckyBranch,
|
||||
deckyChecking = decky.deckyChecking, deckyStage = decky.deckyStage, deckyPercent = decky.deckyPercent,
|
||||
deckyDownloadingBinary = decky.deckyDownloadingBinary,
|
||||
deckyEnabled = decky.deckySupervisor, deckySessionRunning = SessionState.running,
|
||||
@@ -1663,6 +1723,9 @@ class MainActivity : ComponentActivity() {
|
||||
onZramCompression = { on -> SessionPrefs.setZramCompression(this, on); zramCompression = on },
|
||||
onPipAutoEnter = { on -> SessionPrefs.setPipAutoEnter(this, on); pipAutoEnter = on },
|
||||
onUnfoldedControls = { on -> SessionPrefs.setUnfoldedControls(this, on); unfoldedControls = on },
|
||||
onUnfoldedControlsWithController = { on ->
|
||||
SessionPrefs.setUnfoldedControlsWithController(this, on); unfoldedControlsWithController = on
|
||||
},
|
||||
onOsc = { o -> SessionPrefs.setOscMode(this, o); oscMode = o },
|
||||
onBackActionsInverted = { inverted ->
|
||||
SessionPrefs.setBackActionsInverted(this, inverted)
|
||||
@@ -1697,19 +1760,21 @@ class MainActivity : ComponentActivity() {
|
||||
onSteamRepair = { steamRepairQueued = SteamRepair.queue(this) },
|
||||
onSteamClientArch = { id -> SessionPrefs.setSteamClientArch(this, id); steamClientArch = id; refresh() },
|
||||
onMangoapp = { on -> SessionPrefs.setMangoapp(this, on); mangoapp = on },
|
||||
onReturnAfterGame = { on -> SessionPrefs.setReturnAfterGame(this, on); returnAfterGame = on },
|
||||
onSteamController = { id -> SessionPrefs.setSteamController(this, id); steamController = id },
|
||||
onRunSteamAtStartup = { on ->
|
||||
SessionPrefs.setRunSteamAtStartup(this, on)
|
||||
runSteamAtStartup = on
|
||||
},
|
||||
onBootMovie = { id -> SessionPrefs.setBootMovie(this, id); loadBootMovie() },
|
||||
onBootMovie = { id -> com.droiddeck.launcher.session.BootMovie.select(this, id); loadBootMovie() },
|
||||
onBootMovieSound = { on -> SessionPrefs.setBootMovieSound(this, on); bootMovieSound = on },
|
||||
onImportBootMovie = {
|
||||
pickBootMovie.launch(InAppFilePicker.buildIntent(this, com.droiddeck.launcher.session.BootMovie.PICKER_EXTENSIONS, getString(R.string.boot_movie_pick)))
|
||||
},
|
||||
onRemoveBootMovie = { com.droiddeck.launcher.session.BootMovie.removeCustom(this); loadBootMovie() },
|
||||
onBootMovieChanged = { loadBootMovie() },
|
||||
onDeckyInstall = { release -> decky.installDecky(release) },
|
||||
onDeckyCheck = { decky.refreshDecky() },
|
||||
onDeckyBranch = { branch -> decky.switchDeckyBranch(branch) },
|
||||
onDeckyEnabled = { enabled -> DeckyManager.setSupervisorEnabled(this, enabled); decky.deckySupervisor = enabled },
|
||||
onDeckyUninstall = {
|
||||
DeckyManager.uninstall(this, wipeData = false)
|
||||
@@ -1792,6 +1857,30 @@ class MainActivity : ComponentActivity() {
|
||||
}, "steam-art-update").start()
|
||||
}
|
||||
|
||||
private fun removeGame(g: Library.SteamGame) {
|
||||
val folder = g.gameFiles ?: return
|
||||
when (com.droiddeck.launcher.frontend.GameRemoval.kind(g)) {
|
||||
com.droiddeck.launcher.frontend.GameRemoval.Kind.LAUNCHER -> Unit
|
||||
com.droiddeck.launcher.frontend.GameRemoval.Kind.SHORTCUT -> removeAddedGame(folder.path, g.gameId, g.name)
|
||||
com.droiddeck.launcher.frontend.GameRemoval.Kind.STORE -> {
|
||||
val installed = com.droiddeck.launcher.frontend.GameRemoval.storeGame(g)
|
||||
if (installed != null) {
|
||||
com.droiddeck.launcher.stores.StoresState.uninstall(this, installed) { refreshAddedGames(); refresh() }
|
||||
return
|
||||
}
|
||||
if (g.library == Library.ADDED) {
|
||||
removeAddedGame(folder.path, g.gameId, g.name)
|
||||
return
|
||||
}
|
||||
val target = com.droiddeck.launcher.frontend.GameRemoval.steamFiles(g) ?: return
|
||||
Thread({
|
||||
com.droiddeck.launcher.frontend.GameRemoval.delete(target)
|
||||
ui.post { refreshAddedGames(); refresh() }
|
||||
}, "game-remove").start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAddedGame(folder: String, appId: Long?, name: String) {
|
||||
steamGames = steamGames.filterNot { it.library == Library.ADDED && it.gameFiles?.path == folder }
|
||||
Thread({ com.droiddeck.launcher.frontend.AddedGames.remove(this, java.io.File(folder), appId, name) }, "added-game-remove").start()
|
||||
@@ -1825,10 +1914,12 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private var pipAutoEnter by mutableStateOf(false)
|
||||
private var unfoldedControls by mutableStateOf(false)
|
||||
private var unfoldedControlsWithController by mutableStateOf(false)
|
||||
|
||||
private fun loadModeSettings(mode: String) {
|
||||
pipAutoEnter = SessionPrefs.pipAutoEnter(this)
|
||||
unfoldedControls = SessionPrefs.unfoldedControls(this)
|
||||
unfoldedControlsWithController = SessionPrefs.unfoldedControlsWithController(this)
|
||||
resolution = SessionPrefs.resolutionChoice(this, mode, com.droiddeck.launcher.session.SessionDisplay.panelSize(this))
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
forceSsbs = SessionPrefs.forceSsbs(this)
|
||||
@@ -1838,6 +1929,7 @@ class MainActivity : ComponentActivity() {
|
||||
steamClientArch = SessionPrefs.steamClientArch(this)
|
||||
steamRepairQueued = SteamRepair.queued(this)
|
||||
mangoapp = SessionPrefs.mangoapp(this)
|
||||
returnAfterGame = SessionPrefs.returnAfterGame(this)
|
||||
steamController = SessionPrefs.steamController(this)
|
||||
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
|
||||
loadBootMovie()
|
||||
@@ -1920,7 +2012,7 @@ class MainActivity : ComponentActivity() {
|
||||
val listX64 = SessionPrefs.activeSteamClientArch(this) == "x86_64"
|
||||
if (listX64 != steamGamesX64) {
|
||||
steamGamesX64 = listX64
|
||||
steamGames = steamGames.filterNot(Library::fromSteamClient)
|
||||
steamGames = steamGames.filterNot { Library.fromSteamClient(it) || (it.library == Library.ADDED && it.gameId in 1..0xFFFFFFFFL) }
|
||||
}
|
||||
if (!busy && LinuxRuntimeInstaller.isBusy()) followRuntimeOperation { LinuxRuntimeInstaller.attach(it) }
|
||||
if (showComponents) drivers.refreshDrivers()
|
||||
@@ -1962,12 +2054,13 @@ class MainActivity : ComponentActivity() {
|
||||
ui.post { if (scanGeneration == libraryScanGeneration) emulatorList = emus }
|
||||
if (ready) {
|
||||
val quick = runCatching { Library.steamGames(this) }.getOrNull()
|
||||
val claimed = quick?.let { runCatching { Library.claimedFolders(it) }.getOrDefault(emptySet()) }.orEmpty()
|
||||
if (quick != null) ui.post {
|
||||
if (scanGeneration == libraryScanGeneration && steamGames.isNotEmpty()) {
|
||||
val before = steamGames.map { it.gameId }.toSet()
|
||||
val fresh = quick.filter { it.gameId !in before }
|
||||
if (fresh.isNotEmpty()) {
|
||||
steamGames = (quick + steamGames.filter { it.library == Library.ADDED }).distinctBy { it.gameId }
|
||||
steamGames = (quick + steamGames.filter { it.library == Library.ADDED && it.gameFiles?.absolutePath.let { path -> path == null || path !in claimed } }).distinctBy { it.gameId }
|
||||
fresh.forEach { com.droiddeck.launcher.frontend.AutoComponents.queueGame(this, it, asked = false) }
|
||||
}
|
||||
}
|
||||
@@ -2006,10 +2099,14 @@ class MainActivity : ComponentActivity() {
|
||||
private fun onRuntimeButton() {
|
||||
if (runtimeChangesBlocked()) return
|
||||
val release = available
|
||||
if (removalPending || installed != null && release?.version == installed) {
|
||||
if (removalPending) {
|
||||
showRemove = true
|
||||
return
|
||||
}
|
||||
if (installed != null && release?.version == installed) {
|
||||
showRuntimeActions = release
|
||||
return
|
||||
}
|
||||
if (release == null) {
|
||||
Thread({ checkCatalog() }, "catalog").start()
|
||||
return
|
||||
@@ -2093,7 +2190,7 @@ class MainActivity : ComponentActivity() {
|
||||
com.droiddeck.launcher.core.AppLanguage.setChosen(this, com.droiddeck.launcher.core.AppLanguage.SYSTEM)
|
||||
if (!ok) android.widget.Toast.makeText(this, R.string.setup_reset_settings_partial, android.widget.Toast.LENGTH_LONG).show()
|
||||
com.droiddeck.launcher.ui.Motion.refresh(this)
|
||||
recreate()
|
||||
if (!switchLauncherIcon(LauncherIcon.DARK)) recreate()
|
||||
}
|
||||
}, "reset-settings").start()
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -75,11 +76,13 @@ import com.droiddeck.launcher.input.PadBridge
|
||||
import com.droiddeck.launcher.input.PadMouse
|
||||
import com.droiddeck.launcher.input.PadState
|
||||
import com.droiddeck.launcher.input.SteamControllerBle
|
||||
import com.droiddeck.launcher.input.DirectTouchHold
|
||||
import com.droiddeck.launcher.input.PointerGestures
|
||||
import com.droiddeck.launcher.input.SecondScreenDisplay
|
||||
import com.droiddeck.launcher.input.SecondScreenDisplays
|
||||
import com.droiddeck.launcher.input.SecondScreenMode
|
||||
import com.droiddeck.launcher.input.TouchpadGestures
|
||||
import com.droiddeck.launcher.input.TouchscreenGestures
|
||||
import com.droiddeck.launcher.input.ScrollTouchGestures
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.LoadingState
|
||||
@@ -131,6 +134,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var pipUi by mutableStateOf(false)
|
||||
private var pipAutoEnter by mutableStateOf(false)
|
||||
private var unfoldedControls by mutableStateOf(false)
|
||||
private var unfoldedControlsWithController by mutableStateOf(false)
|
||||
private var picturePosition by mutableStateOf("center")
|
||||
|
||||
private fun applyGameAlignment() {
|
||||
@@ -188,7 +192,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
private var watching = true
|
||||
private lateinit var touchpad: TouchpadGestures
|
||||
private lateinit var directTouchHold: DirectTouchHold
|
||||
private lateinit var threeFingerTap: com.droiddeck.launcher.input.ThreeFingerTap
|
||||
private lateinit var touchscreenGestures: TouchscreenGestures
|
||||
private var gestureConfig = com.droiddeck.launcher.input.TouchGestureConfig()
|
||||
private var gestureGame: String? = null
|
||||
private var gestureSettingsOpen by mutableStateOf(false)
|
||||
private var recognizedGesture by mutableStateOf<TouchscreenGestures.Gesture?>(null)
|
||||
private var gesturePointerX = 0f
|
||||
private var gesturePointerY = 0f
|
||||
private var gesturePointerReady = false
|
||||
private val clearRecognizedGesture = Runnable { recognizedGesture = null }
|
||||
private val gestureKeysDown = mutableSetOf<Int>()
|
||||
private lateinit var scrollTouch: ScrollTouchGestures
|
||||
private lateinit var padMouse: PadMouse
|
||||
@Volatile private var padMouseOn = false
|
||||
@@ -209,6 +224,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private val pointerLockListener = WaylandCompositor.PointerLockListener { locked, _, _ ->
|
||||
uiHandler.post {
|
||||
if (pointerLocked == locked) return@post
|
||||
gesturePointerReady = false
|
||||
pointerLocked = locked
|
||||
Log.i(TAG, "pointer lock " + if (locked) "taken by a program" else "released")
|
||||
padMouseCheckedAt = 0L
|
||||
@@ -292,6 +308,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var achievementsInFront by mutableStateOf<AchievementsInFront?>(null)
|
||||
private var achievementsOpen by mutableStateOf(false)
|
||||
private var hudOn by mutableStateOf(true)
|
||||
private var hudFps by mutableStateOf(true)
|
||||
private var hudBattery by mutableStateOf(true)
|
||||
private var hudGraph by mutableStateOf(true)
|
||||
private var hudColor by mutableStateOf(SessionPrefs.HUD_COLOR_MINT)
|
||||
private var hudOpacity by mutableStateOf(SessionPrefs.HUD_OPACITY_DEFAULT)
|
||||
private var upscaler by mutableStateOf(0)
|
||||
private var upscaleSharpness by mutableStateOf(75)
|
||||
private var effects by mutableStateOf(ScreenEffects.OFF)
|
||||
@@ -346,17 +367,46 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var secondScreenDisplays by mutableStateOf<List<SecondScreenDisplay>>(emptyList())
|
||||
private var selectedSecondScreenDisplay by mutableStateOf(SessionState.secondScreenDisplay)
|
||||
private var started = false
|
||||
private var sessionActivityDisplayId = Display.DEFAULT_DISPLAY
|
||||
private lateinit var displayManager: DisplayManager
|
||||
private var secondScreenPresentation: SecondScreenPresentation? = null
|
||||
private val displayListener = object : DisplayManager.DisplayListener {
|
||||
override fun onDisplayAdded(displayId: Int) = refreshSecondScreenDisplays()
|
||||
override fun onDisplayRemoved(displayId: Int) = refreshSecondScreenDisplays()
|
||||
override fun onDisplayAdded(displayId: Int) {
|
||||
notifyActivityDisplayStateChanged(displayId)
|
||||
refreshSecondScreenDisplays()
|
||||
}
|
||||
override fun onDisplayRemoved(displayId: Int) {
|
||||
if (refreshActivityDisplayId() && started) {
|
||||
SessionService.setActivityDisplay(this@SessionActivity, sessionActivityDisplayId)
|
||||
}
|
||||
refreshSecondScreenDisplays()
|
||||
}
|
||||
override fun onDisplayChanged(displayId: Int) {
|
||||
notifyActivityDisplayStateChanged(displayId)
|
||||
followPanelRefresh(displayId)
|
||||
refreshSecondScreenDisplays()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun currentActivityDisplayId(): Int = windowManager.defaultDisplay.displayId
|
||||
|
||||
private fun refreshActivityDisplayId(): Boolean {
|
||||
val current = if (Build.VERSION.SDK_INT >= 30) display?.displayId ?: currentActivityDisplayId()
|
||||
else currentActivityDisplayId()
|
||||
val changed = sessionActivityDisplayId != current
|
||||
sessionActivityDisplayId = current
|
||||
SessionState.activityDisplayId = current
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun notifyActivityDisplayStateChanged(changedDisplayId: Int) {
|
||||
val changed = refreshActivityDisplayId()
|
||||
if (started && (changed || changedDisplayId == sessionActivityDisplayId)) {
|
||||
SessionService.setActivityDisplay(this, sessionActivityDisplayId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun followPanelRefresh(displayId: Int) {
|
||||
if (!CompositorHost.isStarted) return
|
||||
val output = glassesScreen?.display
|
||||
@@ -455,6 +505,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
val bridge = PadBridge(File(LinuxRuntime.sessionRoot(this), "dev/input"))
|
||||
bridge.setSelectSteam(ControllerPrefs.read(this).selectSteam)
|
||||
bridge.setBuiltinPad(SessionState.mode != SessionService.MODE_STEAM || SessionPrefs.builtinPad(this))
|
||||
padBridge = bridge
|
||||
SessionState.padBridge = bridge
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
@@ -483,7 +534,20 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onScreenControls = OnScreenControls(this, bridge, onKeyboard = ::togglePcKeyboard).also { root.addView(it) }
|
||||
keyboard = KeyboardHost(this).also { root.addView(it) }
|
||||
touchpad = TouchpadGestures(PointerGestures.slop(this), pointerListener)
|
||||
directTouchHold = DirectTouchHold(PointerGestures.slop(this), pointerListener) {
|
||||
WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
|
||||
}
|
||||
threeFingerTap = com.droiddeck.launcher.input.ThreeFingerTap(PointerGestures.slop(this) * 2)
|
||||
touchscreenGestures = TouchscreenGestures(
|
||||
ViewConfiguration.get(this).scaledTouchSlop.toFloat(), gestureOutput,
|
||||
)
|
||||
refreshGestureConfig()
|
||||
// A real touch target lets FrameLayout split fingers between the
|
||||
// game surface and on-screen buttons, in either finger-down order.
|
||||
root.isMotionEventSplittingEnabled = true
|
||||
surfaceView.setOnTouchListener { _, event ->
|
||||
routedTouchMode == SessionPrefs.TOUCH_GESTURES && onTouchEvent(event)
|
||||
}
|
||||
padMouse = PadMouse(padMouseListener)
|
||||
bridge.setTouchRoute(object : PadBridge.TouchRoute {
|
||||
override fun active(): Boolean = if (Looper.myLooper() == Looper.getMainLooper()) padMouseMode() else padMouseOn
|
||||
@@ -500,12 +564,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
loading = LoadingState(this, steam = loadingMode() == SessionService.MODE_STEAM)
|
||||
if (!SessionState.running) {
|
||||
val needRuntime = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.installedVersion(this) == null
|
||||
val repair = intent.getBooleanExtra(EXTRA_REPAIR_RUNTIME, false)
|
||||
intent.removeExtra(EXTRA_REPAIR_RUNTIME)
|
||||
val needRuntime = repair || com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.installedVersion(this) == null
|
||||
val needResume = !needRuntime && com.droiddeck.launcher.runtime.DesktopCatalog.interrupted(this).isNotEmpty()
|
||||
val needDesktop = intent.getStringExtra(SessionService.EXTRA_MODE) == SessionService.MODE_DESKTOP &&
|
||||
!com.droiddeck.launcher.runtime.DesktopCatalog.desktopInstalled(this)
|
||||
(repair || !com.droiddeck.launcher.runtime.DesktopCatalog.desktopInstalled(this))
|
||||
val needProton = (intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) == SessionService.MODE_STEAM &&
|
||||
com.droiddeck.launcher.runtime.DesktopCatalog.protonSeedNeeded(this)
|
||||
if (needRuntime || needDesktop || needProton) installThenStart(needRuntime, needDesktop, needProton)
|
||||
if (needRuntime || needResume || needDesktop || needProton) installThenStart(needRuntime, needDesktop, needProton, needResume)
|
||||
}
|
||||
hud = PerfHud(this)
|
||||
hud.onPresentingWindowChanged = {
|
||||
@@ -593,6 +660,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onDispose { com.droiddeck.launcher.glasses.GlassesMenuMirror.setSessionMenuVisible(this@SessionActivity, false) }
|
||||
}
|
||||
androidx.compose.runtime.LaunchedEffect(drawerOpen, pcKeyboardOpen, achievementsOpen, pipUi) { updatePointerCapture() }
|
||||
androidx.compose.runtime.LaunchedEffect(AgentGuest.steamKeyboard) { updateOnScreenControls() }
|
||||
androidx.compose.runtime.LaunchedEffect(picturePosition, SessionState.running, pipUi, cardboard.enabled) { applyGameAlignment() }
|
||||
DroidDeckTheme {
|
||||
if (pipUi) {
|
||||
@@ -605,10 +673,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
} else {
|
||||
com.droiddeck.launcher.ui.FocusGlideHost(androidx.compose.ui.Modifier.fillMaxSize()) {
|
||||
CursorOverlay(cursorPos, cursorVisible, resources.displayMetrics.density,
|
||||
CursorOverlay({ cursorPos }, cursorVisible, resources.displayMetrics.density,
|
||||
cursorImage, cursorHotX, cursorHotY, cursorImageScale)
|
||||
if (hud.text.isNotEmpty()) HudText(hud.text)
|
||||
if (glassesOutputActive && !loading.visible) com.droiddeck.launcher.ui.GlassesPlayingHint(glasses.model)
|
||||
if (hud.text.isNotEmpty() || hud.graph.size >= 2) HudText(hud.text, hud.graph, hudColor, hudOpacity)
|
||||
val phonePanel = glassesOutputActive && !loading.visible && !drawerOpen && !pcKeyboardOpen
|
||||
if (phonePanel) GlassesPhonePanel(glasses, steamVrStatus == SteamVr.Status.RUNNING, headTracking)
|
||||
if (trackingRecovery != null && !drawerOpen && !pcKeyboardOpen && !loading.visible) {
|
||||
androidx.compose.ui.viewinterop.AndroidView(
|
||||
factory = { com.droiddeck.launcher.ui.TrackingRecoveryView(it) },
|
||||
@@ -616,14 +685,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
update = { it.update(trackingRecovery, !glassesOutputActive && glasses.usable && glasses.sbs) },
|
||||
)
|
||||
}
|
||||
if (GlassesRendering.usesGlasses(glasses, glassesOutputActive, steamVrStatus == SteamVr.Status.RUNNING) &&
|
||||
if (!phonePanel && GlassesRendering.usesGlasses(glasses, glassesOutputActive, steamVrStatus == SteamVr.Status.RUNNING) &&
|
||||
headTracking && !drawerOpen && !pcKeyboardOpen && !loading.visible) com.droiddeck.launcher.ui.GlassesRecenterButton(
|
||||
controllerSettings?.tint ?: android.graphics.Color.rgb(90, 160, 255), controllerSettings?.opacity ?: 100,
|
||||
onRecenterJoyCons = if (steamVrStatus == SteamVr.Status.RUNNING &&
|
||||
glasses.usesSpatialControllers(glassesSettings) && glassesSettings.joyConTracking)
|
||||
joyConVr?.let { { it.recenter() } } else null,
|
||||
) { headTracker?.recenter() }
|
||||
if (glasses.present && !drawerOpen && !pcKeyboardOpen && !loading.visible) com.droiddeck.launcher.ui.GlassesCornerButton(glassesSlot,
|
||||
if (!phonePanel && glasses.present && !drawerOpen && !pcKeyboardOpen && !loading.visible) com.droiddeck.launcher.ui.GlassesCornerButton(glassesSlot,
|
||||
controllerSettings?.tint ?: android.graphics.Color.rgb(90, 160, 255), controllerSettings?.opacity ?: 100) {
|
||||
drawerPage = com.droiddeck.launcher.ui.SessionDrawerPage.GLASSES
|
||||
drawerOpen = true
|
||||
@@ -635,6 +704,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onCancel = { SessionService.stop(this@SessionActivity); finish() },
|
||||
endedDetail = loading.endedDetail,
|
||||
onRetry = { retrySession() },
|
||||
onRepair = if (repairOffered) ({ repairRuntime() }) else null,
|
||||
onShareLogs = { shareCurrentSessionLogs() },
|
||||
onClose = { finish() },
|
||||
flood = floodColor?.let { androidx.compose.ui.graphics.Color(it) },
|
||||
@@ -643,6 +713,32 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
window.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(android.graphics.Color.BLACK))
|
||||
},
|
||||
)
|
||||
LaunchedEffect(AgentGuest.focusRevision) {
|
||||
val nextGame = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||
if (SessionState.mode == SessionService.MODE_RUN) SessionState.program?.let { "program:$it" }
|
||||
else {
|
||||
val snapshot = AgentGuest.focus(this@SessionActivity)
|
||||
val focused = snapshot?.optLong("focusedApp")?.takeIf { it > 0 && it != 769L }
|
||||
val windows = snapshot?.optJSONArray("focusableApps")
|
||||
focused?.toString() ?: windows?.let { values ->
|
||||
(0 until values.length()).map { values.optLong(it) }
|
||||
.filter { it > 0 && it != 769L }.distinct().singleOrNull()?.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nextGame != gestureGame) { gestureGame = nextGame; refreshGestureConfig() }
|
||||
}
|
||||
LaunchedEffect(drawerOpen, pcKeyboardOpen, achievementsOpen, gestureSettingsOpen, pipUi, SessionState.suspended) {
|
||||
if (drawerOpen || pcKeyboardOpen || achievementsOpen || gestureSettingsOpen || pipUi || SessionState.suspended) {
|
||||
cancelTouchGestures()
|
||||
}
|
||||
}
|
||||
com.droiddeck.launcher.ui.GestureFeedback(recognizedGesture)
|
||||
if (gestureSettingsOpen) com.droiddeck.launcher.ui.TouchGestureDialog(
|
||||
game = gestureGame,
|
||||
onDismiss = { gestureSettingsOpen = false },
|
||||
onSaved = { gestureSettingsOpen = false; refreshGestureConfig() },
|
||||
)
|
||||
androidx.compose.runtime.LaunchedEffect(drawerOpen) {
|
||||
if (drawerOpen) {
|
||||
syncLossless()
|
||||
@@ -678,7 +774,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
achievementsInFront = found
|
||||
if (found == null) achievementsOpen = false
|
||||
}
|
||||
androidx.compose.runtime.LaunchedEffect(pcKeyboardOpen) { if (pcKeyboardOpen) releasePads() }
|
||||
androidx.compose.runtime.LaunchedEffect(pcKeyboardOpen) { if (pcKeyboardOpen) releasePads(); updateOnScreenControls() }
|
||||
LaunchedEffect(pcKeyboardOpen, drawerOpen, achievementsOpen) {
|
||||
if (pcKeyboardOpen || drawerOpen || achievementsOpen) padMouse.reset()
|
||||
}
|
||||
@@ -696,6 +792,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
isHomeApp = isHomeApp,
|
||||
androidApps = androidApps,
|
||||
hudOn = hudOn,
|
||||
hudFps = hudFps,
|
||||
hudBattery = hudBattery,
|
||||
hudGraph = hudGraph,
|
||||
hudColor = hudColor,
|
||||
hudOpacity = hudOpacity,
|
||||
upscaler = upscaler, upscaleSharpness = upscaleSharpness,
|
||||
effects = effects,
|
||||
cardboard = cardboard,
|
||||
@@ -723,6 +824,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
secondScreenDisplays = secondScreenDisplays,
|
||||
selectedSecondScreenDisplay = selectedSecondScreenDisplay,
|
||||
onHud = { on -> SessionPrefs.setHudEnabled(this@SessionActivity, on); hudOn = on; hud.refresh() },
|
||||
onHudFps = { on -> SessionPrefs.setHudShowFps(this@SessionActivity, on); hudFps = on; hud.refresh() },
|
||||
onHudBattery = { on -> SessionPrefs.setHudShowBattery(this@SessionActivity, on); hudBattery = on; hud.refresh() },
|
||||
onHudGraph = { on -> SessionPrefs.setHudShowGraph(this@SessionActivity, on); hudGraph = on; hud.refresh() },
|
||||
onHudColor = { color -> SessionPrefs.setHudColor(this@SessionActivity, color); hudColor = color },
|
||||
onHudOpacity = { percent -> SessionPrefs.setHudOpacity(this@SessionActivity, percent); hudOpacity = percent },
|
||||
onUpscaler = { m ->
|
||||
SessionPrefs.setUpscaler(this@SessionActivity, m); upscaler = m
|
||||
WaylandCompositor.nativeSetUpscaler(m)
|
||||
@@ -779,6 +885,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionPrefs.setSteamController(this@SessionActivity, id)
|
||||
com.droiddeck.launcher.session.SteamDeckPad.showPad(SessionPrefs.padType(id))
|
||||
com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(id))
|
||||
com.droiddeck.launcher.session.InputSysfsComponent.padTypeChanged(SessionPrefs.padType(id))
|
||||
padType = id
|
||||
},
|
||||
backActionsInverted = backActionsInverted,
|
||||
@@ -820,6 +927,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionService.suspendPolicyChanged(this@SessionActivity)
|
||||
},
|
||||
onTouch = { v -> SessionPrefs.setTouchMode(this@SessionActivity, v); readPrefs() },
|
||||
onGestureSettings = { drawerOpen = false; gestureSettingsOpen = true },
|
||||
onFexPreset = { v -> SessionPrefs.setFexPreset(this@SessionActivity, v); readPrefs() },
|
||||
onSecondScreenMode = ::selectSecondScreenMode,
|
||||
onSecondScreenDisplay = ::selectSecondScreenDisplay,
|
||||
@@ -847,6 +955,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
},
|
||||
unfoldedControls = if (com.droiddeck.launcher.session.SessionDisplay.foldable(this@SessionActivity)) unfoldedControls else null,
|
||||
onUnfoldedControls = ::chooseUnfoldedControls,
|
||||
unfoldedControlsWithController = unfoldedControlsWithController,
|
||||
onUnfoldedControlsWithController = ::chooseUnfoldedControlsWithController,
|
||||
onBackground = { drawerOpen = false; pip.background(); moveTaskToBack(true) },
|
||||
onShareLogs = { drawerOpen = false; shareCurrentSessionLogs() },
|
||||
onStop = { drawerOpen = false; SessionService.stop(this@SessionActivity); finish() },
|
||||
@@ -940,6 +1050,54 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}, 90)
|
||||
}
|
||||
|
||||
@androidx.compose.runtime.Composable
|
||||
private fun GlassesPhonePanel(glasses: com.droiddeck.launcher.glasses.GlassesState, vr: Boolean, headTracking: Boolean) {
|
||||
val battery by androidx.compose.runtime.produceState<String?>(null) {
|
||||
while (true) {
|
||||
val b = registerReceiver(null, android.content.IntentFilter(android.content.Intent.ACTION_BATTERY_CHANGED))
|
||||
val level = b?.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, -1) ?: -1
|
||||
val scale = b?.getIntExtra(android.os.BatteryManager.EXTRA_SCALE, 100) ?: 100
|
||||
val tenths = b?.getIntExtra(android.os.BatteryManager.EXTRA_TEMPERATURE, -1) ?: -1
|
||||
value = if (level < 0) null else getString(R.string.glasses_phone_battery, level * 100 / scale.coerceAtLeast(1), tenths / 10)
|
||||
kotlinx.coroutines.delay(30_000)
|
||||
}
|
||||
}
|
||||
val pads = com.droiddeck.launcher.input.VrControllers.settings.collectAsState().value
|
||||
com.droiddeck.launcher.ui.GlassesPhonePanel(glasses.model, battery, com.droiddeck.launcher.ui.GlassesPhoneActions(
|
||||
onRecenter = if (headTracking) ({ headTracker?.recenter() }) else null,
|
||||
onRecenterJoyCons = if (vr && glasses.usesSpatialControllers(glassesSettings) && glassesSettings.joyConTracking)
|
||||
joyConVr?.let { { it.recenter() } } else null,
|
||||
onSteamVrMenu = if (vr && SessionState.mode == SessionService.MODE_STEAM) ({ sendSteamGuide() }) else null,
|
||||
onSwitchSticks = if (vr && pads.emulate != com.droiddeck.launcher.input.VrControllers.Emulate.OFF) ({ switchVrSticks() }) else null,
|
||||
controllers = getString(when (pads.emulate) {
|
||||
com.droiddeck.launcher.input.VrControllers.Emulate.OFF -> R.string.vr_controllers_off
|
||||
com.droiddeck.launcher.input.VrControllers.Emulate.INDEX -> R.string.vr_controllers_index
|
||||
com.droiddeck.launcher.input.VrControllers.Emulate.TOUCH -> R.string.vr_controllers_touch
|
||||
com.droiddeck.launcher.input.VrControllers.Emulate.VIVE -> R.string.vr_controllers_vive
|
||||
}),
|
||||
onNextControllers = if (vr) ({
|
||||
val all = com.droiddeck.launcher.input.VrControllers.Emulate.entries
|
||||
com.droiddeck.launcher.input.VrControllers.setEmulate(this, all[(pads.emulate.ordinal + 1) % all.size])
|
||||
}) else null,
|
||||
onKeyboard = { keyboard?.toggle() },
|
||||
onSessionMenu = { drawerOpen = true },
|
||||
onStopSteamVr = if (vr) ({ steamVrAction("stop") }) else null,
|
||||
brightness = glasses.brightness,
|
||||
brightnessMax = glasses.brightnessMax,
|
||||
onBrightness = if (glasses.canControl && glasses.hardwareControls) Glasses::setBrightness else null,
|
||||
))
|
||||
}
|
||||
|
||||
/** The SteamVR driver switches pad sticks between hands and game on both stick clicks together. */
|
||||
private fun switchVrSticks() {
|
||||
val chord = { down: Boolean -> padBridge?.applyTouch { state ->
|
||||
state.press(com.droiddeck.launcher.input.PadState.L3, down)
|
||||
state.press(com.droiddeck.launcher.input.PadState.R3, down)
|
||||
} }
|
||||
chord(true)
|
||||
uiHandler.postDelayed({ chord(false) }, 120)
|
||||
}
|
||||
|
||||
private fun triggerSteamQam() {
|
||||
drawerOpen = false
|
||||
padBridge?.triggerQam()
|
||||
@@ -1283,8 +1441,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private fun readPrefs() {
|
||||
pipAutoEnter = SessionPrefs.pipAutoEnter(this)
|
||||
unfoldedControls = SessionPrefs.unfoldedControls(this)
|
||||
unfoldedControlsWithController = SessionPrefs.unfoldedControlsWithController(this)
|
||||
picturePosition = SessionPrefs.picturePosition(this)
|
||||
hudOn = SessionPrefs.hudEnabled(this)
|
||||
hudFps = SessionPrefs.hudShowFps(this)
|
||||
hudBattery = SessionPrefs.hudShowBattery(this)
|
||||
hudGraph = SessionPrefs.hudShowGraph(this)
|
||||
hudColor = SessionPrefs.hudColor(this)
|
||||
hudOpacity = SessionPrefs.hudOpacity(this)
|
||||
upscaler = SessionPrefs.upscaler(this)
|
||||
upscaleSharpness = SessionPrefs.upscaleSharpness(this)
|
||||
effects = SessionPrefs.screenEffects(this)
|
||||
@@ -1298,6 +1462,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val nextTouchMode = SessionPrefs.touchMode(this)
|
||||
if (touchMode != nextTouchMode) {
|
||||
if (::touchpad.isInitialized) touchpad.cancel()
|
||||
if (::directTouchHold.isInitialized) directTouchHold.cancel()
|
||||
if (::scrollTouch.isInitialized) scrollTouch.cancel()
|
||||
if (CompositorHost.isStarted) {
|
||||
WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
|
||||
@@ -1322,17 +1487,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
|
||||
@Volatile private var installingRuntime = false
|
||||
private var repairOffered by mutableStateOf(false)
|
||||
|
||||
private fun installThenStart(runtime: Boolean, desktop: Boolean, proton: Boolean = false) {
|
||||
private fun installThenStart(runtime: Boolean, desktop: Boolean, proton: Boolean = false, resume: Boolean = false) {
|
||||
if (SessionState.stopRequested) return
|
||||
installingRuntime = true
|
||||
SessionState.installing = if (runtime) "runtime" else if (desktop) "desktop" else "proton"
|
||||
SessionState.installing = if (runtime) "runtime" else if (resume) "package" else if (desktop) "desktop" else "proton"
|
||||
SessionEvents.transition(
|
||||
SessionPhase.INSTALLING_RUNTIME,
|
||||
"${SessionState.installing}.installing",
|
||||
mapOf("component" to SessionState.installing),
|
||||
)
|
||||
val first = if (runtime) RUNTIME_LINES else if (desktop) DESKTOP_LINES else PROTON_LINES
|
||||
val first = if (runtime || resume) RUNTIME_LINES else if (desktop) DESKTOP_LINES else PROTON_LINES
|
||||
loading.say(getString(first.downloading), first.topic, readable = true)
|
||||
loading.percent = -1
|
||||
Thread({
|
||||
@@ -1342,6 +1508,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
problem = installRuntime()
|
||||
if (problem == null) SessionEvents.record("runtime.ready") else failedComponent = "runtime"
|
||||
}
|
||||
if (problem == null && resume && !SessionState.stopRequested) {
|
||||
SessionState.installing = "package"
|
||||
SessionEvents.transition(SessionPhase.INSTALLING_RUNTIME, "package.resuming", mapOf("component" to "package"))
|
||||
problem = com.droiddeck.launcher.runtime.DesktopCatalog.resumeInterrupted(this, progressAsIs())
|
||||
?.let { getString(R.string.session_package_resume_failed, it) }
|
||||
if (problem == null) SessionEvents.record("package.ready") else failedComponent = "package"
|
||||
}
|
||||
if (problem == null && desktop && !SessionState.stopRequested) {
|
||||
SessionState.installing = "desktop"
|
||||
SessionEvents.transition(SessionPhase.INSTALLING_RUNTIME, "desktop.installing", mapOf("component" to "desktop"))
|
||||
@@ -1366,7 +1539,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return@post
|
||||
}
|
||||
if (problem != null) {
|
||||
val code = if (failedComponent == "runtime") "RUNTIME_INSTALL_FAILED" else "DESKTOP_INSTALL_FAILED"
|
||||
val code = when (failedComponent) {
|
||||
"runtime" -> "RUNTIME_INSTALL_FAILED"
|
||||
"package" -> "PACKAGE_RESUME_FAILED"
|
||||
else -> "DESKTOP_INSTALL_FAILED"
|
||||
}
|
||||
SessionEvents.fail(code, problem)
|
||||
collectStartArtifacts("session start failed")
|
||||
loading.showEnded(problem)
|
||||
@@ -1414,9 +1591,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
private fun progressAsIs() = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.ProgressListener { stage, percent ->
|
||||
uiHandler.post {
|
||||
loading.percent = percent
|
||||
loading.say(stage, LoadingState.Topic.RUNTIME, readable = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun installRuntime(): String? {
|
||||
val release = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.fetchRelease()
|
||||
?: return getString(R.string.session_runtime_catalog_unreachable)
|
||||
runCatching { com.droiddeck.launcher.runtime.RuntimeInstallService.start(this, release) }
|
||||
val ok = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.install(this, release,
|
||||
progressFor(RUNTIME_LINES, release.size / 1_000_000))
|
||||
return if (ok) null else getString(R.string.session_runtime_install_failed)
|
||||
@@ -1446,10 +1631,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
val runtimeDir = File(filesDir, ".wayland-rt").apply { mkdirs() }
|
||||
FileUtils.copyAsset(this, "wayland/keymap.xkb", File(runtimeDir, "keymap.xkb"))
|
||||
com.droiddeck.launcher.input.VrControllers.publish(this)
|
||||
|
||||
val turnip = TurnipDriver(this)
|
||||
com.droiddeck.launcher.gpu.DriverSetupLog.write(this, "surface.driver-setup")
|
||||
if (!CompositorHost.isStarted) CompositorHost.displayChoice = SessionPrefs.androidDriver(this)
|
||||
if (!CompositorHost.isStarted) {
|
||||
CompositorHost.displayChoice = SessionPrefs.androidDriver(this)
|
||||
com.droiddeck.launcher.gpu.AutoDriverTrial.displayStarting(this, SessionPrefs.androidDriver(this))
|
||||
}
|
||||
val driverId = if (CompositorHost.isStarted) null else turnip.install()
|
||||
|
||||
val previous = SessionState.outputSize
|
||||
@@ -1524,6 +1713,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
intent.getStringExtra(SessionService.EXTRA_STEAM_UI),
|
||||
intent.getStringExtra(SessionService.EXTRA_STEAM_URL),
|
||||
intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS),
|
||||
sessionActivityDisplayId,
|
||||
)
|
||||
}
|
||||
applyFrameGen()
|
||||
@@ -1539,6 +1729,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
surfaceW = width
|
||||
surfaceH = height
|
||||
if (resized) {
|
||||
cancelTouchGestures()
|
||||
Log.i(TAG, "surface resized to ${width}x$height - rebinding the compositor")
|
||||
CompositorHost.resize(holder.surface) { applyFrameGen() }
|
||||
followWindow(width, height)
|
||||
@@ -1557,7 +1748,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun followChoice(): Boolean {
|
||||
val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM)
|
||||
val mode = displayMode()
|
||||
return SessionPrefs.resolutionChoice(this, mode, com.droiddeck.launcher.session.SessionDisplay.panelSize(this)) ==
|
||||
com.droiddeck.launcher.session.SessionDisplay.FOLLOW_SCREEN
|
||||
}
|
||||
@@ -1573,6 +1764,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (refreshActivityDisplayId() && started) {
|
||||
SessionService.setActivityDisplay(this, sessionActivityDisplayId)
|
||||
}
|
||||
cancelTouchGestures()
|
||||
applyFollowOrientation(newConfig)
|
||||
applyUnfoldedControls()
|
||||
}
|
||||
@@ -1581,12 +1776,21 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// Hiding the phone surface after a handoff delivers a late destruction
|
||||
// callback. It must not reset the new output's size or attachment.
|
||||
if (holder !== outputHolder()) return
|
||||
cancelTouchGestures()
|
||||
surfaceW = 0
|
||||
surfaceH = 0
|
||||
glassesOutputActive = false
|
||||
CompositorHost.detach(holder.surface)
|
||||
}
|
||||
|
||||
private fun cancelTouchGestures() {
|
||||
if (::touchscreenGestures.isInitialized) touchscreenGestures.cancel()
|
||||
if (::touchpad.isInitialized) touchpad.cancel()
|
||||
if (::scrollTouch.isInitialized) scrollTouch.cancel()
|
||||
scrollTouchGesture = false
|
||||
gesturePointerReady = false
|
||||
}
|
||||
|
||||
private fun panelBounds(): android.graphics.Rect = if (Build.VERSION.SDK_INT >= 30) {
|
||||
windowManager.maximumWindowMetrics.bounds
|
||||
} else {
|
||||
@@ -1596,8 +1800,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
android.graphics.Rect(0, 0, metrics.widthPixels, metrics.heightPixels)
|
||||
}
|
||||
|
||||
private fun displayMode(): String = SessionPrefs.displayMode(this,
|
||||
intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM, intent.getStringExtra(SessionService.EXTRA_PROGRAM))
|
||||
|
||||
private fun outputSize(): Pair<Int, Int> {
|
||||
val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM)
|
||||
val mode = displayMode()
|
||||
val panel = com.droiddeck.launcher.session.SessionDisplay.panelSize(this)
|
||||
val program = intent.getStringExtra(SessionService.EXTRA_PROGRAM)
|
||||
val choice = if (intent.getStringExtra(SessionService.EXTRA_MODE) == SessionService.MODE_RUN &&
|
||||
@@ -1611,8 +1818,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
glassesScreen?.hz?.takeIf { it > 1f }?.let { return it }
|
||||
val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay
|
||||
val wanted = window.attributes.preferredDisplayModeId
|
||||
val hz = display?.supportedModes?.firstOrNull { wanted != 0 && it.modeId == wanted }?.refreshRate
|
||||
?: display?.refreshRate ?: 60f
|
||||
val requested = display?.supportedModes?.firstOrNull { wanted != 0 && it.modeId == wanted }?.refreshRate
|
||||
val running = display?.refreshRate?.takeIf { it > 1f }
|
||||
val hz = listOfNotNull(requested, running).minOrNull() ?: 60f
|
||||
val mirrorCap = mirrorRefreshLimit().takeIf { it > 0 }?.toFloat() ?: Float.POSITIVE_INFINITY
|
||||
val capped = minOf(hz, PerfMode.peakRefreshCap(this), mirrorCap)
|
||||
return if (capped > 1f) capped else 60f
|
||||
@@ -1716,7 +1924,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return
|
||||
}
|
||||
Thread({
|
||||
val hint = sessionEndHint(SessionState.logFile)
|
||||
val hint = runtimeDamageHint() ?: sessionEndHint(SessionState.logFile)
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed) return@runOnUiThread
|
||||
showEnded(status, hint)
|
||||
@@ -1735,6 +1943,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionService.MODE_DESKTOP -> getString(R.string.session_ended_desktop)
|
||||
else -> getString(R.string.session_ended_named, "Steam")
|
||||
}
|
||||
repairOffered = hint?.repair == true
|
||||
loading.showEnded(
|
||||
hint?.text ?: stopped,
|
||||
getString(R.string.session_ended_detail, status, SessionState.logFile?.path ?: getString(R.string.session_ended_no_log)),
|
||||
@@ -1753,6 +1962,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
uiHandler.post { sessionOverlay.requestFocus() }
|
||||
}
|
||||
|
||||
private fun repairRuntime() {
|
||||
val again = Intent(intent).putExtra(EXTRA_REPAIR_RUNTIME, true)
|
||||
closeAtOnce = true
|
||||
finish()
|
||||
startActivity(again)
|
||||
}
|
||||
|
||||
private fun retrySession() {
|
||||
val again = Intent(intent)
|
||||
closeAtOnce = true
|
||||
@@ -1760,7 +1976,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
startActivity(again)
|
||||
}
|
||||
|
||||
private class EndHint(val text: String, val evenOnSuccess: Boolean = false)
|
||||
private class EndHint(val text: String, val evenOnSuccess: Boolean = false, val repair: Boolean = false)
|
||||
|
||||
private fun runtimeDamageHint(): EndHint? {
|
||||
val broken = runCatching { com.droiddeck.launcher.runtime.RuntimeHealth.brokenLibraryLinks(this) }.getOrDefault(emptyList())
|
||||
if (broken.isEmpty()) return null
|
||||
Log.w(TAG, "runtime has ${broken.size} broken library link(s): ${broken.take(8).joinToString()}")
|
||||
return EndHint(resources.getQuantityString(R.plurals.session_end_runtime_damaged, broken.size, broken.size, broken.first()), repair = true)
|
||||
}
|
||||
|
||||
private fun presentationFault(): String? {
|
||||
com.droiddeck.launcher.wayland.WaylandCompositor.compositorFault().takeIf { it.isNotEmpty() }?.let { return it }
|
||||
@@ -1791,17 +2014,22 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
var enosys = 0
|
||||
var fexMissing = false
|
||||
val size = log.length()
|
||||
java.io.RandomAccessFile(log, "r").use { f ->
|
||||
val text = java.io.RandomAccessFile(log, "r").use { f ->
|
||||
val start = maxOf(0L, size - 512L * 1024)
|
||||
f.seek(start)
|
||||
val bytes = ByteArray((size - start).toInt())
|
||||
f.readFully(bytes)
|
||||
String(bytes, Charsets.ISO_8859_1).lineSequence().forEach { line ->
|
||||
if (line.contains("Function not implemented")) enosys++
|
||||
if (line.contains(FEX_MISSING)) fexMissing = true
|
||||
}
|
||||
String(bytes, Charsets.ISO_8859_1)
|
||||
}
|
||||
if (fexMissing) {
|
||||
text.lineSequence().forEach { line ->
|
||||
if (line.contains("Function not implemented")) enosys++
|
||||
if (line.contains(FEX_MISSING)) fexMissing = true
|
||||
}
|
||||
val steamSetup = com.droiddeck.launcher.session.SteamClientSetupFailure.find(text.lineSequence())
|
||||
if (steamSetup != null) {
|
||||
val message = getString(if (steamSetup.download) R.string.session_end_steam_download else R.string.session_end_steam_setup)
|
||||
EndHint(listOfNotNull(message, steamSetup.curlError).joinToString("\n"))
|
||||
} else if (fexMissing) {
|
||||
val what = com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: getString(R.string.session_end_program)
|
||||
EndHint(getString(R.string.session_end_fex_missing, what), evenOnSuccess = true)
|
||||
} else if (enosys >= 8) {
|
||||
@@ -1844,7 +2072,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (drawerOpen) {
|
||||
if (fromController && (event.keyCode == KeyEvent.KEYCODE_BUTTON_L1 || event.keyCode == KeyEvent.KEYCODE_BUTTON_R1)) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
|
||||
drawerPage = sessionDrawerPages(SessionState.mode == SessionService.MODE_STEAM && secondScreenDisplays.isNotEmpty(), glasses.present)
|
||||
drawerPage = sessionDrawerPages(secondScreenDisplays.isNotEmpty(), glasses.present)
|
||||
.step(drawerPage, if (event.keyCode == KeyEvent.KEYCODE_BUTTON_R1) 1 else -1)
|
||||
releaseDrawerDirection()
|
||||
}
|
||||
@@ -2007,6 +2235,55 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
private fun refreshGestureConfig() {
|
||||
gestureConfig = com.droiddeck.launcher.input.GesturePrefs.read(this, gestureGame)
|
||||
if (::touchscreenGestures.isInitialized) touchscreenGestures.configure(gestureConfig)
|
||||
gesturePointerReady = false
|
||||
recognizedGesture = null
|
||||
}
|
||||
|
||||
private val gestureOutput = object : TouchscreenGestures.Output {
|
||||
override fun moveTo(x: Float, y: Float) {
|
||||
val rect = drawnRect() ?: return
|
||||
if (pointerLocked) {
|
||||
if (gesturePointerReady) moveBy(x - gesturePointerX, y - gesturePointerY, true)
|
||||
} else movePointer(x, y)
|
||||
gesturePointerX = if (pointerLocked) x else x.coerceIn(rect.left, rect.right)
|
||||
gesturePointerY = if (pointerLocked) y else y.coerceIn(rect.top, rect.bottom)
|
||||
gesturePointerReady = true
|
||||
cursorVisible = false
|
||||
}
|
||||
override fun moveBy(dx: Float, dy: Float, relative: Boolean) {
|
||||
val rect = drawnRect() ?: return
|
||||
if (relative || pointerLocked) {
|
||||
val scale = SessionState.outputSize.first / rect.width()
|
||||
WaylandCompositor.sendPointerDelta(dx * scale, dy * scale)
|
||||
} else moveTo(gesturePointerX + dx, gesturePointerY + dy)
|
||||
}
|
||||
override fun button(code: Int, down: Boolean) {
|
||||
WaylandCompositor.nativeSendSceneInput(3, code, if (down) 1 else 0)
|
||||
}
|
||||
override fun prepareRelativePan() {
|
||||
if (pointerLocked) return
|
||||
val rect = drawnRect() ?: return
|
||||
// Gamescope retains the last absolute position while a game can warp
|
||||
// its own cursor to the center. Its first unlocked motion must start
|
||||
// from that same origin, before the game switches to pointer lock.
|
||||
moveTo(rect.centerX(), rect.centerY())
|
||||
}
|
||||
override fun key(code: Int, down: Boolean) {
|
||||
if (down) gestureKeysDown.add(code) else gestureKeysDown.remove(code)
|
||||
if (code !in guestKeysDown) WaylandCompositor.nativeSendKey(code, if (down) 1 else 0)
|
||||
}
|
||||
override fun wheel(steps: Int) { WaylandCompositor.nativeSendSceneInput(4, steps, 0) }
|
||||
override fun showKeyboard() { uiHandler.post { touchscreenGestures.cancel(); togglePcKeyboard() } }
|
||||
override fun gesture(gesture: TouchscreenGestures.Gesture) {
|
||||
recognizedGesture = gesture
|
||||
uiHandler.removeCallbacks(clearRecognizedGesture)
|
||||
uiHandler.postDelayed(clearRecognizedGesture, 1200)
|
||||
}
|
||||
}
|
||||
|
||||
private val pointerListener = object : PointerGestures.Listener {
|
||||
override fun onMove(x: Float, y: Float) {
|
||||
if (pointerLocked) moveLockedPointer(x, y) else movePointer(x, y)
|
||||
@@ -2141,9 +2418,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return true
|
||||
}
|
||||
|
||||
private val shownOnDisplayId: Int get() =
|
||||
if (Build.VERSION.SDK_INT >= 30) display?.displayId ?: windowManager.defaultDisplay.displayId
|
||||
else windowManager.defaultDisplay.displayId
|
||||
|
||||
private fun refreshSecondScreenDisplays() {
|
||||
if (!::displayManager.isInitialized) return
|
||||
val primaryId = display?.displayId ?: windowManager.defaultDisplay.displayId
|
||||
val primaryId = shownOnDisplayId
|
||||
val candidates = SecondScreenDisplays.available(displayManager, primaryId)
|
||||
.filter { glassesScreen == null || it.id != glasses.displayId }
|
||||
val oldIds = secondScreenDisplays.map { it.id }.toSet()
|
||||
@@ -2235,6 +2516,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
applyUnfoldedControls()
|
||||
}
|
||||
|
||||
private fun chooseUnfoldedControlsWithController(on: Boolean) {
|
||||
SessionPrefs.setUnfoldedControlsWithController(this, on)
|
||||
unfoldedControlsWithController = on
|
||||
applyUnfoldedControls()
|
||||
}
|
||||
|
||||
private fun refreshControls() {
|
||||
applyUnfoldedControls()
|
||||
updateOnScreenControls()
|
||||
@@ -2244,11 +2531,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (!::sessionRoot.isInitialized) return
|
||||
val width = sessionRoot.width
|
||||
val height = sessionRoot.height
|
||||
val wanted = unfoldedControls && com.droiddeck.launcher.session.SessionDisplay.foldable(this) &&
|
||||
!PadBridge.anyControllerConnected() && !SteamControllerBle.connected &&
|
||||
SessionState.mode == SessionService.MODE_STEAM && SessionState.deckPad &&
|
||||
!pipUi && !isInPictureInPictureMode && width > 0 && height > 0 &&
|
||||
resources.configuration.smallestScreenWidthDp >= com.droiddeck.launcher.session.SessionDisplay.UNFOLDED_MIN_DP
|
||||
val wanted = com.droiddeck.launcher.session.SessionDisplay.unfoldedControlsWanted(
|
||||
enabled = unfoldedControls,
|
||||
foldable = com.droiddeck.launcher.session.SessionDisplay.foldable(this),
|
||||
controllerConnected = PadBridge.anyControllerConnected() || SteamControllerBle.connected,
|
||||
keepWithController = unfoldedControlsWithController,
|
||||
steamDeckPad = SessionState.mode == SessionService.MODE_STEAM && SessionState.deckPad,
|
||||
pip = pipUi || isInPictureInPictureMode,
|
||||
width = width,
|
||||
height = height,
|
||||
smallestWidthDp = resources.configuration.smallestScreenWidthDp,
|
||||
)
|
||||
val gameHeight = if (wanted) com.droiddeck.launcher.session.SessionDisplay.unfoldedGameHeight(width, height)
|
||||
else FrameLayout.LayoutParams.MATCH_PARENT
|
||||
val params = surfaceView.layoutParams as FrameLayout.LayoutParams
|
||||
@@ -2297,6 +2590,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (options == null || steamTouchControls?.visibility != View.VISIBLE) return SessionPrefs.touchMode(this)
|
||||
return when {
|
||||
options.inputMode == com.droiddeck.launcher.input.SteamTouchConfig.INPUT_CONTROLLER -> SessionPrefs.TOUCH_OFF
|
||||
SessionPrefs.touchMode(this) == SessionPrefs.TOUCH_GESTURES -> SessionPrefs.TOUCH_GESTURES
|
||||
options.mouseMode == com.droiddeck.launcher.input.SteamTouchConfig.MOUSE_RELATIVE -> SessionPrefs.TOUCH_PAD
|
||||
else -> SessionPrefs.TOUCH_DIRECT
|
||||
}
|
||||
@@ -2309,7 +2603,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
touchpad.sensitivity = 1.15f * speed
|
||||
}
|
||||
if (next == routedTouchMode) return
|
||||
if (::touchscreenGestures.isInitialized) touchscreenGestures.cancel()
|
||||
gesturePointerReady = false
|
||||
if (::touchpad.isInitialized) touchpad.cancel()
|
||||
if (::directTouchHold.isInitialized) directTouchHold.cancel()
|
||||
if (::scrollTouch.isInitialized) scrollTouch.cancel()
|
||||
if (CompositorHost.isStarted) WaylandCompositor.nativeSendTouch(3, -1, 0, 0)
|
||||
if (next == SessionPrefs.TOUCH_OFF) cursorVisible = false
|
||||
@@ -2344,6 +2641,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE)) return onMouse(event)
|
||||
if (drawerOpen || pcKeyboardOpen || achievementsOpen || gestureSettingsOpen || pipUi || SessionState.suspended) return true
|
||||
if (routedTouchMode == SessionPrefs.TOUCH_GESTURES) {
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) gesturePointerReady = false
|
||||
return touchscreenGestures.onTouch(event)
|
||||
}
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
scrollTouchGesture = scrollTouchMode()
|
||||
scrollRestX = 0f
|
||||
@@ -2377,6 +2679,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) ignoreGameTouch = false
|
||||
return true
|
||||
}
|
||||
if (directTouchHold.onTouch(event)) return true
|
||||
fun sendTouch(action: Int, index: Int) {
|
||||
val x = event.getX(index).coerceIn(rect.left, rect.right)
|
||||
val y = event.getY(index).coerceIn(rect.top, rect.bottom)
|
||||
@@ -2520,12 +2823,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
?.let { FileUtils.readString(it)?.trim()?.lowercase() }
|
||||
?: SessionPrefs.oscMode(this)
|
||||
val controls = onScreenControls ?: return
|
||||
if (pipUi || unfoldedPanel != null) {
|
||||
if (pipUi || unfoldedPanel != null || pcKeyboardOpen ||
|
||||
(SessionState.mode == SessionService.MODE_STEAM && !SessionPrefs.builtinPad(this))) {
|
||||
onScreenButtonsVisible = false
|
||||
controls.releaseAll()
|
||||
controls.visibility = View.GONE
|
||||
steamTouchControls?.let { it.releaseAll(); it.visibility = View.GONE }
|
||||
com.droiddeck.launcher.input.SteamTouchDevice.current?.plugged = false
|
||||
applyTouchRouting()
|
||||
return
|
||||
}
|
||||
val buttonsOnly = forced == SessionPrefs.OSC_STEAM_QAM
|
||||
@@ -2538,6 +2843,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (steam == null) {
|
||||
steamTouchControls?.let { it.releaseAll(); it.visibility = View.GONE }
|
||||
com.droiddeck.launcher.input.SteamTouchDevice.current?.plugged = false
|
||||
applyTouchRouting()
|
||||
}
|
||||
controls.setButtonsOnly(buttonsOnly)
|
||||
val show = when (forced) {
|
||||
@@ -2550,29 +2856,33 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionState.mode != SessionService.MODE_DESKTOP
|
||||
}
|
||||
onScreenButtonsVisible = show
|
||||
val drawn = show && !keyboardCoversPad()
|
||||
if (steam != null) {
|
||||
if (controls.visibility == View.VISIBLE) { controls.releaseAll(); controls.visibility = View.GONE }
|
||||
if (!show) steam.releaseAll()
|
||||
steam.visibility = if (show) View.VISIBLE else View.GONE
|
||||
if (!drawn) steam.releaseAll()
|
||||
steam.visibility = if (drawn) View.VISIBLE else View.GONE
|
||||
com.droiddeck.launcher.input.SteamTouchDevice.current?.plugged = show
|
||||
applyTouchRouting()
|
||||
Log.i(TAG, "on-screen controls: Steam's touch controller " + if (show) "shown" else "hidden")
|
||||
Log.i(TAG, "on-screen controls: Steam's touch controller " + if (drawn) "shown" else "hidden")
|
||||
return
|
||||
}
|
||||
if (show == (controls.visibility == View.VISIBLE)) return
|
||||
if (!show) controls.releaseAll()
|
||||
controls.visibility = if (show) View.VISIBLE else View.GONE
|
||||
if (drawn == (controls.visibility == View.VISIBLE)) return
|
||||
if (!drawn) controls.releaseAll()
|
||||
controls.visibility = if (drawn) View.VISIBLE else View.GONE
|
||||
Log.i(TAG, "on-screen controls " + when {
|
||||
!show -> "hidden"
|
||||
!drawn -> "hidden"
|
||||
buttonsOnly -> "Steam + QAM"
|
||||
else -> "full pad"
|
||||
})
|
||||
}
|
||||
|
||||
private fun keyboardCoversPad() =
|
||||
SessionState.mode == SessionService.MODE_STEAM && AgentGuest.steamKeyboard
|
||||
|
||||
private fun sendGuestKey(code: Int, down: Boolean) {
|
||||
if (down && pipUi) return
|
||||
if (down) guestKeysDown.add(code) else guestKeysDown.remove(code)
|
||||
WaylandCompositor.nativeSendKey(code, if (down) 1 else 0)
|
||||
if (down || code !in gestureKeysDown) WaylandCompositor.nativeSendKey(code, if (down) 1 else 0)
|
||||
}
|
||||
|
||||
private fun pipBounds(): android.graphics.Rect {
|
||||
@@ -2600,6 +2910,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
releasePads()
|
||||
touchpad.cancel()
|
||||
scrollTouch.cancel()
|
||||
directTouchHold.cancel()
|
||||
guestKeysDown.toList().forEach { sendGuestKey(it, false) }
|
||||
keyboard?.hide()
|
||||
sessionClipboard.stop()
|
||||
@@ -2649,11 +2960,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
super.onStart()
|
||||
if (runtimeRemovalBlocked) return
|
||||
started = true
|
||||
refreshActivityDisplayId()
|
||||
displayManager.registerDisplayListener(displayListener, Handler(Looper.getMainLooper()))
|
||||
refreshSecondScreenDisplays()
|
||||
if (secondScreenMode != SecondScreenMode.NONE && secondScreenPresentation == null) showSecondScreen(secondScreenMode)
|
||||
startedScreens++
|
||||
SessionService.setActivityVisible(this, true)
|
||||
SessionService.setActivityVisible(this, true, sessionActivityDisplayId)
|
||||
Glasses.rescan()
|
||||
updateGlassesScreen()
|
||||
updateHandCamera()
|
||||
@@ -2667,7 +2979,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
closeSecondScreen(reset = false)
|
||||
startedScreens = (startedScreens - 1).coerceAtLeast(0)
|
||||
updateGlassesScreen()
|
||||
if (startedScreens == 0) SessionService.setActivityVisible(this, false)
|
||||
if (startedScreens == 0) SessionService.setActivityVisible(this, false, sessionActivityDisplayId)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@@ -2698,6 +3010,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (refreshActivityDisplayId() && started) {
|
||||
SessionService.setActivityDisplay(this, sessionActivityDisplayId)
|
||||
}
|
||||
applyFollowOrientation(resources.configuration)
|
||||
if (runtimeRemovalBlocked) return
|
||||
com.droiddeck.launcher.ui.Motion.refresh(this)
|
||||
@@ -2726,6 +3041,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
override fun onPause() {
|
||||
if (runtimeRemovalBlocked) { super.onPause(); return }
|
||||
sessionClipboard.stop()
|
||||
if (::touchscreenGestures.isInitialized) touchscreenGestures.cancel()
|
||||
if (::touchpad.isInitialized) touchpad.cancel()
|
||||
if (::directTouchHold.isInitialized) directTouchHold.cancel()
|
||||
uiHandler.removeCallbacks(clearRecognizedGesture)
|
||||
gestureSettingsOpen = false
|
||||
drawerOpen = false
|
||||
pcKeyboardOpen = false
|
||||
achievementsOpen = false
|
||||
@@ -2975,7 +3295,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private fun glassesOutputSize(): Pair<Int, Int>? {
|
||||
if (glassesScreen == null || glasses.width <= 0 || glasses.height <= 0) return null
|
||||
val panel = (if (glasses.sbs) glasses.width / 2 else glasses.width) to glasses.height
|
||||
val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM)
|
||||
val mode = displayMode()
|
||||
val display = com.droiddeck.launcher.session.SessionDisplay
|
||||
val choice = SessionPrefs.resolutionChoice(this, mode, panel)
|
||||
val size = display.resolveChoice(panel, choice)
|
||||
@@ -3101,6 +3421,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (!hasFocus && ::padMouse.isInitialized) padMouse.reset()
|
||||
if (runtimeRemovalBlocked) return
|
||||
if (!hasFocus) {
|
||||
if (::touchscreenGestures.isInitialized) touchscreenGestures.cancel()
|
||||
if (::touchpad.isInitialized) touchpad.cancel()
|
||||
}
|
||||
if (hasFocus && !pipUi) {
|
||||
goFullscreen()
|
||||
refreshClipboard()
|
||||
@@ -3170,7 +3494,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
private fun finishFlooded() {
|
||||
if (isFinishing) return
|
||||
val signal = com.droiddeck.launcher.ui.Themes.byId(SessionPrefs.theme(this)).signal
|
||||
val signal = com.droiddeck.launcher.ui.Themes.current(this).signal
|
||||
com.droiddeck.launcher.ui.QuitFlood.mark(signal.toArgb())
|
||||
window.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(signal.toArgb()))
|
||||
quitFlooded = true
|
||||
@@ -3201,7 +3525,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
private fun completeLeave() {
|
||||
if (isFinishing) return
|
||||
val signal = com.droiddeck.launcher.ui.Themes.byId(SessionPrefs.theme(this)).signal
|
||||
val signal = com.droiddeck.launcher.ui.Themes.current(this).signal
|
||||
com.droiddeck.launcher.ui.QuitFlood.mark(signal.toArgb())
|
||||
window.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(signal.toArgb()))
|
||||
quitFlooded = true
|
||||
@@ -3211,6 +3535,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
companion object {
|
||||
const val EXTRA_RETURN_HOME = "returnHome"
|
||||
const val EXTRA_STEAMVR = "steamVr"
|
||||
const val EXTRA_REPAIR_RUNTIME = "repairRuntime"
|
||||
private const val LEAVE_TIMEOUT_MS = 1_500L
|
||||
private const val TAG = "SessionActivity"
|
||||
private var startedScreens = 0
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import android.os.FileObserver
|
||||
import android.util.Log
|
||||
@@ -19,6 +20,8 @@ object AgentGuest {
|
||||
private val main by lazy { Handler(Looper.getMainLooper()) }
|
||||
var focusRevision by mutableIntStateOf(0)
|
||||
private set
|
||||
var steamKeyboard by mutableStateOf(false)
|
||||
private set
|
||||
private const val TAG = "AgentGuest"
|
||||
private const val POLL_MS = 50L
|
||||
private val lock = Any()
|
||||
@@ -32,6 +35,7 @@ object AgentGuest {
|
||||
observer?.stopWatching()
|
||||
observer = null
|
||||
lastFocusKey = null
|
||||
main.post { steamKeyboard = false }
|
||||
val dir = dir(context)
|
||||
dir.deleteRecursively()
|
||||
dir.mkdirs()
|
||||
@@ -43,6 +47,7 @@ object AgentGuest {
|
||||
SessionEvents.record("agent.ready", mapOf("pid" to it.optInt("pid"), "focus" to it.optBoolean("focus")))
|
||||
}
|
||||
"focus.json" -> readJson(File(dir, path))?.let(::recordFocus)
|
||||
"keyboard.json" -> readJson(File(dir, path))?.let { recordKeyboard(this, it) }
|
||||
}
|
||||
}
|
||||
}.also { it.startWatching() }
|
||||
@@ -53,9 +58,19 @@ object AgentGuest {
|
||||
synchronized(lock) {
|
||||
observer?.stopWatching()
|
||||
observer = null
|
||||
main.post { steamKeyboard = false }
|
||||
}
|
||||
}
|
||||
|
||||
private fun recordKeyboard(source: FileObserver, snapshot: JSONObject) {
|
||||
val showing = snapshot.optBoolean("steamKeyboard")
|
||||
synchronized(lock) {
|
||||
if (source !== observer) return
|
||||
main.post { steamKeyboard = showing }
|
||||
}
|
||||
SessionEvents.record("keyboard.changed", mapOf("steamKeyboard" to showing, "seq" to snapshot.optInt("seq")))
|
||||
}
|
||||
|
||||
private fun recordFocus(snapshot: JSONObject) {
|
||||
val fields = linkedMapOf<String, Any?>(
|
||||
"focusedApp" to snapshot.opt("focusedApp"),
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.BuildConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
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,
|
||||
) {
|
||||
sealed interface State {
|
||||
data object Missing : State
|
||||
data class Downloading(val progress: Float) : State
|
||||
data object Failed : State
|
||||
data object Ready : State
|
||||
}
|
||||
|
||||
private val mutableState = MutableStateFlow<State>(State.Missing)
|
||||
val state: StateFlow<State> = mutableState.asStateFlow()
|
||||
private val lock = Any()
|
||||
|
||||
val available: Boolean get() = sha256.isNotEmpty()
|
||||
private val tag get() = "pack-$name-${sha256.take(16)}"
|
||||
private val urls get() = listOfNotNull(
|
||||
"https://github.com/$REPO/releases/download/$tag/$name.tar.zst",
|
||||
BuildConfig.PR_NUMBER.takeIf { it > 0 }?.let { "https://github.com/$PR_REPO/releases/download/pr-$it/$tag.tar.zst" },
|
||||
)
|
||||
|
||||
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()
|
||||
.filter { dir ->
|
||||
dir.isDirectory && runCatching { File(dir, COMPLETE).readText().trim() }.getOrNull() == dir.name &&
|
||||
required.all { File(dir, it).isFile }
|
||||
}
|
||||
.maxByOrNull { it.lastModified() }?.name
|
||||
|
||||
fun refresh(context: Context): State {
|
||||
val now = mutableState.value
|
||||
if (now is State.Downloading) return now
|
||||
return (if (installed(context)) State.Ready else if (now == State.Failed) State.Failed else State.Missing)
|
||||
.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) {
|
||||
if (installed(app)) { mutableState.value = State.Ready; return }
|
||||
if (mutableState.value is State.Downloading) return
|
||||
mutableState.value = State.Downloading(0f)
|
||||
}
|
||||
scope.launch {
|
||||
val ok = runCatching { install(app) }.onFailure { Log.w(TAG, "$name: install failed", it) }.getOrDefault(false)
|
||||
mutableState.value = if (ok) State.Ready else State.Failed
|
||||
if (ok) onReady(app)
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
ensure(context)
|
||||
while (running()) {
|
||||
val now = state.value
|
||||
onState(now)
|
||||
when (now) {
|
||||
State.Ready -> return installed(context)
|
||||
State.Failed -> return false
|
||||
else -> try { Thread.sleep(250) } catch (e: InterruptedException) { return false }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun install(context: Context): Boolean {
|
||||
if (!available) {
|
||||
Log.w(TAG, if (reuseDeviceCopy) "$name: this build has no pack and none is installed" else "$name: this build has no pack")
|
||||
return false
|
||||
}
|
||||
val root = root(context).apply { mkdirs() }
|
||||
val archive = File(root, "$sha256.tar.zst")
|
||||
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() }
|
||||
}
|
||||
if (!downloaded) return false
|
||||
val staging = File(root, "$sha256.partial")
|
||||
staging.deleteRecursively()
|
||||
try {
|
||||
if (!FileInputStream(archive).use { TarZst.extract(it, staging) }) return false
|
||||
check(required.all { File(staging, it).isFile }) { "$name pack is incomplete" }
|
||||
File(staging, COMPLETE).writeText(sha256)
|
||||
val target = dir(context)
|
||||
target.deleteRecursively()
|
||||
check(staging.renameTo(target)) { "could not move the $name pack into place" }
|
||||
} finally {
|
||||
staging.deleteRecursively()
|
||||
archive.delete()
|
||||
}
|
||||
root.listFiles().orEmpty().filter { it.name != sha256 }.forEach { it.deleteRecursively() }
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AddonPack"
|
||||
private const val REPO = "Droid-Deck/DroidDeck-Components"
|
||||
private const val PR_REPO = "Droid-Deck/DroidDeck-CI"
|
||||
const val COMPLETE = ".complete"
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@JvmField
|
||||
val PROOT = AddonPack("proot", BuildConfig.PACK_PROOT_SHA256, BuildConfig.PACK_PROOT_SIZE,
|
||||
listOf("libproot.so", "libproot-loader.so"))
|
||||
@JvmField
|
||||
val DIRECTAUDIO = AddonPack("directaudio", BuildConfig.PACK_DIRECTAUDIO_SHA256, BuildConfig.PACK_DIRECTAUDIO_SIZE,
|
||||
listOf("linux-wine11/version.txt", "linux-wine11-systhread/version.txt"))
|
||||
val TRACKING = AddonPack("tracking", BuildConfig.PACK_TRACKING_SHA256, BuildConfig.PACK_TRACKING_SIZE, listOf(
|
||||
"hand_landmarker.task", "hand_landmarker_lite.task", "pose_landmarker_lite.task",
|
||||
"libopencv_java4.so", "libmediapipe_tasks_jni.so"))
|
||||
val XBOX = AddonPack("xbox", BuildConfig.PACK_XBOX_SHA256, BuildConfig.PACK_XBOX_SIZE,
|
||||
listOf("version", "xodus-cli", "xodus-service"))
|
||||
val COMET = AddonPack("comet", BuildConfig.PACK_COMET_SHA256, BuildConfig.PACK_COMET_SIZE,
|
||||
listOf("version", "comet"))
|
||||
}
|
||||
}
|
||||
@@ -207,9 +207,11 @@ object DeviceReport {
|
||||
h("Drivers")
|
||||
val gpu = com.droiddeck.launcher.gpu.GpuInfo.detect()
|
||||
k("GPU", "${gpu.name} · ${gpu.family.label(gpu.name)} · ${gpu.supportText}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "")
|
||||
if (gpu.model == 740) k("TP UBWC hint", com.droiddeck.launcher.gpu.TpUbwcHint.forThisDevice() ?: "driver default")
|
||||
k("Driver mode", SessionPrefs.gpuDriverMode(context) +
|
||||
(com.droiddeck.launcher.gpu.DriverPairs.recommendedKey(gpu, com.droiddeck.launcher.gpu.DriverPairs.from(
|
||||
com.droiddeck.launcher.gpu.TurnipReleases.cached(context)))?.let { " (recommended pair: $it)" } ?: ""))
|
||||
com.droiddeck.launcher.gpu.TurnipReleases.cached(context)), com.droiddeck.launcher.gpu.TurnipReleases.autoPicks(context),
|
||||
com.droiddeck.launcher.gpu.AutoDriverTrial.bad(context))?.let { " (recommended pair: $it)" } ?: ""))
|
||||
val turnip = TurnipDriver(context)
|
||||
val androidChoice = SessionPrefs.androidDriver(context)
|
||||
val displayId = androidChoice.ifEmpty { turnip.autoId() }
|
||||
@@ -236,6 +238,9 @@ object DeviceReport {
|
||||
k("Installed version", LinuxRuntimeInstaller.installedVersion(context))
|
||||
k("Runtime ready", LinuxRuntime.isInstalled(context))
|
||||
k("Desktop installed", DesktopCatalog.desktopInstalled(context))
|
||||
k("Interrupted installs", DesktopCatalog.interrupted(context).joinToString { "${it.id} ${it.version}" }.ifEmpty { "none" })
|
||||
val broken = runCatching { com.droiddeck.launcher.runtime.RuntimeHealth.brokenLibraryLinks(context) }.getOrDefault(emptyList())
|
||||
k("Broken library links", if (broken.isEmpty()) "none" else "${broken.size}: ${broken.take(8).joinToString()}")
|
||||
k("Runtime root", LinuxRuntime.rootDir(context).path)
|
||||
|
||||
h("Settings in effect")
|
||||
@@ -260,6 +265,7 @@ object DeviceReport {
|
||||
k("Guest host name", SessionPrefs.guestHostname(context))
|
||||
k("DirectAudio for games", SessionPrefs.directAudio(context))
|
||||
k("Stretch 16:9 to panel", SessionPrefs.stretch16x9(context))
|
||||
k("Return to launcher after a game", SessionPrefs.returnAfterGame(context))
|
||||
k("Microphone", SessionPrefs.micEnabled(context))
|
||||
k("On-screen controls", SessionPrefs.oscMode(context))
|
||||
k("Touch mode", SessionPrefs.touchMode(context))
|
||||
|
||||
@@ -38,6 +38,7 @@ object DownloadSource {
|
||||
private val CATALOGS = listOf(
|
||||
"https://raw.githubusercontent.com/Droid-Deck/DroidDeck-CI/",
|
||||
"https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/",
|
||||
"https://raw.githubusercontent.com/Droid-Deck/Drivers-CI/",
|
||||
)
|
||||
|
||||
data class Mirror(val host: String, val api: Boolean)
|
||||
|
||||
@@ -54,7 +54,12 @@ public final class Downloader {
|
||||
java.util.function.Consumer<Float> progress) {
|
||||
if (expectedSha256 != null && !expectedSha256.matches("(?i)[0-9a-f]{64}"))
|
||||
throw new IllegalArgumentException("Expected SHA-256 must be 64 hex characters");
|
||||
return downloadCandidates(url, DownloadSource.INSTANCE.candidates(url), destination, resume, expectedSha256, progress);
|
||||
return downloadCandidates(url, sources(url, expectedSha256), destination, resume, expectedSha256, progress);
|
||||
}
|
||||
|
||||
/** A third-party mirror may only serve bytes that can be checked against a digest from a direct source. */
|
||||
static List<String> sources(String url, String expectedSha256) {
|
||||
return expectedSha256 == null ? java.util.Collections.singletonList(url) : DownloadSource.INSTANCE.candidates(url);
|
||||
}
|
||||
|
||||
// Package-private for the local multi-source HTTP test.
|
||||
|
||||
@@ -29,6 +29,7 @@ object GameEnvironment {
|
||||
|
||||
fun defaults(preset: String, forceSsbs: Boolean = false): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
"MESA_SHADER_CACHE_DISABLE" to "false",
|
||||
"DROIDDECK_ANTICHEAT_OFFLINE" to "1",
|
||||
"VKD3D_FEATURE_LEVEL" to "12_2",
|
||||
"VKD3D_SHADER_MODEL" to "6_9",
|
||||
"DOTNET_GCRegionRange" to "800000000",
|
||||
|
||||
@@ -32,6 +32,7 @@ object GameEnvironmentOptions {
|
||||
Option("MESA_EXTENSION_MAX_YEAR", "", 0, Type.TEXT),
|
||||
Option("MESA_GL_VERSION_OVERRIDE", "", 0, Type.TEXT),
|
||||
Option("PULSE_LATENCY_MSEC", "", 0, Type.NUMBER),
|
||||
Option("DROIDDECK_ANTICHEAT_OFFLINE", "1", R.string.game_env_anticheat_offline, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("WINE_LARGE_ADDRESS_AWARE", "0", 0, Type.TOGGLE, listOf("0", "1")),
|
||||
Option("WINEDLLOVERRIDES", "", 0, Type.TEXT),
|
||||
Option("GALLIUM_HUD", "simple", 0, Type.MULTIPLE, listOf("simple", "fps", "frametime")),
|
||||
|
||||
@@ -2,14 +2,51 @@ package com.droiddeck.launcher.core
|
||||
|
||||
object LogRedactor {
|
||||
private val EMAIL = Regex("[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}")
|
||||
private val HIDL_SHARED_LIBRARY = Regex(
|
||||
"(?:android\\.(?:hardware|frameworks|hidl|system)|vendor\\.[A-Za-z0-9_]+\\.hardware)" +
|
||||
"(?:\\.[A-Za-z0-9_]+)+@[0-9]+\\.[0-9]+(?:-[A-Za-z0-9_]+)*\\.so"
|
||||
)
|
||||
private val hidlMatcher = object : ThreadLocal<java.util.regex.Matcher>() {
|
||||
override fun initialValue(): java.util.regex.Matcher = HIDL_SHARED_LIBRARY.toPattern().matcher("")
|
||||
}
|
||||
|
||||
private fun isHidlLoaderDiagnostic(input: String, match: MatchResult): Boolean {
|
||||
val matcher = hidlMatcher.get()!!
|
||||
val isLibrary = try { matcher.reset(match.value).matches() } finally { matcher.reset("") }
|
||||
if (!isLibrary) return false
|
||||
val start = match.range.first
|
||||
val end = match.range.last + 1
|
||||
val prefix = "library \""
|
||||
val suffix = "\" not found"
|
||||
val earliestQuote = (start - 4096).coerceAtLeast(0)
|
||||
var quote = start - 1
|
||||
while (quote >= earliestQuote && input[quote] != '"') {
|
||||
val char = input[quote]
|
||||
if (!char.isLetterOrDigit() && char !in "/._-") return false
|
||||
quote--
|
||||
}
|
||||
if (quote < earliestQuote) return false
|
||||
val pathStart = quote + 1
|
||||
if (pathStart < start && (input[start - 1] != '/' ||
|
||||
!(input.regionMatches(pathStart, "/vendor/", 0, 8) ||
|
||||
input.regionMatches(pathStart, "/system/", 0, 8)))) return false
|
||||
val prefixStart = quote - prefix.length + 1
|
||||
val suffixEnd = end + suffix.length
|
||||
return prefixStart >= 0 && suffixEnd <= input.length &&
|
||||
input.regionMatches(prefixStart, prefix, 0, prefix.length, ignoreCase = true) &&
|
||||
input.regionMatches(end, suffix, 0, suffix.length, ignoreCase = true) &&
|
||||
(prefixStart == 0 || !(input[prefixStart - 1].isLetterOrDigit() || input[prefixStart - 1] == '_')) &&
|
||||
(suffixEnd == input.length || !(input[suffixEnd].isLetterOrDigit() || input[suffixEnd] == '_'))
|
||||
}
|
||||
|
||||
private val JWT_LABELLED = Regex("(?i)(\\bJWT[\\s=:]+)(\\d{8,})")
|
||||
private val JWT_BASE64 = Regex("ey[A-Za-z0-9_\\-]{6,}\\.[A-Za-z0-9_\\-]{6,}\\.[A-Za-z0-9_\\-]{6,}")
|
||||
private val GUID = Regex("\\b[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\\b")
|
||||
private val SECRET_KV = Regex(
|
||||
"(?i)\\b(access[_-]?token|refresh[_-]?token|auth[_-]?token|authtoken|token|authcode|" +
|
||||
"(?i)(\\b(?:access[_-]?token|refresh[_-]?token|auth[_-]?token|authtoken|token|authcode|" +
|
||||
"auth[_-]?ticket|ticket|sessionid|steamloginsecure|webapikey|api[_-]?key|" +
|
||||
"machine[_-]?auth(?:[_-]?token)?|machineauth|password|passwd|pwd|secret)" +
|
||||
"(\\s*[=:]\\s*|=)([^\\s\"'<>&;,]{4,})"
|
||||
"machine[_-]?auth(?:[_-]?token)?|machineauth|password|passwd|pwd|secret)[\"']?\\s*[=:]\\s*)" +
|
||||
"(?:([\"'])((?:\\\\.|(?!\\2)[^\\\\\\r\\n]){4,})\\2|([^\\s\"'<>&;,]{4,}))"
|
||||
)
|
||||
private val GUARD_CODE = Regex(
|
||||
"(?i)((?:steam\\s*)?guard\\s*code[\\s:=]*|two[\\s-]?factor[\\s:=]*|2fa[\\s:=]*)([A-Za-z0-9]{5})"
|
||||
@@ -146,16 +183,21 @@ object LogRedactor {
|
||||
return out
|
||||
}
|
||||
|
||||
fun isText(file: java.io.File): Boolean = try {
|
||||
file.isFile && file.length() < 64L * 1024 * 1024 && file.inputStream().use { input ->
|
||||
/** The at-rest rewrite reads a file into memory twice; larger logs are only scrubbed on the way out. */
|
||||
const val IN_MEMORY_SCRUB_MAX = 64L * 1024 * 1024
|
||||
|
||||
fun isBinary(file: java.io.File): Boolean = try {
|
||||
file.inputStream().use { input ->
|
||||
val buf = ByteArray(4096)
|
||||
val n = input.read(buf)
|
||||
n <= 0 || (0 until n).none { buf[it].toInt() == 0 }
|
||||
n > 0 && (0 until n).any { buf[it].toInt() == 0 }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
fun isText(file: java.io.File): Boolean = file.isFile && file.length() < IN_MEMORY_SCRUB_MAX && !isBinary(file)
|
||||
|
||||
const val RULES_VERSION = 3
|
||||
|
||||
private val LAN_IPV4 = Regex(
|
||||
@@ -176,8 +218,62 @@ object LogRedactor {
|
||||
}
|
||||
}
|
||||
|
||||
private const val SHARE_LINE_MAX = 64 * 1024
|
||||
|
||||
fun scrubForShare(src: java.io.File, out: java.io.Writer) {
|
||||
src.forEachLine { line -> out.write(redactForShare(line)); out.write("\n") }
|
||||
src.inputStream().buffered().use { input ->
|
||||
input.mark(2)
|
||||
val first = input.read()
|
||||
val second = input.read()
|
||||
input.reset()
|
||||
if ((first == 0xff && second == 0xfe) || (first == 0xfe && second == 0xff)) {
|
||||
out.write("<unsupported encoding; file withheld>\n")
|
||||
return
|
||||
}
|
||||
val decoder = Charsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(java.nio.charset.CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(java.nio.charset.CodingErrorAction.REPORT)
|
||||
java.io.InputStreamReader(input, decoder).use { reader ->
|
||||
val buffer = CharArray(8192)
|
||||
val line = StringBuilder()
|
||||
var withheld = false
|
||||
var afterCr = false
|
||||
fun emit() {
|
||||
out.write(if (withheld) "<unsupported or oversized line withheld>" else redactForShare(line.toString()))
|
||||
out.write("\n")
|
||||
line.setLength(0)
|
||||
withheld = false
|
||||
}
|
||||
try {
|
||||
while (true) {
|
||||
val size = reader.read(buffer)
|
||||
if (size < 0) break
|
||||
for (i in 0 until size) {
|
||||
val ch = buffer[i]
|
||||
if (afterCr) {
|
||||
afterCr = false
|
||||
if (ch == '\n') continue
|
||||
}
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
emit()
|
||||
afterCr = ch == '\r'
|
||||
} else if (!withheld) {
|
||||
// Splitting a line can separate a credential from its label.
|
||||
if (ch == '\u0000' || line.length == SHARE_LINE_MAX) {
|
||||
withheld = true
|
||||
line.setLength(0)
|
||||
} else {
|
||||
line.append(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (withheld || line.isNotEmpty()) emit()
|
||||
} catch (_: java.nio.charset.CharacterCodingException) {
|
||||
out.write("<unsupported encoding; file withheld>\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun quoted(value: String): String {
|
||||
@@ -232,7 +328,7 @@ object LogRedactor {
|
||||
if (out.has("jwt")) out.set(JWT_LABELLED.replace(out.text) { "${it.groupValues[1]}<redacted:jwt>" })
|
||||
if ("ey" in out.text) out.set(JWT_BASE64.replace(out.text, "<redacted:jwt>"))
|
||||
if (out.has("token", "auth", "ticket", "sessionid", "steamloginsecure", "api", "machine", "pass", "pwd", "secret")) {
|
||||
out.set(SECRET_KV.replace(out.text) { "${it.groupValues[1]}${it.groupValues[2]}<redacted:token>" })
|
||||
out.set(SECRET_KV.replace(out.text) { "${it.groupValues[1]}${it.groupValues[2]}<redacted:token>${it.groupValues[2]}" })
|
||||
}
|
||||
if (out.has("guard", "factor", "2fa")) out.set(GUARD_CODE.replace(out.text) { "${it.groupValues[1]}<redacted:code>" })
|
||||
if (out.hasRun(32, ::isHex)) out.set(WEBAPI_KEY.replace(out.text, "<redacted:key>"))
|
||||
@@ -250,7 +346,9 @@ object LogRedactor {
|
||||
}
|
||||
if (out.has("login")) out.set(LOGIN_KV.replace(out.text) { "${it.groupValues[1]}${it.groupValues[2]}${quoted(it.groupValues[3])}" })
|
||||
for (r in accounts) if (r.needs == null || out.has(r.needs)) out.set(r.regex.replace(out.text, "<redacted:account>"))
|
||||
if ('@' in out.text) out.set(EMAIL.replace(out.text, "<redacted:email>"))
|
||||
if ('@' in out.text) out.set(EMAIL.replace(out.text) { m ->
|
||||
m.value.takeIf { isHidlLoaderDiagnostic(out.text, m) } ?: "<redacted:email>"
|
||||
})
|
||||
if (out.has("jwt", "token", "ticket", "sessionid", "steamloginsecure", "machineauth")) {
|
||||
out.set(RESIDUAL.replace(out.text) { "${it.groupValues[1]}=<redacted:token>" })
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class AppFilesProvider : DocumentsProvider() {
|
||||
|
||||
override fun deleteDocument(documentId: String) {
|
||||
val file = fileFor(documentId)
|
||||
if (file.absolutePath == base.absolutePath) throw FileNotFoundException("the app folder itself is not deleted")
|
||||
if (file.canonicalPath == baseCanonical) throw FileNotFoundException("the app folder itself is not deleted")
|
||||
try {
|
||||
deleteTree(file.toPath())
|
||||
} catch (e: IOException) {
|
||||
@@ -154,7 +154,7 @@ class AppFilesProvider : DocumentsProvider() {
|
||||
|
||||
private fun fileFor(documentId: String): File {
|
||||
val f = File(documentId)
|
||||
if (!f.isAbsolute || !inside(f)) throw FileNotFoundException("$documentId is outside the app's files")
|
||||
if (!f.isAbsolute || f.normalize().path != documentId || !inside(f)) throw FileNotFoundException("$documentId is outside the app's files")
|
||||
if (!f.exists()) throw FileNotFoundException("$documentId not found")
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -249,11 +250,11 @@ fun CollapsibleRail(
|
||||
if (active) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||
shape,
|
||||
)
|
||||
.clickable(onClick = item.onClick)
|
||||
.clickable(role = androidx.compose.ui.semantics.Role.Button, onClick = item.onClick)
|
||||
.padding(horizontal = if (collapsed) 2.dp else 11.dp, vertical = if (collapsed) 6.dp else 9.dp)
|
||||
} else {
|
||||
base
|
||||
.clickable(onClick = item.onClick)
|
||||
.clickable(role = androidx.compose.ui.semantics.Role.Button, onClick = item.onClick)
|
||||
.background(if (active) MaterialTheme.colorScheme.primary.copy(alpha = 0.14f) else Color.Transparent)
|
||||
.padding(horizontal = if (collapsed) 2.dp else 13.dp, vertical = if (collapsed) 7.dp else 11.dp)
|
||||
}
|
||||
@@ -337,7 +338,7 @@ fun RailTopTabs(
|
||||
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.4f)),
|
||||
)
|
||||
links.forEach { link ->
|
||||
TopTabCell(link.icon, link.label, selected = false, onClick = link.onClick, modifier = Modifier.weight(1f))
|
||||
TopTabCell(link.icon, link.label, selected = false, onClick = link.onClick, isTab = false, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +353,7 @@ private fun TopTabCell(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isTab: Boolean = true,
|
||||
) {
|
||||
val tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Column(
|
||||
@@ -359,7 +361,13 @@ private fun TopTabCell(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(9.dp))
|
||||
.background(if (selected) MaterialTheme.colorScheme.primary.copy(alpha = 0.14f) else Color.Transparent)
|
||||
.clickable(onClick = onClick)
|
||||
.then(
|
||||
if (isTab) {
|
||||
Modifier.selectable(selected = selected, role = androidx.compose.ui.semantics.Role.Tab, onClick = onClick)
|
||||
} else {
|
||||
Modifier.clickable(role = androidx.compose.ui.semantics.Role.Button, onClick = onClick)
|
||||
},
|
||||
)
|
||||
.padding(vertical = 6.dp, horizontal = 1.dp),
|
||||
) {
|
||||
Icon(icon, contentDescription = label, tint = tint, modifier = Modifier.size(20.dp))
|
||||
|
||||
@@ -193,7 +193,7 @@ internal fun FileItemRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 3.dp)
|
||||
.combinedClickable(
|
||||
.combinedClickable(role = androidx.compose.ui.semantics.Role.Button,
|
||||
onClick = { if (selectionMode) onToggleSelect() else onTap() },
|
||||
onLongClick = onLongPress,
|
||||
),
|
||||
@@ -302,7 +302,7 @@ internal fun ParentFolderRow(compact: Boolean = false, onTap: () -> Unit) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 3.dp)
|
||||
.clickable(onClick = onTap),
|
||||
.clickable(role = androidx.compose.ui.semantics.Role.Button, onClick = onTap),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
@@ -422,7 +422,7 @@ private fun FavoriteCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 3.dp)
|
||||
.clickable(onClick = onJump),
|
||||
.clickable(role = androidx.compose.ui.semantics.Role.Button, onClick = onJump),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
@@ -526,7 +526,7 @@ internal fun FileGridTile(
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.combinedClickable(
|
||||
.combinedClickable(role = androidx.compose.ui.semantics.Role.Button,
|
||||
onClick = { if (selectionMode) onToggleSelect() else onTap() },
|
||||
onLongClick = onLongPress,
|
||||
),
|
||||
|
||||
@@ -90,6 +90,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
@@ -151,8 +152,11 @@ fun FileManagerScreen(
|
||||
var conflictChoice by remember { mutableStateOf<ConflictChoice?>(null) }
|
||||
var conflictApplyToAll by remember { mutableStateOf(false) }
|
||||
var operationJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }
|
||||
var cancelledJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }
|
||||
var pendingBulkDelete by remember { mutableStateOf<List<File>>(emptyList()) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
val visibleEntries = if (searchQuery.isBlank()) entries
|
||||
else entries.filter { it.name.contains(searchQuery, ignoreCase = true) }
|
||||
var showSearch by remember { mutableStateOf(false) }
|
||||
var sortBy by remember { mutableStateOf(browsePrefs.getString("fmSortBy", "name") ?: "name") }
|
||||
var sortDesc by remember { mutableStateOf(browsePrefs.getBoolean("fmSortDesc", false)) }
|
||||
@@ -174,16 +178,20 @@ fun FileManagerScreen(
|
||||
var showFavorites by remember { mutableStateOf(false) }
|
||||
var favTick by remember { mutableIntStateOf(0) }
|
||||
|
||||
var listingJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }
|
||||
|
||||
fun loadDirectory(dir: File, resetScroll: Boolean = true) {
|
||||
currentDir = dir
|
||||
if (pickMode) pickPrefs.edit().putString("lastFilePickerDir", dir.absolutePath).apply()
|
||||
scope.launch {
|
||||
listingJob?.cancel()
|
||||
listingJob = scope.launch {
|
||||
val list = withContext(Dispatchers.IO) {
|
||||
dir.listFiles()?.toList()
|
||||
?.filter { if (pickDirMode) it.isDirectory else !pickMode || it.isDirectory || matchesPickExt(it) }
|
||||
?.filter { showHidden || !it.name.startsWith(".") }
|
||||
?.sortedWith(comparatorFor(sortBy, sortDesc)) ?: emptyList()
|
||||
}
|
||||
if (currentDir != dir) return@launch
|
||||
entries = list
|
||||
if (resetScroll) listState.scrollToItem(0)
|
||||
}
|
||||
@@ -220,7 +228,7 @@ fun FileManagerScreen(
|
||||
|
||||
fun uniqueDestination(dir: File, name: String): File {
|
||||
var candidate = File(dir, name)
|
||||
if (!candidate.exists()) return candidate
|
||||
if (!java.nio.file.Files.exists(candidate.toPath(), java.nio.file.LinkOption.NOFOLLOW_LINKS)) return candidate
|
||||
val dot = name.lastIndexOf('.')
|
||||
val base = if (dot > 0) name.substring(0, dot) else name
|
||||
val ext = if (dot > 0) name.substring(dot) else ""
|
||||
@@ -228,7 +236,7 @@ fun FileManagerScreen(
|
||||
do {
|
||||
candidate = File(dir, "$base ($i)$ext")
|
||||
i++
|
||||
} while (candidate.exists())
|
||||
} while (java.nio.file.Files.exists(candidate.toPath(), java.nio.file.LinkOption.NOFOLLOW_LINKS))
|
||||
return candidate
|
||||
}
|
||||
|
||||
@@ -252,7 +260,7 @@ fun FileManagerScreen(
|
||||
|
||||
fun performPaste() {
|
||||
val sources = clipboardFiles
|
||||
if (sources.isEmpty()) return
|
||||
if (sources.isEmpty() || isOperationRunning || operationJob?.isActive == true || cancelledJob?.isCompleted == false) return
|
||||
val dstDir = currentDir
|
||||
val cut = isCutOperation
|
||||
|
||||
@@ -261,6 +269,7 @@ fun FileManagerScreen(
|
||||
operationDeterminate = true
|
||||
operationLabel = context.getString(if (cut) R.string.fm_op_moving else R.string.fm_op_copying)
|
||||
isOperationRunning = true
|
||||
val job = coroutineContext[kotlinx.coroutines.Job]
|
||||
|
||||
var applyToAll: ConflictChoice? = null
|
||||
var failed = 0
|
||||
@@ -278,7 +287,7 @@ fun FileManagerScreen(
|
||||
}
|
||||
|
||||
var dst = File(dstDir, src.name)
|
||||
if (dst.exists()) {
|
||||
if (java.nio.file.Files.exists(dst.toPath(), java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
|
||||
val choice = applyToAll ?: askConflict(src)?.also {
|
||||
if (conflictApplyToAll) applyToAll = it
|
||||
} ?: run { skipped++; null } ?: continue
|
||||
@@ -296,6 +305,7 @@ fun FileManagerScreen(
|
||||
}
|
||||
var lastPct = -1
|
||||
val onProgress = FileOps.ProgressCallback { copied, total ->
|
||||
job?.ensureActive()
|
||||
val pct = if (total > 0) ((copied * 100) / total).toInt() else 100
|
||||
if (pct != lastPct) {
|
||||
lastPct = pct
|
||||
@@ -304,8 +314,8 @@ fun FileManagerScreen(
|
||||
}
|
||||
val target = dst
|
||||
val ok = withContext(Dispatchers.IO) {
|
||||
if (cut) FileOps.moveWithProgress(src, target, onProgress)
|
||||
else FileOps.copyWithProgress(src, target, onProgress)
|
||||
if (cut) FileOps.moveWithProgress(src, target, onProgress) { job?.isActive == false }
|
||||
else FileOps.copyWithProgress(src, target, onProgress) { job?.isActive == false }
|
||||
}
|
||||
if (ok) done++ else failed++
|
||||
}
|
||||
@@ -791,19 +801,21 @@ fun FileManagerScreen(
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
val allVisibleSelected = visibleEntries.isNotEmpty() && visibleEntries.all { it.absolutePath in selectedPaths }
|
||||
val anyVisibleSelected = visibleEntries.any { it.absolutePath in selectedPaths }
|
||||
val selBarPadding = PaddingValues(horizontal = 10.dp, vertical = 2.dp)
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
selectedPaths = if (selectedPaths.size == entries.size) emptySet()
|
||||
else entries.map { it.absolutePath }.toSet()
|
||||
val shown = visibleEntries.map { it.absolutePath }
|
||||
selectedPaths = if (allVisibleSelected) selectedPaths - shown.toSet() else selectedPaths + shown
|
||||
},
|
||||
contentPadding = selBarPadding,
|
||||
) { Text(if (selectedPaths.size == entries.size) stringResource(R.string.fm_select_none) else stringResource(R.string.fm_select_all), fontSize = 12.sp) }
|
||||
) { Text(if (allVisibleSelected) stringResource(R.string.fm_select_none) else stringResource(R.string.fm_select_all), fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
enabled = selectedPaths.isNotEmpty(),
|
||||
enabled = anyVisibleSelected,
|
||||
onClick = {
|
||||
clipboardFiles = entries.filter { it.absolutePath in selectedPaths }
|
||||
clipboardFiles = visibleEntries.filter { it.absolutePath in selectedPaths }
|
||||
isCutOperation = false
|
||||
selectionMode = false
|
||||
selectedPaths = emptySet()
|
||||
@@ -812,9 +824,9 @@ fun FileManagerScreen(
|
||||
) { Text(stringResource(R.string.fm_copy), fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
enabled = selectedPaths.isNotEmpty(),
|
||||
enabled = anyVisibleSelected,
|
||||
onClick = {
|
||||
clipboardFiles = entries.filter { it.absolutePath in selectedPaths }
|
||||
clipboardFiles = visibleEntries.filter { it.absolutePath in selectedPaths }
|
||||
isCutOperation = true
|
||||
selectionMode = false
|
||||
selectedPaths = emptySet()
|
||||
@@ -823,8 +835,8 @@ fun FileManagerScreen(
|
||||
) { Text(stringResource(R.string.fm_cut), fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
enabled = selectedPaths.isNotEmpty(),
|
||||
onClick = { pendingBulkDelete = entries.filter { it.absolutePath in selectedPaths } },
|
||||
enabled = anyVisibleSelected,
|
||||
onClick = { pendingBulkDelete = visibleEntries.filter { it.absolutePath in selectedPaths } },
|
||||
contentPadding = selBarPadding,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
|
||||
) { Text(stringResource(R.string.common_delete), color = MaterialTheme.colorScheme.error, fontSize = 12.sp) }
|
||||
@@ -842,7 +854,7 @@ fun FileManagerScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.1f))
|
||||
.clickable { performPaste() }
|
||||
.clickable(enabled = !isOperationRunning) { performPaste() }
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(Icons.Filled.ContentPaste, stringResource(R.string.fm_paste), tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp))
|
||||
@@ -878,6 +890,7 @@ fun FileManagerScreen(
|
||||
)
|
||||
if (operationJob != null) {
|
||||
TextButton(onClick = {
|
||||
cancelledJob = operationJob
|
||||
operationJob?.cancel()
|
||||
operationJob = null
|
||||
isOperationRunning = false
|
||||
@@ -964,8 +977,7 @@ fun FileManagerScreen(
|
||||
.fillMaxSize()
|
||||
.nestedScroll(pullState.nestedScrollConnection),
|
||||
) {
|
||||
val shownEntries = if (searchQuery.isBlank()) entries
|
||||
else entries.filter { it.name.contains(searchQuery, ignoreCase = true) }
|
||||
val shownEntries = visibleEntries
|
||||
|
||||
if (showGrid && entries.isNotEmpty()) {
|
||||
LazyVerticalGrid(
|
||||
|
||||
@@ -45,23 +45,26 @@ object FileOps {
|
||||
return f.length()
|
||||
}
|
||||
|
||||
fun moveWithProgress(src: File, dst: File, progress: ProgressCallback?): Boolean {
|
||||
fun moveWithProgress(src: File, dst: File, progress: ProgressCallback?, isCancelled: () -> Boolean = { false }): Boolean {
|
||||
if (sameFile(src, dst)) return true
|
||||
if (isCancelled()) return false
|
||||
if (!dst.exists() && src.renameTo(dst)) {
|
||||
progress?.onProgress(1, 1)
|
||||
return true
|
||||
}
|
||||
if (!copyWithProgress(src, dst, progress)) return false
|
||||
if (!copyWithProgress(src, dst, progress, isCancelled)) return false
|
||||
if (isCancelled()) return false
|
||||
return delete(src)
|
||||
}
|
||||
|
||||
fun copyWithProgress(src: File, dst: File, progress: ProgressCallback?): Boolean {
|
||||
fun copyWithProgress(src: File, dst: File, progress: ProgressCallback?, isCancelled: () -> Boolean = { false }): Boolean {
|
||||
val total = maxOf(totalSize(src), 1L)
|
||||
return copyWithProgress(src, dst, longArrayOf(0), total, progress)
|
||||
return copyWithProgress(src, dst, longArrayOf(0), total, progress, isCancelled)
|
||||
}
|
||||
|
||||
private fun copyWithProgress(src: File, dst: File, done: LongArray, total: Long, progress: ProgressCallback?): Boolean {
|
||||
if (isSymlink(src)) return true
|
||||
private fun copyWithProgress(src: File, dst: File, done: LongArray, total: Long, progress: ProgressCallback?, isCancelled: () -> Boolean): Boolean {
|
||||
if (isCancelled()) throw java.util.concurrent.CancellationException("Copy cancelled: $src")
|
||||
if (isSymlink(src)) return copyLink(src, dst)
|
||||
if (sameFile(src, dst)) return true
|
||||
if (src.isDirectory && isWithin(dst, src)) {
|
||||
Log.e(TAG, "Refusing to copy directory into itself: $src -> $dst")
|
||||
@@ -71,7 +74,7 @@ object FileOps {
|
||||
if (!dst.exists() && !dst.mkdirs()) return false
|
||||
var allOk = true
|
||||
src.list()?.forEach { name ->
|
||||
if (!copyWithProgress(File(src, name), File(dst, name), done, total, progress)) {
|
||||
if (!copyWithProgress(File(src, name), File(dst, name), done, total, progress, isCancelled)) {
|
||||
Log.e(TAG, "Failed to copy: ${File(src, name)}")
|
||||
allOk = false
|
||||
}
|
||||
@@ -81,13 +84,41 @@ object FileOps {
|
||||
val parent = dst.parentFile
|
||||
if (!src.exists() || (parent != null && !parent.exists() && !parent.mkdirs())) return false
|
||||
val chunk = 4L * 1024 * 1024
|
||||
var staging: File? = null
|
||||
var created = false
|
||||
var output: java.nio.channels.FileChannel? = null
|
||||
try {
|
||||
output = try {
|
||||
java.nio.channels.FileChannel.open(dst.toPath(), java.nio.file.StandardOpenOption.WRITE, java.nio.file.StandardOpenOption.CREATE_NEW)
|
||||
.also { created = true }
|
||||
} catch (e: java.nio.file.FileAlreadyExistsException) {
|
||||
null
|
||||
}
|
||||
val target = if (!created && isSymlink(dst)) dst.canonicalFile else dst
|
||||
var permissions: Set<java.nio.file.attribute.PosixFilePermission>? = null
|
||||
if (output == null) {
|
||||
if (isSymlink(target) || target.parentFile?.isDirectory != true) {
|
||||
throw IOException("Destination symlink target is not a writable directory entry: $dst")
|
||||
}
|
||||
if (target.isDirectory) throw IOException("Destination is a directory: $target")
|
||||
permissions = try {
|
||||
Files.getPosixFilePermissions(target.toPath())
|
||||
} catch (e: java.nio.file.NoSuchFileException) {
|
||||
null
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
null
|
||||
}
|
||||
if (target.exists() && !target.canWrite()) throw IOException("Destination is not writable: $target")
|
||||
val directory = Files.createTempDirectory(checkNotNull(target.absoluteFile.parentFile).toPath(), ".droiddeck-copy-")
|
||||
staging = directory.toFile()
|
||||
output = FileOutputStream(File(staging, "file")).channel
|
||||
}
|
||||
FileInputStream(src).channel.use { input ->
|
||||
FileOutputStream(dst).channel.use { output ->
|
||||
checkNotNull(output).use { channel ->
|
||||
val size = input.size()
|
||||
var position = 0L
|
||||
while (position < size) {
|
||||
val transferred = input.transferTo(position, minOf(chunk, size - position), output)
|
||||
val transferred = input.transferTo(position, minOf(chunk, size - position), channel)
|
||||
if (transferred <= 0) break
|
||||
position += transferred
|
||||
done[0] += transferred
|
||||
@@ -96,11 +127,54 @@ object FileOps {
|
||||
if (position < size) throw IOException("Incomplete copy: $position/$size bytes")
|
||||
}
|
||||
}
|
||||
staging?.let { directory ->
|
||||
val copied = File(directory, "file").toPath()
|
||||
if (permissions != null && Files.getPosixFilePermissions(copied) != permissions) {
|
||||
Files.setPosixFilePermissions(copied, permissions)
|
||||
}
|
||||
if (isCancelled()) throw java.util.concurrent.CancellationException("Copy cancelled: $src")
|
||||
Files.move(copied, target.toPath(), java.nio.file.StandardCopyOption.ATOMIC_MOVE)
|
||||
}
|
||||
return true
|
||||
} catch (e: java.util.concurrent.CancellationException) {
|
||||
if (created) dst.delete()
|
||||
throw e
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to copy file: $src to $dst", e)
|
||||
dst.delete()
|
||||
if (created) dst.delete()
|
||||
return false
|
||||
} finally {
|
||||
output?.close()
|
||||
staging?.let { directory ->
|
||||
File(directory, "file").delete()
|
||||
directory.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyLink(src: File, dst: File): Boolean {
|
||||
if (src.absoluteFile == dst.absoluteFile) return true
|
||||
return try {
|
||||
val target = Files.readSymbolicLink(src.toPath())
|
||||
val parent = dst.parentFile
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) return false
|
||||
try {
|
||||
Files.createSymbolicLink(dst.toPath(), target)
|
||||
} catch (e: java.nio.file.FileAlreadyExistsException) {
|
||||
val staging = Files.createTempDirectory(checkNotNull(dst.absoluteFile.parentFile).toPath(), ".droiddeck-link-")
|
||||
val tmp = staging.resolve("link")
|
||||
try {
|
||||
Files.createSymbolicLink(tmp, target)
|
||||
Files.move(tmp, dst.toPath(), java.nio.file.StandardCopyOption.ATOMIC_MOVE)
|
||||
} finally {
|
||||
Files.deleteIfExists(tmp)
|
||||
Files.deleteIfExists(staging)
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to copy link: $src to $dst", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -323,8 +323,8 @@ object AddedGameArt {
|
||||
if (dst.isFile) continue
|
||||
val bytes = get("$CDN$id/$remote") ?: steamAssetUrl(id, remote)?.let { get(it) } ?: continue
|
||||
val tmp = File(cache, "$localName.tmp")
|
||||
tmp.writeBytes(bytes)
|
||||
if (tmp.renameTo(dst)) { got = true; File(cache, "$localName.src").writeText(STEAM) }
|
||||
if (runCatching { tmp.writeBytes(bytes) }.isFailure) { tmp.delete(); continue }
|
||||
if (tmp.renameTo(dst)) { got = true; runCatching { File(cache, "$localName.src").writeText(STEAM) } }
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
@@ -41,10 +41,20 @@ object AddedGames {
|
||||
return SessionPrefs.gameFolderBinds(context).map { (host, guest) -> Root(File(host), guest) }
|
||||
}
|
||||
|
||||
private val SKIP = Regex(
|
||||
"(?i)^(unins.*|setup.*|.*redist.*|vcredist.*|dxsetup.*|dxwebsetup.*|.*crash.*|.*report.*|dotnet.*|directx.*|.*prereq.*" +
|
||||
"|.*installer.*|.*uninstall.*|.*updater?.*|.*config(ur.*)?|.*settings.*|.*editor.*|.*server.*|.*benchmark.*|.*helper.*|.*eac.*|.*easyanticheat.*|.*battleye.*)\\.exe$",
|
||||
private val SKIP_PREFIX = Regex("(?i)^(unins|setup|vc_?redist|dxsetup|dxwebsetup|dotnet|directx|easyanticheat|battleye).*")
|
||||
private val SKIP_PART = Regex("(?i)redist|prereq|installer|uninstall|crashreport|crashhandler|crashpad|crashsender|launchhelper")
|
||||
private val SKIP_WORDS = setOf(
|
||||
"eac", "report", "reporter", "editor", "server", "benchmark", "helper",
|
||||
"update", "updater", "config", "configure", "configurator", "configuration", "settings",
|
||||
)
|
||||
private val WORD = Regex("[A-Z]?[a-z]+|[A-Z]+(?![a-z])|[0-9]+")
|
||||
|
||||
internal fun skipExe(name: String): Boolean {
|
||||
if (!name.endsWith(".exe", ignoreCase = true)) return false
|
||||
val base = name.dropLast(4)
|
||||
if (SKIP_PREFIX.matches(base) || SKIP_PART.containsMatchIn(base)) return true
|
||||
return WORD.findAll(base).any { it.value.lowercase() in SKIP_WORDS }
|
||||
}
|
||||
|
||||
const val GUEST_DIR = "/root/Games"
|
||||
|
||||
@@ -75,7 +85,7 @@ object AddedGames {
|
||||
val key = "${folder.path}|${folder.lastModified()}"
|
||||
val now = System.currentTimeMillis()
|
||||
ranked[key]?.takeIf { now - it.first < RANK_FRESH_MS }?.let { return it.second }
|
||||
val guessed = GameExePicker.rank(folder) { !SKIP.matches(it.name) }
|
||||
val guessed = GameExePicker.rank(folder) { !skipExe(it.name) }
|
||||
val declared = declaredExes(folder)
|
||||
val preferred = declared.map { it.path }.toSet()
|
||||
val result = if (declared.isEmpty()) guessed else GameExePicker.Ranking(
|
||||
@@ -128,7 +138,7 @@ object AddedGames {
|
||||
val root = runCatching { folder.canonicalPath + File.separator }.getOrNull() ?: return emptyList()
|
||||
return names.asSequence()
|
||||
.map { File(folder, it.trim().replace('\\', '/').trimStart('/')) }
|
||||
.filter { it.name.endsWith(".exe", ignoreCase = true) && !SKIP.matches(it.name) && it.isFile }
|
||||
.filter { it.name.endsWith(".exe", ignoreCase = true) && !skipExe(it.name) && it.isFile }
|
||||
.filter { runCatching { it.canonicalPath.startsWith(root) }.getOrDefault(false) }
|
||||
.distinctBy { it.path }
|
||||
.toList()
|
||||
@@ -204,8 +214,10 @@ object AddedGames {
|
||||
GamesFolderMigration.run(context)
|
||||
val path = canonical(exe)
|
||||
fun holds(f: File) = inside(path, canonical(f))
|
||||
AddedExes.list(context).map { File(it.folder) }.firstOrNull(::holds)?.let { return it }
|
||||
SessionPrefs.removedAddedGames(context).map { File(it) }.firstOrNull(::holds)?.let { return it }
|
||||
val entries = AddedExes.list(context)
|
||||
fun owns(f: File) = holds(f) && sameGame(f, currentExe(context, entries, f), exe)
|
||||
entries.map { File(it.folder) }.filter(::owns).maxByOrNull { canonical(it).length }?.let { return it }
|
||||
SessionPrefs.removedAddedGames(context).map { File(it) }.filter(::owns).maxByOrNull { canonical(it).length }?.let { return it }
|
||||
StoreInstallRoot.gameFolders(context).firstOrNull(::holds)?.let { return it }
|
||||
val dirs = listOfNotNull(
|
||||
GameStorage.effective(context)?.let { File(it.path, "steamapps/common") },
|
||||
@@ -220,6 +232,24 @@ object AddedGames {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun currentExe(context: Context, entries: List<AddedExes.Entry>, folder: File): File? =
|
||||
SessionPrefs.addedGameExe(context, folder.path).takeIf { it.isNotEmpty() }?.let(::File)
|
||||
?: entries.firstOrNull { it.folder == folder.path }?.let { File(it.exe) }
|
||||
?: rank(folder).exes.firstOrNull()
|
||||
|
||||
private fun sameGame(folder: File, current: File?, pick: File): Boolean {
|
||||
val base = canonical(folder)
|
||||
if (current == null || pick.parentFile?.let(::canonical) == base) return true
|
||||
val known = canonical(current)
|
||||
if (!inside(known, base) || known.substringBeforeLast('/') == base) return true
|
||||
val picked = canonical(pick)
|
||||
fun top(p: String) = p.removePrefix("$base/").substringBefore('/')
|
||||
fun stem(p: String) = p.substringAfterLast('/').lowercase().removeSuffix(".exe").replace(EXE_BUILD, "").filter { it.isLetterOrDigit() }
|
||||
return top(known) == top(picked) || stem(known).let { it.isNotEmpty() && it == stem(picked) }
|
||||
}
|
||||
|
||||
private val EXE_BUILD = Regex("([-_. ](x64|x86|win64|win32|64|32|shipping))+$")
|
||||
|
||||
fun addExe(context: Context, exe: File): AddResult {
|
||||
val app = context.applicationContext
|
||||
if (!exe.isFile) return AddResult.NotFound
|
||||
@@ -228,7 +258,10 @@ object AddedGames {
|
||||
if (owner != null && owner.path !in SessionPrefs.removedAddedGames(app)) {
|
||||
single(app, owner.path)?.takeIf { canonical(it.exe) == path }?.let { return AddResult.Existing(it) }
|
||||
}
|
||||
if (owner == null && guestPath(app, exe) == null) return AddResult.Unreachable
|
||||
if (guestPath(app, exe) == null) {
|
||||
(owner ?: exe.parentFile)?.takeIf { guestPath(app, it) == null }?.let { bindFolder(app, it) }
|
||||
if (guestPath(app, exe) == null) return AddResult.Unreachable
|
||||
}
|
||||
val folder = owner ?: exe.parentFile ?: return AddResult.NotFound
|
||||
SessionPrefs.setAddedGameRemoved(app, folder.path, false)
|
||||
SessionPrefs.setAddedGameExe(app, folder.path, exe.path)
|
||||
@@ -243,6 +276,8 @@ object AddedGames {
|
||||
|
||||
fun addFolder(context: Context, dir: File, onGame: (Game) -> Unit = {}, onChecking: (String?) -> Unit = {}): FolderResult {
|
||||
val app = context.applicationContext
|
||||
// Binds are mounted at session start, so a new one reaches the session from the next start.
|
||||
if (guestPath(app, dir) == null) bindFolder(app, dir)
|
||||
val removed = SessionPrefs.removedAddedGames(app).toSet()
|
||||
val listed = listedFolders(app, removed)
|
||||
var already = 0
|
||||
@@ -266,6 +301,20 @@ object AddedGames {
|
||||
return FolderResult(added, already)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
internal fun bindFolder(context: Context, dir: File) {
|
||||
if (!dir.isDirectory || !dir.canRead()) return
|
||||
val binds = SessionPrefs.gameFolderBinds(context)
|
||||
if (binds.any { it.first == dir.path }) return
|
||||
val taken = binds.map { it.second }.toSet()
|
||||
val base = GamesFolderMigration.legacyRoots(binds.map { it.first } + dir.path).last().guest
|
||||
var guest = base
|
||||
var n = 2
|
||||
while (guest in taken) guest = "$base-${n++}"
|
||||
SessionPrefs.setGameFolderBinds(context, binds + (dir.path to guest))
|
||||
Log.i(TAG, "games: bound $dir at $guest")
|
||||
}
|
||||
|
||||
private fun listedFolders(context: Context, removed: Set<String>): List<String> {
|
||||
val out = ArrayList<String>()
|
||||
GamesFolderMigration.run(context)
|
||||
|
||||
@@ -63,7 +63,8 @@ object CoverArt {
|
||||
missFile(context, rom).delete()
|
||||
found = true
|
||||
} else {
|
||||
missFile(context, rom).apply { parentFile?.mkdirs(); writeText(url ?: "") }
|
||||
runCatching { missFile(context, rom).apply { parentFile?.mkdirs(); writeText(url ?: "") } }
|
||||
.onFailure { Log.w(TAG, "${rom.name}: miss marker not written: $it") }
|
||||
}
|
||||
}
|
||||
return found
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.io.InputStream
|
||||
object GameFiles {
|
||||
const val EXTENSION = "steam"
|
||||
private const val MAX_BYTES = 128
|
||||
private const val MAX_NAME_BYTES = 200
|
||||
private const val INVENTORY = ".droiddeck-sync.json"
|
||||
private const val FORMAT = "droiddeck-game-files-v1"
|
||||
|
||||
@@ -50,8 +51,15 @@ object GameFiles {
|
||||
fun filename(game: Library.SteamGame, disambiguate: Boolean = false): String {
|
||||
val sanitized = game.name.replace(Regex("[\\p{Cntrl}\\\\/:*?\"<>|]"), "_")
|
||||
.trim().trim('.').ifBlank { "Game" }
|
||||
val points = sanitized.codePoints().limit(40).toArray()
|
||||
val title = String(points, 0, points.size)
|
||||
val points = sanitized.codePoints().toArray()
|
||||
var end = 0
|
||||
var bytes = 0
|
||||
while (end < points.size) {
|
||||
bytes += String(points, end, 1).toByteArray().size
|
||||
if (bytes > MAX_NAME_BYTES) break
|
||||
end++
|
||||
}
|
||||
val title = String(points, 0, end).trimEnd { it.isWhitespace() || it == '.' }.ifBlank { "Game" }
|
||||
val suffix = if (disambiguate) " (${game.gameIdString})" else ""
|
||||
return "$title$suffix.$EXTENSION"
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ object GameIdentifier {
|
||||
}
|
||||
|
||||
private val JUNK_NAME_RE = Regex(
|
||||
"""(?i)^(gse|steam|steam client|goldberg.*|launcher|game|play|start|setup|.* launcher.*|.*launcher redirector|.* redirector|rockstar games launcher.*|epic games launcher|smartsteamemu|coldclient.*|steamemu.*)$"""
|
||||
"""(?i)^(gse|steam|steam client|goldberg.*|launcher|game|play|start|setup|.* launcher.*|.*launcher redirector|.* redirector|rockstar games launcher.*|epic games launcher|smartsteamemu|coldclient.*|steamemu.*|bootstrappackagedgame)$"""
|
||||
)
|
||||
|
||||
fun isJunkPeName(name: String): Boolean {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import com.droiddeck.launcher.stores.InstalledStoreGame
|
||||
import com.droiddeck.launcher.stores.Store
|
||||
import com.droiddeck.launcher.stores.StoreGameSidecar
|
||||
import com.droiddeck.launcher.stores.StoreInstalls
|
||||
import com.droiddeck.launcher.stores.StoresState
|
||||
import java.io.File
|
||||
|
||||
object GameRemoval {
|
||||
enum class Kind { STORE, SHORTCUT, LAUNCHER }
|
||||
|
||||
fun kind(game: Library.SteamGame): Kind = when {
|
||||
game.library == Library.EPIC_LAUNCHER -> Kind.LAUNCHER
|
||||
game.library == Library.ADDED && game.source == Library.ADDED -> Kind.SHORTCUT
|
||||
else -> Kind.STORE
|
||||
}
|
||||
|
||||
class SteamFiles(val dir: File, val manifest: File, val partial: File?)
|
||||
|
||||
fun steamFiles(game: Library.SteamGame): SteamFiles? {
|
||||
val dir = game.gameFiles ?: return null
|
||||
val common = dir.parentFile ?: return null
|
||||
if (common.name != "common") return null
|
||||
val steamapps = common.parentFile ?: return null
|
||||
if (steamapps.name != "steamapps") return null
|
||||
val manifest = File(steamapps, "appmanifest_${game.appId}.acf")
|
||||
if (!manifest.isFile) return null
|
||||
if (!declares(manifest, game.appId)) return null
|
||||
return SteamFiles(dir, manifest, File(steamapps, "downloading/${game.appId}"))
|
||||
}
|
||||
|
||||
fun delete(target: SteamFiles): Boolean {
|
||||
StoreInstalls.deleteTree(target.dir)
|
||||
if (target.dir.exists()) return false
|
||||
target.partial?.let { StoreInstalls.deleteTree(it) }
|
||||
target.manifest.delete()
|
||||
return !target.manifest.exists()
|
||||
}
|
||||
|
||||
fun storeGame(game: Library.SteamGame): InstalledStoreGame? {
|
||||
val folder = game.gameFiles ?: return null
|
||||
val store = Store.byId(game.source)
|
||||
val id = game.storeId
|
||||
if (store != null && id != null) StoresState.installedGame(store, id)?.let { return it }
|
||||
val sidecar = StoreGameSidecar.read(folder)?.takeIf { it.isInstalled } ?: return null
|
||||
return InstalledStoreGame(sidecar, folder)
|
||||
}
|
||||
|
||||
private fun declares(manifest: File, appId: Int): Boolean =
|
||||
runCatching { manifest.readText() }.getOrNull()?.let { Regex("\"appid\"\\s+\"$appId\"").containsMatchIn(it) } == true
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import com.droiddeck.launcher.LauncherIcon
|
||||
import com.droiddeck.launcher.MainActivity
|
||||
import com.droiddeck.launcher.R
|
||||
import java.io.File
|
||||
@@ -23,7 +24,7 @@ object GameShortcuts {
|
||||
val name = game.name.ifBlank { context.getString(R.string.app_name) }
|
||||
val icon = iconBitmap(game)?.let(Icon::createWithBitmap) ?: Icon.createWithResource(context, R.mipmap.ic_launcher)
|
||||
return ShortcutInfo.Builder(context, "game:${game.gameIdString}")
|
||||
.setActivity(android.content.ComponentName(context, MainActivity::class.java))
|
||||
.setActivity(LauncherIcon.activity(context))
|
||||
.setShortLabel(name.take(40)).setLongLabel(name).setIcon(icon)
|
||||
.setIntent(launchIntent(context, game.gameIdString)).build()
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ object Library {
|
||||
private val NAME = Regex("^\\s*\"name\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE)
|
||||
private val STATE = Regex("^\\s*\"StateFlags\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
private val LAST_PLAYED = Regex("^\\s*\"LastPlayed\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
private val PRIVATE_APPS = Regex("\"PrivateApps_\\d+\"\\s*\"\\[([^\\]]*)\\]\"")
|
||||
private val INSTALL_DIR = Regex("^\\s*\"installdir\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE)
|
||||
|
||||
internal fun steamLibraries(context: Context, arch: String = SessionPrefs.activeSteamClientArch(context)): List<Pair<File, String>> {
|
||||
@@ -137,13 +138,35 @@ object Library {
|
||||
}
|
||||
|
||||
fun steamLibraryArt(context: Context): List<Pair<Int, File>> {
|
||||
val cache = File(SessionPrefs.activeSteamRoot(context), "appcache/librarycache")
|
||||
val root = SessionPrefs.activeSteamRoot(context)
|
||||
val cache = File(root, "appcache/librarycache")
|
||||
val concealed = concealedSteamApps(root)
|
||||
return cache.listFiles().orEmpty().mapNotNull { dir ->
|
||||
val appId = dir.name.toIntOrNull()?.takeIf { it !in NOT_GAMES } ?: return@mapNotNull null
|
||||
val appId = dir.name.toIntOrNull()?.takeIf { it !in NOT_GAMES && it.toLong() !in concealed } ?: return@mapNotNull null
|
||||
steamCacheImage(cache, appId, STEAM_CAPSULES)?.let { appId to it }
|
||||
}
|
||||
}
|
||||
|
||||
/** Apps the user hid (the "hidden" collection) or marked private in Steam. */
|
||||
internal fun concealedSteamApps(root: File): Set<Long> {
|
||||
val config = AddedGames.accountConfig(root) ?: return emptySet()
|
||||
val hidden = runCatching {
|
||||
val entries = org.json.JSONArray(File(config, "cloudstorage/cloud-storage-namespace-1.json").readText())
|
||||
val entry = (0 until entries.length()).map { entries.getJSONArray(it) }
|
||||
.firstOrNull { it.optString(0) == "user-collections.hidden" }?.optJSONObject(1)
|
||||
if (entry == null || entry.optBoolean("is_deleted")) emptySet() else {
|
||||
val collection = org.json.JSONObject(entry.optString("value"))
|
||||
fun ids(key: String) = collection.optJSONArray(key)?.let { a -> (0 until a.length()).map { a.optLong(it) } }.orEmpty()
|
||||
ids("added").toSet() - ids("removed").toSet()
|
||||
}
|
||||
}.getOrDefault(emptySet())
|
||||
val private = runCatching {
|
||||
PRIVATE_APPS.find(File(config, "localconfig.vdf").readText())?.groupValues?.get(1)
|
||||
?.split(',')?.mapNotNull { it.trim().toLongOrNull() }?.toSet()
|
||||
}.getOrNull().orEmpty()
|
||||
return hidden + private
|
||||
}
|
||||
|
||||
fun fromSteamClient(g: SteamGame): Boolean = g.library != ADDED && g.source == SOURCE_STEAM
|
||||
|
||||
fun launchableGames(
|
||||
@@ -155,9 +178,23 @@ object Library {
|
||||
var steam = steamGames(context, strictRead, arch)
|
||||
val targets = steam.filter(::fromSteamClient).map { SteamGrid.target(it, context) } + list.map { SteamGrid.target(it, context) }
|
||||
if (runCatching { SteamGrid.sync(context, targets) }.getOrDefault(false)) steam = steamGames(context, strictRead, arch)
|
||||
return (steam + list.map { g -> addedGame(context, g, roots) }).distinctBy { it.gameId }
|
||||
val claimed = claimedFolders(steam)
|
||||
val own = list.filter { g ->
|
||||
g.steamAppId != null || claimed.isEmpty() ||
|
||||
(runCatching { g.folder.canonicalPath }.getOrDefault(g.folder.absolutePath) !in claimed &&
|
||||
AddedGames.guestPath(context, g.folder).let { it == null || it !in claimed })
|
||||
}
|
||||
return (steam + own.map { g -> addedGame(context, g, roots) }).distinctBy { it.gameId }
|
||||
}
|
||||
|
||||
fun claimedFolders(steam: List<SteamGame>): Set<String> = steam.filter(::fromSteamClient).flatMap { g ->
|
||||
val dir = g.gameFiles ?: return@flatMap emptyList()
|
||||
listOfNotNull(
|
||||
runCatching { dir.canonicalPath }.getOrNull(),
|
||||
runCatching { java.nio.file.Files.readSymbolicLink(dir.toPath()).toString() }.getOrNull(),
|
||||
)
|
||||
}.toSet()
|
||||
|
||||
fun addedGame(context: Context, g: AddedGames.Game, roots: List<File> = steamLibraries(context).map { it.first }): SteamGame =
|
||||
AddedGameArt.resolve(context, g).let { art ->
|
||||
SteamGame(
|
||||
|
||||
@@ -153,7 +153,7 @@ object SteamAppInfo {
|
||||
return String(bytes, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun readMap(b: ByteBuffer, strings: List<String>?): Map<String, Any> {
|
||||
internal fun readMap(b: ByteBuffer, strings: List<String>?): Map<String, Any> {
|
||||
val out = LinkedHashMap<String, Any>()
|
||||
while (b.hasRemaining()) {
|
||||
val type = b.get().toInt() and 0xFF
|
||||
|
||||
@@ -43,6 +43,7 @@ data class GlassesState(
|
||||
val volumeMax: Int = 8,
|
||||
val sunblock: Boolean = false,
|
||||
val hardwareControls: Boolean = true,
|
||||
val supportsDisplayModeControl: Boolean = true,
|
||||
val hasSunblock: Boolean = true,
|
||||
val controlError: String? = null,
|
||||
val tracking: Boolean = false,
|
||||
@@ -50,6 +51,7 @@ data class GlassesState(
|
||||
val noSignal: Boolean = false,
|
||||
val sbsSupport: Int = SBS_UNKNOWN,
|
||||
val sbs90: Boolean = false,
|
||||
val rates2d: List<Int> = listOf(60, 90, 120),
|
||||
val experimental: Boolean = false,
|
||||
val supportsScreen3dof: Boolean = false,
|
||||
val supports30Hz: Boolean = false,
|
||||
@@ -63,7 +65,7 @@ data class GlassesState(
|
||||
val spatialConnected: Boolean get() = canControl && supportsSpatial
|
||||
fun usesSpatialControllers(settings: GlassesSettings) = spatialConnected && settings.positionalTracking &&
|
||||
(settings.handControllers || settings.joyConTracking)
|
||||
val can3d: Boolean get() = canControl && !noSignal && sbsSupport != SBS_FAILS
|
||||
val can3d: Boolean get() = canControl && supportsDisplayModeControl && !noSignal && sbsSupport != SBS_FAILS
|
||||
val verticalFovDeg: Float get() {
|
||||
val half = Math.toRadians(diagonalFovDeg / 2.0)
|
||||
val aspect = if (width > 0 && height > 0)
|
||||
@@ -212,7 +214,7 @@ object Glasses {
|
||||
}
|
||||
|
||||
fun sendMode(mode: Int) = post {
|
||||
if (!_state.value.canControl) return@post
|
||||
if (!_state.value.canControl || !_state.value.supportsDisplayModeControl) return@post
|
||||
if (link?.send(VitureProtocol.MSG_DISPLAY, byteArrayOf(mode.toByte())) == true) lastModeSentAt = SystemClock.uptimeMillis()
|
||||
}
|
||||
|
||||
@@ -469,6 +471,7 @@ object Glasses {
|
||||
.put("controlError", s.controlError ?: JSONObject.NULL)
|
||||
.put("experimental", s.experimental)
|
||||
.put("supportsScreen3dof", s.supportsScreen3dof)
|
||||
.put("supportsDisplayModeControl", s.supportsDisplayModeControl)
|
||||
.put("supportsSpatial", s.supportsSpatial)
|
||||
.put("firmware", (link as? VitureSdkLink)?.firmware ?: JSONObject.NULL)
|
||||
.put("spatialSdk", (link as? VitureSdkLink)?.spatialDiagnostics()?.let {
|
||||
@@ -613,13 +616,16 @@ object Glasses {
|
||||
switching3d = modeState.pendingStereo,
|
||||
brightnessMax = m?.brightnessMax ?: 8, volumeMax = m?.volumeMax ?: 8,
|
||||
sunblock = if (m === XrealModel) it.sunblock else _settings.value.sunblock ?: it.sunblock,
|
||||
hardwareControls = device?.vendorId == VitureProtocol.VENDOR_ID || m === XrealModel,
|
||||
hardwareControls = m?.hardwareControls == true &&
|
||||
(device?.vendorId == VitureProtocol.VENDOR_ID || m === XrealModel || m is RokidModel),
|
||||
supportsDisplayModeControl = m?.supportsDisplayModeControl == true,
|
||||
hasSunblock = m?.hasSunblock == true,
|
||||
diagonalFovDeg = m?.diagonalFovDeg ?: 46f,
|
||||
tracking = it.tracking && link != null,
|
||||
noSignal = noSignal,
|
||||
sbs90 = m?.sbs90 == true,
|
||||
experimental = m === XrealModel,
|
||||
rates2d = m?.rates2d ?: listOf(60, 90, 120),
|
||||
experimental = m === XrealModel || (m is VitureProtocol.Model && m.family == VitureProtocol.Family.PRO2 && !m.needsSdk),
|
||||
supportsScreen3dof = link?.hasImu == true,
|
||||
supports30Hz = display?.let(::supports30Hz) == true,
|
||||
supportsSpatial = (link as? VitureSdkLink)?.isCarina == true,
|
||||
@@ -634,7 +640,8 @@ object Glasses {
|
||||
scheduleModeCheck()
|
||||
}
|
||||
|
||||
internal fun isGlassesVendor(vid: Int) = vid == VitureProtocol.VENDOR_ID || RayNeoModel.isVendor(vid) || vid == XrealModel.VENDOR_ID
|
||||
internal fun isGlassesVendor(vid: Int) = vid == VitureProtocol.VENDOR_ID || RayNeoModel.isVendor(vid) ||
|
||||
vid == XrealModel.VENDOR_ID || vid == RokidModel.VENDOR_ID
|
||||
|
||||
internal fun supports30Hz(display: Display): Boolean {
|
||||
val current = display.mode
|
||||
@@ -650,15 +657,18 @@ object Glasses {
|
||||
internal fun deviceModel(device: UsbDevice): GlassesModel? {
|
||||
val name = runCatching { device.productName }.getOrNull()
|
||||
return if (device.vendorId == VitureProtocol.VENDOR_ID) {
|
||||
VitureSdk.marketName(device.productId)?.let(VitureProtocol::sdkModel)
|
||||
if (device.productId == VitureProtocol.PRO2_PRODUCT_ID) VitureProtocol.model(device.productId, name)
|
||||
else VitureSdk.marketName(device.productId)?.let(VitureProtocol::sdkModel)
|
||||
?: VitureProtocol.model(device.productId, name)
|
||||
}
|
||||
else if (XrealModel.matches(device.vendorId, device.productId)) XrealModel
|
||||
else RayNeoModel.model(device.vendorId, device.productId, name)
|
||||
else RokidModel.model(device.vendorId, device.productId, name)
|
||||
?: RayNeoModel.model(device.vendorId, device.productId, name)
|
||||
}
|
||||
|
||||
internal fun isGlassesDisplayName(name: String) =
|
||||
name.contains("VITURE", ignoreCase = true) || RayNeoModel.isDisplay(name) || XrealModel.isDisplay(name)
|
||||
name.contains("VITURE", ignoreCase = true) || RayNeoModel.isDisplay(name) || XrealModel.isDisplay(name) ||
|
||||
RokidModel.isDisplay(name)
|
||||
|
||||
private fun findDisplay(usbPresent: Boolean): Display? {
|
||||
return selectGlassesDisplay(displays.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION).toList(), usbPresent)
|
||||
@@ -715,7 +725,9 @@ object Glasses {
|
||||
calibrationKey = calibrationStore.key(device.vendorId, device.productId,
|
||||
runCatching { device.serialNumber }.getOrNull(), model?.name.orEmpty())
|
||||
val l = if (device.vendorId == VitureProtocol.VENDOR_ID) {
|
||||
if ((model as? VitureProtocol.Model)?.needsSdk == true) {
|
||||
if (device.productId == VitureProtocol.PRO2_PRODUCT_ID) {
|
||||
VitureV2Link.open(usb, device, v2Listener(generation))
|
||||
} else if ((model as? VitureProtocol.Model)?.needsSdk == true) {
|
||||
// Android can evict cacheDir while the SDK is running.
|
||||
Log.i(TAG, "opening VITURE SDK pid=0x%04X spatial=%s".format(device.productId, spatial))
|
||||
VitureSdkLink.open(usb, device, File(app.noBackupFilesDir, "viture"), sdkListener(generation),
|
||||
@@ -723,6 +735,8 @@ object Glasses {
|
||||
} else VitureLink.open(usb, device, linkListener(generation))
|
||||
} else if (model === XrealModel) {
|
||||
XrealLink.open(app, xrealListener(generation))
|
||||
} else if (model is RokidModel) {
|
||||
RokidLink.open(usb, device, rokidListener(generation))
|
||||
} else {
|
||||
val baseKey = calibrationKey!!
|
||||
val gtUsb = RayNeoProtocol.isGt(device.vendorId, device.productId)
|
||||
@@ -790,6 +804,33 @@ object Glasses {
|
||||
}
|
||||
}
|
||||
|
||||
private fun rokidListener(generation: Long) = object : RokidLink.Listener {
|
||||
override fun onReady() = post {
|
||||
if (generation != linkGeneration) return@post
|
||||
publish { it.copy(connected = true, controlError = null) }
|
||||
updateImu()
|
||||
applyMode(force = false)
|
||||
}
|
||||
override fun onValue(message: Int, value: Int) = post {
|
||||
if (generation != linkGeneration) return@post
|
||||
when (message) {
|
||||
VitureProtocol.MSG_GET_DISPLAY -> glassesMode = value
|
||||
VitureProtocol.MSG_BRIGHTNESS -> publish { it.copy(brightness = value) }
|
||||
}
|
||||
}
|
||||
override fun onPose(raw: ByteArray, pose: FloatArray, timeNs: Long) {
|
||||
if (generation != linkGeneration) return
|
||||
imuPackets++
|
||||
probe?.invoke(raw, pose)
|
||||
if (simulating.get() == null) deliver(pose, timeNs)
|
||||
}
|
||||
override fun onLost() = post {
|
||||
if (generation != linkGeneration) return@post
|
||||
closeLink()
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sdkListener(generation: Long) = object : VitureSdkLink.Listener {
|
||||
override fun onSpatial(pose: FloatArray, timeNs: Long, valid: Boolean) {
|
||||
if (generation == linkGeneration) SpatialTracking.accept(pose, timeNs, generation, valid)
|
||||
@@ -817,6 +858,40 @@ object Glasses {
|
||||
}
|
||||
}
|
||||
|
||||
private fun v2Listener(generation: Long) = object : VitureV2Link.Listener {
|
||||
override fun onReady() = post {
|
||||
if (generation != linkGeneration) return@post
|
||||
val current = link as? VitureV2Link ?: return@post
|
||||
publish { it.copy(connected = current.ready, supportsScreen3dof = current.hasImu,
|
||||
tracking = current.trackingActive, controlError = null) }
|
||||
updateImu()
|
||||
}
|
||||
override fun onValue(msgId: Int, value: Int) = post {
|
||||
if (generation != linkGeneration) return@post
|
||||
when (msgId) {
|
||||
VitureProtocol.MSG_GET_DISPLAY -> glassesMode = value
|
||||
VitureProtocol.MSG_GET_BRIGHTNESS -> publish { it.copy(brightness = value) }
|
||||
VitureProtocol.MSG_GET_VOLUME -> publish { it.copy(volume = value) }
|
||||
}
|
||||
}
|
||||
override fun onPose(raw: ByteArray, q: FloatArray, timeNs: Long) {
|
||||
if (generation != linkGeneration) return
|
||||
imuPackets++
|
||||
probe?.invoke(raw, q)
|
||||
if (simulating.get() == null) deliver(q, timeNs)
|
||||
}
|
||||
override fun onTracking(active: Boolean) = post {
|
||||
if (generation != linkGeneration) return@post
|
||||
val current = link as? VitureV2Link ?: return@post
|
||||
publish { it.copy(tracking = active && current.trackingActive) }
|
||||
}
|
||||
override fun onLost() = post {
|
||||
if (generation != linkGeneration) return@post
|
||||
closeLink()
|
||||
publish { it.copy(controlError = app.getString(R.string.glasses_connection_failed)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun rayNeoListener(generation: Long) = object : RayNeoLink.Listener {
|
||||
override fun onDeviceInfo(boardId: Int, calibrationSaved: Boolean) = post {
|
||||
if (generation != linkGeneration) return@post
|
||||
@@ -892,7 +967,7 @@ object Glasses {
|
||||
private fun applyMode(force: Boolean) {
|
||||
val s = _state.value
|
||||
val m = model ?: return
|
||||
if (!s.canControl) return
|
||||
if (!s.canControl || !s.supportsDisplayModeControl) return
|
||||
if (modeState.restorePending) {
|
||||
sendMode(false)
|
||||
return
|
||||
@@ -914,6 +989,7 @@ object Glasses {
|
||||
|
||||
private fun sendMode(sbs: Boolean): Boolean {
|
||||
val m = model ?: return false
|
||||
if (!m.supportsDisplayModeControl) return false
|
||||
if (sbs && !_settings.value.gameOnGlasses) update { it.copy(gameOnGlasses = true) }
|
||||
val g = _settings.value
|
||||
val resolution = GlassesDisplayModes.selected(g, _state.value, sbs)
|
||||
@@ -946,11 +1022,11 @@ object Glasses {
|
||||
val l = link ?: return
|
||||
if (!l.ready) return
|
||||
val want = (sink != null || poseObservers.isNotEmpty() || probe != null || _calibration.value.running || SpatialTracking.enabled) && l.hasImu
|
||||
if (want == _state.value.tracking) return
|
||||
if (l !is VitureV2Link && want == _state.value.tracking) return
|
||||
val accepted = l.send(VitureProtocol.MSG_IMU, byteArrayOf(if (want) 1 else 0))
|
||||
if (want && !accepted) return
|
||||
if (want) l.send(VitureProtocol.MSG_IMU_RATE, byteArrayOf(VitureProtocol.IMU_RATE_240.toByte()))
|
||||
publish { it.copy(tracking = want) }
|
||||
publish { it.copy(tracking = if (l is VitureV2Link) l.trackingActive else want) }
|
||||
}
|
||||
|
||||
private fun linkListener(generation: Long) = object : VitureLink.Listener {
|
||||
|
||||
@@ -4,10 +4,13 @@ package com.droiddeck.launcher.glasses
|
||||
interface GlassesModel {
|
||||
val name: String
|
||||
val controllable: Boolean
|
||||
val hardwareControls: Boolean get() = true
|
||||
val supportsDisplayModeControl: Boolean get() = true
|
||||
val brightnessMax: Int
|
||||
val volumeMax: Int
|
||||
val diagonalFovDeg: Float
|
||||
val sbs90: Boolean
|
||||
val rates2d: List<Int> get() = listOf(60, 90, 120)
|
||||
val hasSunblock: Boolean get() = true
|
||||
val resolutions: List<GlassesResolution> get() = emptyList()
|
||||
fun mode(hz: Int, sbs: Boolean, resolution: GlassesResolution?): Int = mode(hz, sbs)
|
||||
|
||||
@@ -12,8 +12,8 @@ internal object GlassesRendering {
|
||||
fun refreshRates(settings: GlassesSettings, state: GlassesState): List<Int> = buildList {
|
||||
if (native30HzAvailable(settings, state)) add(30)
|
||||
add(60)
|
||||
if (!state.sbs || state.sbs90) add(90)
|
||||
if (!state.sbs) add(120)
|
||||
if (state.sbs) { if (state.sbs90) add(90) }
|
||||
else addAll(state.rates2d.filter { it > 60 })
|
||||
}
|
||||
|
||||
fun outputHz(settings: GlassesSettings, state: GlassesState): Int {
|
||||
@@ -21,6 +21,7 @@ internal object GlassesRendering {
|
||||
return when {
|
||||
requested == 30 -> if (native30HzAvailable(settings, state)) 30 else 60
|
||||
state.sbs && !state.sbs90 -> 60
|
||||
!state.sbs && requested !in state.rates2d -> state.rates2d.filter { it <= requested }.maxOrNull() ?: 60
|
||||
else -> requested
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
internal enum class HandModel(val asset: String, val sha256: String) {
|
||||
FULL("models/hand_landmarker.task", "fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1"),
|
||||
LITE("models/hand_landmarker_lite.task", "173c0f47ecee7e935bd4a1e93aa580756b4180c1307c07d709313e82c28672c8");
|
||||
import com.droiddeck.launcher.handtracking.TrackingPack
|
||||
|
||||
internal enum class HandModel(val file: String, val sha256: String) {
|
||||
FULL(TrackingPack.HAND, "fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1"),
|
||||
LITE(TrackingPack.HAND_LITE, "173c0f47ecee7e935bd4a1e93aa580756b4180c1307c07d709313e82c28672c8");
|
||||
|
||||
companion object {
|
||||
val DEFAULT = FULL
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import android.hardware.usb.UsbConstants
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbDeviceConnection
|
||||
import android.hardware.usb.UsbEndpoint
|
||||
import android.hardware.usb.UsbInterface
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
|
||||
/** Controls are vendor control transfers; the HID interrupt endpoint streams IMU reports unprompted. */
|
||||
internal class RokidLink private constructor(
|
||||
private val connection: UsbDeviceConnection,
|
||||
private val usbInterface: UsbInterface,
|
||||
private val endpoint: UsbEndpoint,
|
||||
private val listener: Listener,
|
||||
) : GlassesLink {
|
||||
interface Listener {
|
||||
fun onReady()
|
||||
fun onValue(message: Int, value: Int)
|
||||
fun onPose(raw: ByteArray, pose: FloatArray, timeNs: Long)
|
||||
fun onLost()
|
||||
}
|
||||
|
||||
@Volatile private var running = true
|
||||
@Volatile override var ready = false
|
||||
private set
|
||||
@Volatile private var tracking = false
|
||||
private val controlLock = Any()
|
||||
private lateinit var worker: Thread
|
||||
override val hasImu = true
|
||||
|
||||
override fun send(msgId: Int, payload: ByteArray): Boolean {
|
||||
if (!running) return false
|
||||
val value = payload.firstOrNull()?.toInt()?.and(0xff) ?: -1
|
||||
return when (msgId) {
|
||||
VitureProtocol.MSG_IMU -> (value in 0..1).also { if (it) tracking = value == 1 }
|
||||
VitureProtocol.MSG_GET_DISPLAY -> display()?.let { listener.onValue(msgId, it) } != null
|
||||
VitureProtocol.MSG_DISPLAY -> value in RokidModel.MODES &&
|
||||
control(RokidModel.REQUEST_SET_DISPLAY, value, RokidModel.INDEX_DISPLAY) &&
|
||||
true.also { listener.onValue(VitureProtocol.MSG_GET_DISPLAY, value) }
|
||||
VitureProtocol.MSG_GET_BRIGHTNESS -> read(RokidModel.REQUEST_GET_BRIGHTNESS, RokidModel.INDEX_BRIGHTNESS)
|
||||
?.let { listener.onValue(VitureProtocol.MSG_BRIGHTNESS, RokidModel.brightnessStep(it)) } != null
|
||||
VitureProtocol.MSG_BRIGHTNESS -> value in 0..RokidModel.BRIGHTNESS_STEPS &&
|
||||
control(RokidModel.REQUEST_SET_BRIGHTNESS, RokidModel.brightnessRaw(value), RokidModel.INDEX_BRIGHTNESS)
|
||||
else -> false // Volume is the glasses' USB audio device; there is no film or IMU-rate control.
|
||||
}
|
||||
}
|
||||
|
||||
private fun display(): Int? = read(RokidModel.REQUEST_GET_DISPLAY, RokidModel.INDEX_DISPLAY)
|
||||
|
||||
private fun read(request: Int, index: Int): Int? = synchronized(controlLock) {
|
||||
if (!running) return null
|
||||
val buffer = ByteArray(64)
|
||||
val count = connection.controlTransfer(VENDOR_IN, request, 0, index, buffer, buffer.size, TIMEOUT_MS)
|
||||
// Display mode is reported in the second byte, brightness in the first.
|
||||
val offset = if (request == RokidModel.REQUEST_GET_DISPLAY) 1 else 0
|
||||
if (count > offset) buffer[offset].toInt() and 0xff else null
|
||||
}
|
||||
|
||||
private fun control(request: Int, value: Int, index: Int): Boolean = synchronized(controlLock) {
|
||||
running && connection.controlTransfer(VENDOR_OUT, request, value, index, byteArrayOf(1), 1, TIMEOUT_MS) >= 0
|
||||
}
|
||||
|
||||
private fun run() {
|
||||
val buffer = ByteArray(maxOf(endpoint.maxPacketSize, 64))
|
||||
val parser = RokidProtocol.Parser()
|
||||
var fusion = ImuFusion()
|
||||
var wasTracking = false
|
||||
var brightness = -1
|
||||
var lastSensor = SystemClock.elapsedRealtime()
|
||||
try {
|
||||
ready = true
|
||||
listener.onReady()
|
||||
while (running) {
|
||||
val count = connection.bulkTransfer(endpoint, buffer, buffer.size, 250)
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val sample = if (count > 0) parser.parse(buffer, count) else null
|
||||
if (sample == null) {
|
||||
if (now - lastSensor > 3000) error("Rokid IMU stream stalled")
|
||||
continue
|
||||
}
|
||||
lastSensor = now
|
||||
sample.brightness?.takeIf { it != brightness }?.let {
|
||||
brightness = it
|
||||
listener.onValue(VitureProtocol.MSG_BRIGHTNESS, RokidModel.brightnessStep(it))
|
||||
}
|
||||
val enabled = tracking
|
||||
if (enabled && !wasTracking) fusion = ImuFusion()
|
||||
wasTracking = enabled
|
||||
if (!enabled) continue
|
||||
// Max captures: already right/up/back; level gravity +Y, turn left +gyroY, look up +gyroX, roll right -gyroZ.
|
||||
fusion.update(sample.timeNs, sample.accel, sample.gyro)?.let {
|
||||
if (running && tracking) listener.onPose(buffer.copyOf(count), it, SystemClock.elapsedRealtimeNanos())
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (running) Log.w(TAG, "Rokid stream failed", e)
|
||||
} finally {
|
||||
ready = false
|
||||
if (running) listener.onLost()
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
synchronized(controlLock) {
|
||||
if (!running) return
|
||||
running = false
|
||||
}
|
||||
if (Thread.currentThread() !== worker) worker.join(500)
|
||||
runCatching { connection.releaseInterface(usbInterface) }
|
||||
connection.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "RokidLink"
|
||||
private const val TIMEOUT_MS = 250
|
||||
private const val VENDOR_IN = UsbConstants.USB_DIR_IN or UsbConstants.USB_TYPE_VENDOR
|
||||
private const val VENDOR_OUT = UsbConstants.USB_DIR_OUT or UsbConstants.USB_TYPE_VENDOR
|
||||
|
||||
internal fun sensorEndpoint(device: UsbDevice): Pair<UsbInterface, UsbEndpoint>? {
|
||||
for (i in 0 until device.interfaceCount) {
|
||||
val iface = device.getInterface(i)
|
||||
if (iface.interfaceClass != UsbConstants.USB_CLASS_HID) continue
|
||||
for (e in 0 until iface.endpointCount) {
|
||||
val endpoint = iface.getEndpoint(e)
|
||||
if (endpoint.direction == UsbConstants.USB_DIR_IN &&
|
||||
endpoint.type == UsbConstants.USB_ENDPOINT_XFER_INT) return iface to endpoint
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun open(usb: UsbManager, device: UsbDevice, listener: Listener): RokidLink? {
|
||||
val (iface, endpoint) = sensorEndpoint(device) ?: return null
|
||||
val conn = usb.openDevice(device) ?: return null
|
||||
if (!conn.claimInterface(iface, true)) {
|
||||
conn.close()
|
||||
return null
|
||||
}
|
||||
return RokidLink(conn, iface, endpoint, listener).also { link ->
|
||||
link.worker = Thread({ link.run() }, "rokid-imu").apply { isDaemon = true }
|
||||
link.worker.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/** Rokid Air/Max share one USB identity; the product string distinguishes them. */
|
||||
internal class RokidModel private constructor(override val name: String, private val max: Boolean) : GlassesModel {
|
||||
override val controllable = true
|
||||
override val brightnessMax = BRIGHTNESS_STEPS
|
||||
override val volumeMax = 0
|
||||
override val hasSunblock = false
|
||||
override val diagonalFovDeg = if (max) 50f else 43f
|
||||
override val sbs90 = max
|
||||
override val rates2d = if (max) listOf(60, 120) else listOf(60)
|
||||
override val resolutions = if (max) listOf(GlassesResolution(1920, 1080), GlassesResolution(1920, 1200))
|
||||
else listOf(GlassesResolution(1920, 1080))
|
||||
|
||||
override fun mode(hz: Int, sbs: Boolean): Int = mode(hz, sbs, null)
|
||||
|
||||
override fun mode(hz: Int, sbs: Boolean, resolution: GlassesResolution?): Int = when {
|
||||
!max -> if (sbs) MODE_SBS_1080_60 else MODE_2D_60
|
||||
sbs && resolution?.height == 1080 -> MODE_SBS_1080_60
|
||||
sbs -> if (hz >= 90) MODE_SBS_1200_90 else MODE_SBS_1200_60
|
||||
hz >= 120 -> MODE_2D_120
|
||||
else -> MODE_2D_60
|
||||
}
|
||||
|
||||
override fun modeForStep(step: Int): Int? = null
|
||||
|
||||
override fun equals(other: Any?) = other is RokidModel && other.name == name && other.max == max
|
||||
override fun hashCode() = name.hashCode()
|
||||
|
||||
companion object {
|
||||
const val VENDOR_ID = 0x04d2
|
||||
const val PRODUCT_ID = 0x162f
|
||||
const val BRIGHTNESS_STEPS = 10
|
||||
|
||||
// Vendor control requests, device recipient.
|
||||
const val REQUEST_SET_DISPLAY = 0x01
|
||||
const val REQUEST_GET_DISPLAY = 0x81
|
||||
const val REQUEST_SET_BRIGHTNESS = 0x02
|
||||
const val REQUEST_GET_BRIGHTNESS = 0x82
|
||||
const val INDEX_DISPLAY = 0x01
|
||||
const val INDEX_BRIGHTNESS = 0x02
|
||||
|
||||
const val MODE_2D_60 = 0
|
||||
const val MODE_SBS_1080_60 = 1
|
||||
const val MODE_2D_120 = 3
|
||||
const val MODE_SBS_1200_90 = 4
|
||||
const val MODE_SBS_1200_60 = 5
|
||||
val MODES = setOf(MODE_2D_60, MODE_SBS_1080_60, MODE_2D_120, MODE_SBS_1200_90, MODE_SBS_1200_60)
|
||||
|
||||
// 0x04d2 is a shared vendor ID, so the Rokid product string is required too.
|
||||
fun model(vid: Int, pid: Int, name: String?): GlassesModel? {
|
||||
if (vid != VENDOR_ID || pid != PRODUCT_ID) return null
|
||||
val product = name?.trim()?.takeIf { it.startsWith("Rokid", ignoreCase = true) } ?: return null
|
||||
return RokidModel(product, product.contains("Max", ignoreCase = true))
|
||||
}
|
||||
|
||||
fun isDisplay(name: String) = name.contains("Rokid", ignoreCase = true)
|
||||
|
||||
/** Device brightness is 0..100; the shared slider uses ten steps. */
|
||||
fun brightnessStep(raw: Int): Int = ((raw.coerceIn(0, 100) + 5) / 10).coerceIn(0, BRIGHTNESS_STEPS)
|
||||
fun brightnessRaw(step: Int): Int = step.coerceIn(0, BRIGHTNESS_STEPS) * 10
|
||||
}
|
||||
}
|
||||
|
||||
internal object RokidProtocol {
|
||||
data class Sample(val timeNs: Long, val accel: Vec3, val gyro: Vec3, val brightness: Int?)
|
||||
|
||||
private const val COMBINED = 17
|
||||
private const val SEPARATE = 4
|
||||
private const val SENSOR_ACCEL = 1
|
||||
private const val SENSOR_GYRO = 2
|
||||
|
||||
/** Max sends one combined report; Air sends accel and gyro reports that share a timestamp. */
|
||||
class Parser {
|
||||
private var accel: Vec3? = null
|
||||
private var accelTime = -1L
|
||||
private var gyro: Vec3? = null
|
||||
private var gyroTime = -2L
|
||||
|
||||
fun parse(packet: ByteArray, length: Int = packet.size): Sample? {
|
||||
if (length < 64) return null
|
||||
val b = ByteBuffer.wrap(packet, 0, length).order(ByteOrder.LITTLE_ENDIAN)
|
||||
return when (packet[0].toInt() and 0xff) {
|
||||
COMBINED -> Sample(b.getLong(1), vec(b, 9), vec(b, 21), packet[59].toInt() and 0xff)
|
||||
SEPARATE -> {
|
||||
val time = b.getLong(9) * 1000 // Air reports microseconds; Max reports nanoseconds.
|
||||
when (packet[1].toInt() and 0xff) {
|
||||
SENSOR_ACCEL -> { accel = vec(b, 21); accelTime = time }
|
||||
SENSOR_GYRO -> { gyro = vec(b, 21); gyroTime = time }
|
||||
else -> return null
|
||||
}
|
||||
if (accelTime != gyroTime) return null
|
||||
Sample(time, accel!!, gyro!!, null).also { accelTime = -1L; gyroTime = -2L }
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun vec(b: ByteBuffer, offset: Int) = Vec3(b.getFloat(offset), b.getFloat(offset + 4), b.getFloat(offset + 8))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import com.droiddeck.launcher.core.AddonPack
|
||||
import android.content.Context
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.handtracking.TrackingPack
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
@@ -11,7 +13,6 @@ import com.google.mediapipe.tasks.core.BaseOptions
|
||||
import com.google.mediapipe.tasks.core.Delegate
|
||||
import com.google.mediapipe.tasks.vision.core.RunningMode
|
||||
import com.google.mediapipe.tasks.vision.handlandmarker.HandLandmarker
|
||||
import org.opencv.android.OpenCVLoader
|
||||
import org.opencv.android.Utils
|
||||
import org.opencv.core.*
|
||||
import org.opencv.imgproc.Imgproc
|
||||
@@ -170,11 +171,11 @@ internal class StereoHandEngine(private val context: Context, private val token:
|
||||
} finally { calibrationProgress = calibrationProgress.copy(solving = false) }
|
||||
} }
|
||||
private fun detector(selected: HandModel=model, useGpu: Boolean=gpu, maxHands: Int=2): HandLandmarker {
|
||||
val bytes=context.assets.open(selected.asset).use { it.readBytes() }
|
||||
val bytes=TrackingPack.file(context,selected.file).readBytes()
|
||||
val hash=MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { "%02x".format(it) }
|
||||
check(hash==selected.sha256) { context.getString(R.string.spatial_model_checksum_error) }
|
||||
return HandLandmarker.createFromOptions(context,HandLandmarker.HandLandmarkerOptions.builder()
|
||||
.setBaseOptions(BaseOptions.builder().setModelAssetPath(selected.asset)
|
||||
.setBaseOptions(BaseOptions.builder().setModelAssetBuffer(ByteBuffer.allocateDirect(bytes.size).put(bytes).apply { rewind() })
|
||||
.setDelegate(if(useGpu) Delegate.GPU else Delegate.CPU).build())
|
||||
.setRunningMode(RunningMode.VIDEO).setNumHands(maxHands).setMinHandDetectionConfidence(0.6f)
|
||||
.setMinHandPresenceConfidence(0.6f).setMinTrackingConfidence(0.6f).build())
|
||||
@@ -325,7 +326,11 @@ internal class StereoHandEngine(private val context: Context, private val token:
|
||||
SpatialTracking.session.setHands(generation,SpatialHand(),SpatialHand())
|
||||
}
|
||||
private fun loop() {
|
||||
try { check(OpenCVLoader.initLocal()) } catch(e: Throwable) { message=context.getString(R.string.spatial_opencv_error, e.message.orEmpty());return }
|
||||
val loaded=TrackingPack.awaitNative(context,{ running }) { state ->
|
||||
if(state is AddonPack.State.Downloading)
|
||||
message=context.getString(R.string.tracking_pack_downloading,(state.progress*100).toInt())
|
||||
}
|
||||
if(!loaded) { if(running) message=context.getString(R.string.tracking_pack_failed);return }
|
||||
var grayL=Mat();var grayR=Mat();val rgba=Mat()
|
||||
var bmpL: Bitmap?=null;var bmpR: Bitmap?=null
|
||||
var leftGray=ByteArray(0);var rightGray=ByteArray(0)
|
||||
@@ -336,7 +341,7 @@ internal class StereoHandEngine(private val context: Context, private val token:
|
||||
var lastSource=0L;var lastStamp=0L;var captureOn=false;var captureRate=1000;var lastInputSeq=0L
|
||||
var rate=0f
|
||||
try {
|
||||
check(OpenCVLoader.initLocal()) { context.getString(R.string.spatial_opencv_load_error) }
|
||||
check(TrackingPack.loadNative(context)) { context.getString(R.string.spatial_opencv_load_error) }
|
||||
joyConFlow=StereoJoyConFlow()
|
||||
while(running) {
|
||||
while(true) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlin.math.sqrt
|
||||
|
||||
object VitureProtocol {
|
||||
const val VENDOR_ID = 0x35CA
|
||||
const val PRO2_PRODUCT_ID = 0x1301
|
||||
const val PACKET_SIZE = 64
|
||||
private const val HEADER = 0x12
|
||||
|
||||
@@ -33,13 +34,19 @@ object VitureProtocol {
|
||||
|
||||
enum class Family { ONE, PRO, LUMA, ULTRA, BEAST, PRO2 }
|
||||
|
||||
class Model(override val name: String, val family: Family) : GlassesModel {
|
||||
class Model(
|
||||
override val name: String,
|
||||
val family: Family,
|
||||
val needsSdk: Boolean = family == Family.ULTRA || family == Family.BEAST || family == Family.PRO2,
|
||||
) : GlassesModel {
|
||||
override val controllable: Boolean get() = true
|
||||
val needsSdk: Boolean get() = family == Family.ULTRA || family == Family.BEAST || family == Family.PRO2
|
||||
private val directPro2: Boolean get() = family == Family.PRO2 && !needsSdk
|
||||
override val hardwareControls: Boolean get() = !directPro2
|
||||
override val supportsDisplayModeControl: Boolean get() = !directPro2
|
||||
val tall: Boolean get() = family == Family.LUMA || family == Family.ULTRA || family == Family.BEAST
|
||||
override val sbs90: Boolean get() = family == Family.ULTRA
|
||||
override val brightnessMax: Int get() = 8
|
||||
override val volumeMax: Int get() = if (family == Family.BEAST) 15 else 8
|
||||
override val brightnessMax: Int get() = if (directPro2) 0 else 8
|
||||
override val volumeMax: Int get() = if (directPro2) 0 else if (family == Family.BEAST) 15 else 8
|
||||
override val hasSunblock: Boolean get() = family != Family.PRO2
|
||||
override val diagonalFovDeg: Float get() = when (family) {
|
||||
Family.ONE -> 43f
|
||||
@@ -89,6 +96,7 @@ object VitureProtocol {
|
||||
}
|
||||
|
||||
fun model(productId: Int, productName: String?): Model? {
|
||||
if (productId == PRO2_PRODUCT_ID) return Model("VITURE Pro 2", Family.PRO2, needsSdk = false)
|
||||
val family = when (productId) {
|
||||
0x1011, 0x1013, 0x1015, 0x1017, 0x101b -> Family.ONE
|
||||
0x1019, 0x101d -> Family.PRO
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import android.hardware.usb.UsbConstants
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbDeviceConnection
|
||||
import android.hardware.usb.UsbEndpoint
|
||||
import android.hardware.usb.UsbInterface
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.hardware.usb.UsbRequest
|
||||
import android.util.Log
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.ArrayBlockingQueue
|
||||
import java.util.concurrent.TimeoutException
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
internal class VitureV2Link internal constructor(
|
||||
private val transport: Transport,
|
||||
private val listener: Listener,
|
||||
) : GlassesLink {
|
||||
interface Listener {
|
||||
fun onReady()
|
||||
fun onValue(msgId: Int, value: Int)
|
||||
fun onPose(raw: ByteArray, q: FloatArray, timeNs: Long)
|
||||
fun onTracking(active: Boolean)
|
||||
fun onLost()
|
||||
}
|
||||
|
||||
internal data class Received(val bytes: ByteArray, val timeNs: Long)
|
||||
internal interface Transport {
|
||||
fun write(packet: ByteArray): Boolean
|
||||
fun read(timeoutMs: Long): Received?
|
||||
fun close()
|
||||
}
|
||||
|
||||
private data class Command(val packet: ByteArray, val stream: Boolean? = null)
|
||||
private val commands = ArrayBlockingQueue<Command>(16)
|
||||
private val stateLock = Any()
|
||||
private val bad = AtomicLong()
|
||||
@Volatile private var running = true
|
||||
@Volatile private var wanted = true
|
||||
@Volatile override var ready = false
|
||||
private set
|
||||
@Volatile var trackingActive = false
|
||||
private set
|
||||
override val hasImu: Boolean get() = ready
|
||||
override val badFrames: Long get() = bad.get()
|
||||
@Volatile private var streamOn = false
|
||||
private var lastPoseAt = 0L
|
||||
private var lastTick: Long? = null
|
||||
private var poseNotBefore = 0L
|
||||
private val worker = Thread(::run, "viture-v2").apply { isDaemon = true; start() }
|
||||
|
||||
override fun send(msgId: Int, payload: ByteArray): Boolean {
|
||||
synchronized(stateLock) {
|
||||
if (!running || !ready) return false
|
||||
if (msgId == VitureProtocol.MSG_IMU) {
|
||||
if (payload.size != 1 || payload[0].toInt() !in 0..1) return false
|
||||
val enabled = payload[0].toInt() == 1
|
||||
if (wanted == enabled) return true
|
||||
val command = Command(VitureV2Protocol.imuControl(
|
||||
if (enabled) VitureV2Protocol.STREAM_POSE else VitureV2Protocol.STREAM_OFF), enabled)
|
||||
if (!commands.offer(command)) return false
|
||||
wanted = enabled
|
||||
if (!enabled) {
|
||||
streamOn = false
|
||||
tracking(false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (msgId == VitureProtocol.MSG_GET_IMU) return true
|
||||
// Keep the measured 120 Hz mode; the generic controller requests 240 Hz.
|
||||
if (msgId == VitureProtocol.MSG_IMU_RATE) return false
|
||||
val query = when (msgId) {
|
||||
VitureProtocol.MSG_GET_BRIGHTNESS -> VitureV2Protocol.MSG_GET_BRIGHTNESS
|
||||
VitureProtocol.MSG_GET_VOLUME -> VitureV2Protocol.MSG_GET_VOLUME
|
||||
VitureProtocol.MSG_GET_DISPLAY -> VitureV2Protocol.MSG_GET_DISPLAY
|
||||
else -> return false
|
||||
}
|
||||
return payload.isEmpty() && commands.offer(Command(VitureV2Protocol.packet(query)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun tracking(active: Boolean) {
|
||||
if (trackingActive != active) {
|
||||
trackingActive = active
|
||||
listener.onTracking(active)
|
||||
}
|
||||
}
|
||||
|
||||
private fun run() {
|
||||
try {
|
||||
if (!transport.write(VitureV2Protocol.imuControl(VitureV2Protocol.STREAM_POSE)))
|
||||
throw IOException("Could not start pose stream")
|
||||
poseNotBefore = System.nanoTime()
|
||||
streamOn = true
|
||||
lastPoseAt = System.nanoTime()
|
||||
while (running) {
|
||||
commands.poll()?.let { command ->
|
||||
if (!transport.write(command.packet)) throw IOException("USB command failed")
|
||||
val completedAt = System.nanoTime()
|
||||
synchronized(stateLock) {
|
||||
command.stream?.let {
|
||||
streamOn = it
|
||||
lastTick = null
|
||||
poseNotBefore = completedAt
|
||||
lastPoseAt = System.nanoTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
transport.read(100)?.let(::receive)
|
||||
if (streamOn && System.nanoTime() - lastPoseAt > POSE_TIMEOUT_NS)
|
||||
throw IOException("No fresh valid pose samples")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (running) {
|
||||
ready = false
|
||||
tracking(false)
|
||||
Log.w(TAG, "V2 connection lost: ${e.message}")
|
||||
listener.onLost()
|
||||
}
|
||||
} finally {
|
||||
running = false
|
||||
ready = false
|
||||
tracking(false)
|
||||
runCatching { transport.write(VitureV2Protocol.imuControl(VitureV2Protocol.STREAM_OFF)) }
|
||||
transport.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun receive(received: Received) = synchronized(stateLock) {
|
||||
if (!running) return
|
||||
val frame = VitureV2Protocol.parse(received.bytes) ?: run { bad.incrementAndGet(); return }
|
||||
if (frame.msgId == VitureV2Protocol.EVENT_POSE) {
|
||||
val pose = VitureV2Protocol.pose(frame) ?: run { bad.incrementAndGet(); return }
|
||||
if (!streamOn || !wanted || received.timeNs < poseNotBefore) return
|
||||
val previous = lastTick
|
||||
if (previous != null) {
|
||||
val delta = (pose.deviceTick - previous) and 0xffffffffL
|
||||
if (delta == 0L || delta >= 0x80000000L) return
|
||||
}
|
||||
lastTick = pose.deviceTick
|
||||
lastPoseAt = System.nanoTime()
|
||||
val becameReady = !ready
|
||||
ready = true
|
||||
tracking(true)
|
||||
if (becameReady) listener.onReady()
|
||||
val q = pose.quaternion
|
||||
// Upstream right/up/back pose converted to the shared VITURE legacy basis.
|
||||
val shared = floatArrayOf(-q[2], -q[0], q[1], q[3])
|
||||
// Upstream does not establish device tick units; use host monotonic receipt time.
|
||||
if (running && wanted && streamOn) listener.onPose(received.bytes, shared, received.timeNs)
|
||||
return
|
||||
}
|
||||
VitureV2Protocol.reply(frame, VitureV2Protocol.MSG_IMU_CONTROL)?.let {
|
||||
if (!it.success) throw IOException("Pose stream rejected with status ${it.status}")
|
||||
}
|
||||
val queries = arrayOf(
|
||||
VitureV2Protocol.MSG_GET_BRIGHTNESS to VitureProtocol.MSG_GET_BRIGHTNESS,
|
||||
VitureV2Protocol.MSG_GET_VOLUME to VitureProtocol.MSG_GET_VOLUME,
|
||||
VitureV2Protocol.MSG_GET_DISPLAY to VitureProtocol.MSG_GET_DISPLAY,
|
||||
)
|
||||
for ((query, internalId) in queries) {
|
||||
val reply = VitureV2Protocol.reply(frame, query) ?: continue
|
||||
if (reply.success && reply.body.size == 1) listener.onValue(internalId, reply.body[0].toInt() and 255)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
synchronized(stateLock) {
|
||||
running = false
|
||||
ready = false
|
||||
wanted = false
|
||||
commands.clear()
|
||||
}
|
||||
if (Thread.currentThread() !== worker) worker.join(1500)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VitureV2Link"
|
||||
private const val POSE_TIMEOUT_NS = 3_000_000_000L
|
||||
const val PRODUCT_ID = 0x1301
|
||||
|
||||
fun open(usb: UsbManager, device: UsbDevice, listener: Listener): VitureV2Link? {
|
||||
if (device.vendorId != VitureProtocol.VENDOR_ID || device.productId != PRODUCT_ID) return null
|
||||
val candidates = (0 until device.interfaceCount).map(device::getInterface).filter {
|
||||
it.interfaceClass == UsbConstants.USB_CLASS_HID && it.alternateSetting == 0 && it.endpointCount == 2
|
||||
}.mapNotNull { intf ->
|
||||
val endpoints = (0 until intf.endpointCount).map(intf::getEndpoint)
|
||||
val input = endpoints.singleOrNull { it.direction == UsbConstants.USB_DIR_IN &&
|
||||
it.type == UsbConstants.USB_ENDPOINT_XFER_INT && it.maxPacketSize == 64 }
|
||||
val output = endpoints.singleOrNull { it.direction == UsbConstants.USB_DIR_OUT &&
|
||||
it.type == UsbConstants.USB_ENDPOINT_XFER_INT && it.maxPacketSize == 64 }
|
||||
if (input != null && output != null) Triple(intf, input, output) else null
|
||||
}
|
||||
val (intf, input, output) = candidates.singleOrNull() ?: return null
|
||||
val connection = usb.openDevice(device) ?: return null
|
||||
if (!connection.claimInterface(intf, true)) { connection.close(); return null }
|
||||
return try {
|
||||
VitureV2Link(AndroidTransport(connection, intf, input, output), listener)
|
||||
} catch (e: Exception) {
|
||||
connection.releaseInterface(intf)
|
||||
connection.close()
|
||||
Log.w(TAG, "Could not initialize USB requests", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AndroidTransport(
|
||||
private val connection: UsbDeviceConnection,
|
||||
private val intf: UsbInterface,
|
||||
input: UsbEndpoint,
|
||||
output: UsbEndpoint,
|
||||
) : Transport {
|
||||
private val inputRequest = UsbRequest()
|
||||
private val outputRequest = UsbRequest()
|
||||
private val inputBuffer = ByteBuffer.allocateDirect(64)
|
||||
private var outputBuffer: ByteBuffer? = null
|
||||
private var outputPending = false
|
||||
private val received = ArrayDeque<Received>()
|
||||
|
||||
init {
|
||||
try {
|
||||
if (!inputRequest.initialize(connection, input) || !outputRequest.initialize(connection, output))
|
||||
throw IOException("USB request initialization failed")
|
||||
queueInput()
|
||||
} catch (e: Exception) {
|
||||
inputRequest.close()
|
||||
outputRequest.close()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun queueInput() {
|
||||
inputBuffer.clear()
|
||||
if (!inputRequest.queue(inputBuffer)) throw IOException("USB input queue failed")
|
||||
}
|
||||
|
||||
private fun await(timeoutMs: Long) {
|
||||
val completed = try { connection.requestWait(timeoutMs) } catch (_: TimeoutException) { return }
|
||||
?: throw IOException("USB device disconnected")
|
||||
if (completed === outputRequest) {
|
||||
outputPending = false
|
||||
} else if (completed === inputRequest) {
|
||||
val count = inputBuffer.position()
|
||||
if (count > 0) {
|
||||
val bytes = ByteArray(count)
|
||||
inputBuffer.flip()
|
||||
inputBuffer.get(bytes)
|
||||
if (received.size >= 512) throw IOException("USB input backlog overflow")
|
||||
received.addLast(Received(bytes, System.nanoTime()))
|
||||
}
|
||||
queueInput()
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(packet: ByteArray): Boolean {
|
||||
if (outputPending) throw IOException("Previous USB output has not completed")
|
||||
val buffer = ByteBuffer.allocateDirect(packet.size).apply { put(packet); flip() }
|
||||
outputBuffer = buffer
|
||||
outputPending = outputRequest.queue(buffer)
|
||||
if (!outputPending) return false
|
||||
val deadline = System.nanoTime() + 250_000_000L
|
||||
while (outputPending && System.nanoTime() < deadline) await(25)
|
||||
if (outputPending) {
|
||||
outputRequest.cancel()
|
||||
throw IOException("USB output timed out")
|
||||
}
|
||||
return buffer.position() == packet.size
|
||||
}
|
||||
|
||||
override fun read(timeoutMs: Long): Received? {
|
||||
if (received.isEmpty()) await(timeoutMs)
|
||||
return received.removeFirstOrNull()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
inputRequest.cancel()
|
||||
if (outputPending) outputRequest.cancel()
|
||||
connection.releaseInterface(intf)
|
||||
connection.close()
|
||||
inputRequest.close()
|
||||
outputRequest.close()
|
||||
outputBuffer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Adapted from elasticjava/viture-v2, MIT, Copyright (c) 2026 Holger Bartnick.
|
||||
// Source: ece8c30485f7d357fefa362ae74f3eabac9b3899; see docs/third-party/viture-v2/.
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import kotlin.math.sqrt
|
||||
|
||||
object VitureV2Protocol {
|
||||
const val PACKET_SIZE = 64
|
||||
const val HEADER_SIZE = 8
|
||||
const val RESPONSE_OFFSET = 0x2000
|
||||
private const val PREAMBLE = 0x0010
|
||||
private const val MAX_PAYLOAD_SIZE = PACKET_SIZE - HEADER_SIZE
|
||||
|
||||
const val MSG_IMU_CONTROL = 0x0301
|
||||
const val MSG_GET_SERIAL = 0x3002
|
||||
const val MSG_GET_VERSION = 0x3003
|
||||
const val MSG_GET_BRIGHTNESS = 0x3122
|
||||
const val MSG_GET_DUTY_CYCLE = 0x3125
|
||||
const val MSG_GET_DISPLAY = 0x3141
|
||||
const val MSG_GET_VOLUME = 0x3201
|
||||
const val MSG_GET_WEAR = 0x3321
|
||||
const val EVENT_POSE = 0x7308
|
||||
const val EVENT_RAW = 0x7309
|
||||
|
||||
const val STREAM_OFF = 0
|
||||
const val STREAM_POSE = 1
|
||||
const val STREAM_RAW = 2
|
||||
|
||||
enum class Rate(val wireValue: Int, val hz: Int) {
|
||||
HZ_60(0, 60), HZ_90(1, 90), HZ_120(2, 120),
|
||||
HZ_240(3, 240), HZ_500(4, 500), HZ_1000(5, 1000),
|
||||
}
|
||||
|
||||
class Frame internal constructor(val msgId: Int, val payload: ByteArray)
|
||||
|
||||
class Reply internal constructor(val status: Int, val body: ByteArray) {
|
||||
val success: Boolean get() = status == 0
|
||||
}
|
||||
|
||||
// Device ticks have no established time unit; quaternion is xyzw with no axis remapping.
|
||||
class Pose internal constructor(
|
||||
val deviceTick: Long,
|
||||
val quaternion: FloatArray,
|
||||
val unknown: Long,
|
||||
)
|
||||
|
||||
class RawSample internal constructor(
|
||||
val deviceTick: Long,
|
||||
val gyroRadPerSec: FloatArray,
|
||||
val accelG: FloatArray,
|
||||
val payload: ByteArray,
|
||||
)
|
||||
|
||||
fun packet(msgId: Int, payload: ByteArray = ByteArray(0)): ByteArray {
|
||||
require(msgId in 0..0xFFFF)
|
||||
require(payload.size <= MAX_PAYLOAD_SIZE)
|
||||
return ByteArray(HEADER_SIZE + payload.size).also { buffer ->
|
||||
putU16(buffer, 0, PREAMBLE)
|
||||
putU16(buffer, 2, msgId)
|
||||
putU16(buffer, 4, payload.size)
|
||||
putU16(buffer, 6, checksum(payload, 0, payload.size))
|
||||
payload.copyInto(buffer, HEADER_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
fun imuControl(streams: Int, rate: Rate = Rate.HZ_120): ByteArray {
|
||||
// Upstream measured that stream mask 3 ACKs but produces no events on Pro 2.
|
||||
require(streams == STREAM_OFF || streams == STREAM_POSE || streams == STREAM_RAW)
|
||||
require(streams != STREAM_POSE || rate.hz <= 240)
|
||||
val wireRate = if (streams == STREAM_OFF) 0 else rate.wireValue
|
||||
return packet(MSG_IMU_CONTROL, byteArrayOf(streams.toByte(), wireRate.toByte()))
|
||||
}
|
||||
|
||||
fun parse(buffer: ByteArray, size: Int = buffer.size): Frame? {
|
||||
if (size < HEADER_SIZE || size > PACKET_SIZE || size > buffer.size) return null
|
||||
if (u16(buffer, 0) != PREAMBLE) return null
|
||||
val payloadSize = u16(buffer, 4)
|
||||
if (payloadSize > MAX_PAYLOAD_SIZE || HEADER_SIZE + payloadSize > size) return null
|
||||
if (checksum(buffer, HEADER_SIZE, payloadSize) != u16(buffer, 6)) return null
|
||||
return Frame(u16(buffer, 2), buffer.copyOfRange(HEADER_SIZE, HEADER_SIZE + payloadSize))
|
||||
}
|
||||
|
||||
fun reply(frame: Frame, requestMsgId: Int): Reply? {
|
||||
if (requestMsgId !in 0..0xFFFF || frame.msgId != ((requestMsgId + RESPONSE_OFFSET) and 0xFFFF)) return null
|
||||
if (frame.payload.isEmpty()) return null
|
||||
return Reply(u8(frame.payload[0]), frame.payload.copyOfRange(1, frame.payload.size))
|
||||
}
|
||||
|
||||
fun pose(frame: Frame): Pose? {
|
||||
if (frame.msgId != EVENT_POSE || frame.payload.size != 24) return null
|
||||
val payload = frame.payload
|
||||
val w = float(payload, 8)
|
||||
val x = float(payload, 12)
|
||||
val y = float(payload, 16)
|
||||
val z = float(payload, 20)
|
||||
if (!w.isFinite() || !x.isFinite() || !y.isFinite() || !z.isFinite()) return null
|
||||
val norm = sqrt(w.toDouble() * w + x.toDouble() * x + y.toDouble() * y + z.toDouble() * z)
|
||||
if (norm !in 0.9..1.1) return null
|
||||
val n = norm.toFloat()
|
||||
return Pose(u32(payload, 4), floatArrayOf(x / n, y / n, z / n, w / n), u32(payload, 0))
|
||||
}
|
||||
|
||||
fun raw(frame: Frame): RawSample? {
|
||||
if (frame.msgId != EVENT_RAW || frame.payload.size != 56) return null
|
||||
val payload = frame.payload
|
||||
val gyro = floatArrayOf(float(payload, 10), float(payload, 14), float(payload, 18))
|
||||
val accel = floatArrayOf(float(payload, 22), float(payload, 26), float(payload, 30))
|
||||
if (gyro.any { !it.isFinite() } || accel.any { !it.isFinite() }) return null
|
||||
return RawSample(u32(payload, 4), gyro, accel, payload.copyOf())
|
||||
}
|
||||
|
||||
private fun checksum(buffer: ByteArray, offset: Int, size: Int): Int {
|
||||
var sum = 0
|
||||
for (i in offset until offset + size) sum += u8(buffer[i])
|
||||
return sum and 0xFFFF
|
||||
}
|
||||
|
||||
private fun u8(value: Byte) = value.toInt() and 0xFF
|
||||
|
||||
private fun u16(buffer: ByteArray, offset: Int) =
|
||||
u8(buffer[offset]) or (u8(buffer[offset + 1]) shl 8)
|
||||
|
||||
private fun u32(buffer: ByteArray, offset: Int): Long =
|
||||
u16(buffer, offset).toLong() or (u16(buffer, offset + 2).toLong() shl 16)
|
||||
|
||||
private fun float(buffer: ByteArray, offset: Int): Float = Float.fromBits(u32(buffer, offset).toInt())
|
||||
|
||||
private fun putU16(buffer: ByteArray, offset: Int, value: Int) {
|
||||
buffer[offset] = value.toByte()
|
||||
buffer[offset + 1] = (value ushr 8).toByte()
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,27 @@ import kotlin.math.abs
|
||||
|
||||
/** 1S radians/sec and device nanoseconds, mapped using directed hardware captures. */
|
||||
internal class XrealOrientationFilter {
|
||||
private val fusion = ImuFusion()
|
||||
|
||||
// 1S reports right/down/forward. Rotate 180 degrees about X into the
|
||||
// shared right/up/back basis before either gravity alignment or fusion.
|
||||
// Captures: level gravity -Y; looking left -gyroY; up +gyroX; right tilt +gyroZ.
|
||||
fun update(sample: XrealProtocol.Sample): FloatArray? = fusion.update(sample.timeNs,
|
||||
Vec3(sample.accel.x, -sample.accel.y, -sample.accel.z),
|
||||
Vec3(sample.gyro.x, -sample.gyro.y, -sample.gyro.z))
|
||||
}
|
||||
|
||||
/** Gyro integration with gravity correction in the shared right/up/back basis, rad/s and device ns. */
|
||||
internal class ImuFusion {
|
||||
private var previous = 0L
|
||||
private var rotation = Quaternion.IDENTITY
|
||||
private var initialized = false
|
||||
private var bias = Vec3(0f, 0f, 0f)
|
||||
|
||||
fun update(sample: XrealProtocol.Sample): FloatArray? {
|
||||
if (sample.timeNs <= previous) return null
|
||||
val dt = if (previous == 0L) 0f else (sample.timeNs - previous) * 1e-9f
|
||||
previous = sample.timeNs
|
||||
// 1S reports right/down/forward. Rotate 180 degrees about X into the
|
||||
// shared right/up/back basis before either gravity alignment or fusion.
|
||||
// Captures: level gravity -Y; looking left -gyroY; up +gyroX; right tilt +gyroZ.
|
||||
val accel = Vec3(sample.accel.x, -sample.accel.y, -sample.accel.z)
|
||||
val gyro = Vec3(sample.gyro.x, -sample.gyro.y, -sample.gyro.z)
|
||||
fun update(timeNs: Long, accel: Vec3, gyro: Vec3): FloatArray? {
|
||||
if (timeNs <= previous) return null
|
||||
val dt = if (previous == 0L) 0f else (timeNs - previous) * 1e-9f
|
||||
previous = timeNs
|
||||
val norm = accel.length()
|
||||
val gravityValid = abs(norm - 9.81f) < 1.5f
|
||||
if (dt > 0.25f) initialized = false // Never integrate a reconnect/stall as motion.
|
||||
|
||||
@@ -28,7 +28,7 @@ object AutoDriverTrial {
|
||||
release, p.getString("name", "").orEmpty(),
|
||||
p.getString("display", "").orEmpty(), p.getString("linux", "").orEmpty(),
|
||||
p.getString("previousDisplay", "").orEmpty(), p.getString("previousLinux", "").orEmpty(),
|
||||
p.getInt("starts", 0),
|
||||
maxOf(p.getInt("starts", 0), p.getInt("displayStarts", 0)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ object AutoDriverTrial {
|
||||
.putString("release", release).putString("name", name)
|
||||
.putString("display", display).putString("linux", linux)
|
||||
.putString("previousDisplay", previousDisplay).putString("previousLinux", previousLinux)
|
||||
.putInt("starts", 0)
|
||||
.putInt("starts", 0).putInt("displayStarts", 0)
|
||||
.commit()
|
||||
}
|
||||
|
||||
@@ -48,13 +48,24 @@ object AutoDriverTrial {
|
||||
|
||||
fun bad(context: Context): Set<String> = prefs(context).getStringSet("bad", emptySet()).orEmpty()
|
||||
|
||||
/**
|
||||
* Counted before the compositor loads the trial display driver, so a driver that kills the process during
|
||||
* Vulkan init still counts towards a rollback; sessionStarting runs too late for that.
|
||||
*/
|
||||
fun displayStarting(context: Context, display: String) {
|
||||
val p = prefs(context)
|
||||
if (p.getString("release", null) == null || p.getString("display", "") != display) return
|
||||
val counted = maxOf(p.getInt("displayStarts", 0), p.getInt("starts", 0))
|
||||
p.edit().putInt("displayStarts", counted + 1).commit()
|
||||
}
|
||||
|
||||
fun sessionStarting(context: Context, display: String, linux: String) {
|
||||
val trial = current(context)
|
||||
watching = null
|
||||
if (trial == null || trial.display != display || trial.linux != linux) return
|
||||
app = context.applicationContext
|
||||
watching = trial
|
||||
prefs(context).edit().putInt("starts", trial.starts + 1).commit()
|
||||
prefs(context).edit().putInt("starts", prefs(context).getInt("starts", 0) + 1).commit()
|
||||
}
|
||||
|
||||
fun sessionReady() {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import com.droiddeck.launcher.gpu.GpuInfo.Family
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
object AutoPicks {
|
||||
const val URL = "https://raw.githubusercontent.com/Droid-Deck/Drivers-CI/main/auto-drivers.json"
|
||||
const val SCHEMA = 1
|
||||
const val NEWEST_DD_TURNIP = DriverPairs.DD_TURNIP
|
||||
|
||||
class Rule(
|
||||
val families: Set<Family>,
|
||||
val models: Set<Int> = emptySet(),
|
||||
val oneUi8Gen2: Boolean? = null,
|
||||
val minSdk: Int = 0,
|
||||
val blobMin: Int = 0,
|
||||
val blobMax: Int = Int.MAX_VALUE,
|
||||
val prefer: List<String>,
|
||||
) {
|
||||
fun matches(gpu: GpuInfo, sdk: Int, blob: Int?): Boolean =
|
||||
gpu.family in families &&
|
||||
(models.isEmpty() || gpu.model in models) &&
|
||||
(oneUi8Gen2 == null || gpu.oneUi8Gen2 == oneUi8Gen2) &&
|
||||
sdk >= minSdk &&
|
||||
(blobMin == 0 && blobMax == Int.MAX_VALUE || blob != null && blob in blobMin..blobMax)
|
||||
}
|
||||
|
||||
val BUILT_IN = listOf(
|
||||
Rule(setOf(Family.A8XX), models = setOf(840), prefer = listOf(NEWEST_DD_TURNIP, DriverPairs.WN_BALANCED)),
|
||||
Rule(setOf(Family.A8XX), prefer = listOf(DriverPairs.WN_BALANCED)),
|
||||
Rule(setOf(Family.A7XX_LOW), prefer = listOf(DriverPairs.BANNER_710)),
|
||||
Rule(setOf(Family.A7XX), oneUi8Gen2 = true, prefer = listOf(DriverPairs.BANNER_ONEUI)),
|
||||
Rule(setOf(Family.A7XX, Family.A6XX), prefer = listOf(DriverPairs.BANNER)),
|
||||
Rule(setOf(Family.ADRENO_UNKNOWN), minSdk = 36, prefer = listOf(DriverPairs.WN_BALANCED)),
|
||||
Rule(setOf(Family.ADRENO_UNKNOWN), prefer = listOf(DriverPairs.BANNER)),
|
||||
)
|
||||
|
||||
fun parse(text: String?): List<Rule>? = runCatching {
|
||||
val json = JSONObject(text ?: return null)
|
||||
if (json.optInt("schemaVersion") != SCHEMA) return null
|
||||
val rules = json.getJSONArray("rules")
|
||||
(0 until rules.length()).mapNotNull { i -> rule(rules.getJSONObject(i)) }.takeIf { it.isNotEmpty() }
|
||||
}.getOrNull()
|
||||
|
||||
private fun rule(o: JSONObject): Rule? {
|
||||
val families = strings(o.optJSONArray("families")).mapNotNull { name -> Family.values().firstOrNull { it.name == name } }.toSet()
|
||||
val prefer = strings(o.optJSONArray("prefer"))
|
||||
if (families.isEmpty() || prefer.isEmpty()) return null
|
||||
val models = o.optJSONArray("models")?.let { a -> (0 until a.length()).map { a.getInt(it) }.toSet() } ?: emptySet()
|
||||
return Rule(
|
||||
families, models,
|
||||
if (o.has("oneUi8Gen2")) o.getBoolean("oneUi8Gen2") else null,
|
||||
o.optInt("minSdk", 0), o.optInt("blobMin", 0), o.optInt("blobMax", Int.MAX_VALUE), prefer,
|
||||
)
|
||||
}
|
||||
|
||||
private fun strings(a: JSONArray?): List<String> = a?.let { (0 until it.length()).map { i -> it.getString(i) } }.orEmpty()
|
||||
|
||||
fun preferences(gpu: GpuInfo, rules: List<Rule>, sdk: Int, blob: Int?): List<String> =
|
||||
rules.firstOrNull { it.matches(gpu, sdk, blob) }?.prefer.orEmpty()
|
||||
|
||||
fun blobVersion(driverVersion: String?): Int? = driverVersion?.split('.')?.getOrNull(1)?.toIntOrNull()
|
||||
}
|
||||
@@ -57,10 +57,11 @@ object DeviceInfo {
|
||||
add("Model found from", when (gpu.modelSource) {
|
||||
"kernel" -> "the kernel's GPU name"
|
||||
"vulkan" -> "the Vulkan driver (the kernel's name has no model)"
|
||||
"platform" -> "the platform (the kernel's name has no model)"
|
||||
"platform" -> "the platform or SoC (the kernel and Vulkan names have no model)"
|
||||
else -> "unknown"
|
||||
})
|
||||
add("Kernel name", gpu.kgslName)
|
||||
add("Kernel chip id", gpu.kgslChipId)
|
||||
val kgsl = "/sys/class/kgsl/kgsl-3d0"
|
||||
add("Clock range", gpuRange())
|
||||
add("Load", read("$kgsl/gpu_busy_percentage")?.let { if (it.endsWith("%")) it else "$it %" })
|
||||
|
||||
@@ -111,15 +111,23 @@ object DriverPairs {
|
||||
}
|
||||
}
|
||||
|
||||
fun recommendedKey(gpu: GpuInfo, pairs: List<DriverPair>, sdk: Int = android.os.Build.VERSION.SDK_INT): String? = when (gpu.family) {
|
||||
Family.POWERVR -> NAD_RADV_XCLIPSE.takeIf { gpu.name.contains("Xclipse", ignoreCase = true) }
|
||||
Family.NOT_ADRENO, Family.MALI_OTHER -> null
|
||||
Family.MALI_JM, Family.MALI_G610_G710, Family.MALI_G615, Family.MALI_G720, Family.MALI_LATEST -> maliKey(gpu, pairs, sdk)
|
||||
Family.A8XX -> if (gpu.model == 840) newestDdTurnip(pairs) ?: WN_BALANCED else WN_BALANCED
|
||||
Family.A7XX_LOW -> BANNER_710
|
||||
Family.A7XX -> if (gpu.oneUi8Gen2) BANNER_ONEUI else BANNER
|
||||
Family.A6XX -> BANNER
|
||||
Family.ADRENO_UNKNOWN -> if (android.os.Build.VERSION.SDK_INT >= 36) WN_BALANCED else BANNER
|
||||
fun recommendedKey(
|
||||
gpu: GpuInfo, pairs: List<DriverPair>,
|
||||
rules: List<AutoPicks.Rule> = AutoPicks.BUILT_IN, bad: Set<String> = emptySet(),
|
||||
sdk: Int = android.os.Build.VERSION.SDK_INT, blob: Int? = null,
|
||||
): String? {
|
||||
when (gpu.family) {
|
||||
Family.POWERVR -> return NAD_RADV_XCLIPSE.takeIf { gpu.name.contains("Xclipse", ignoreCase = true) }
|
||||
Family.MALI_OTHER, Family.NOT_ADRENO -> return null
|
||||
Family.MALI_JM, Family.MALI_G610_G710, Family.MALI_G615, Family.MALI_G720, Family.MALI_LATEST ->
|
||||
return maliKey(gpu, pairs, sdk)
|
||||
else -> Unit
|
||||
}
|
||||
val keys = AutoPicks.preferences(gpu, rules, sdk, blob)
|
||||
.ifEmpty { AutoPicks.preferences(gpu, AutoPicks.BUILT_IN, sdk, blob) }
|
||||
.mapNotNull { if (it == AutoPicks.NEWEST_DD_TURNIP) newestDdTurnip(pairs) else it }
|
||||
return keys.firstOrNull { key -> pairs.any { it.key == key && it.complete && AutoDriverTrial.release(it) !in bad } }
|
||||
?: keys.firstOrNull()
|
||||
}
|
||||
|
||||
private fun maliKey(gpu: GpuInfo, pairs: List<DriverPair>, sdk: Int): String? {
|
||||
|
||||
@@ -7,12 +7,13 @@ import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
|
||||
object FrameGen {
|
||||
const val ENGINE_OFF = "off"
|
||||
const val ENGINE_WINFG = "winfg"
|
||||
const val ENGINE_DDFG = "ddfg"
|
||||
private const val LEGACY_ENGINE_WINFG = "winfg"
|
||||
const val ENGINE_LSFG = "lsfg"
|
||||
|
||||
private const val WINFG_PERF_PRESET = 2
|
||||
private const val WINFG_MODEL = 4
|
||||
private const val FLOW_SCALE_WINFG = 0.60f
|
||||
private const val DDFG_PERF_PRESET = 2
|
||||
private const val DDFG_MODEL = 4
|
||||
private const val FLOW_SCALE_DDFG = 0.60f
|
||||
private const val FLOW_SCALE_LSFG = 0.80f
|
||||
|
||||
val ADAPTIVE_TARGETS = listOf(120, 90, 60)
|
||||
@@ -29,7 +30,7 @@ object FrameGen {
|
||||
|
||||
fun mode(context: Context): Mode {
|
||||
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
val engine = prefs.getString("engine", ENGINE_OFF) ?: ENGINE_OFF
|
||||
val engine = prefs.getString("engine", ENGINE_OFF).let { if (it == LEGACY_ENGINE_WINFG) ENGINE_DDFG else it ?: ENGINE_OFF }
|
||||
val target = prefs.getInt("target", 0)
|
||||
if (engine == ENGINE_LSFG && target in ADAPTIVE_TARGETS) return Mode(engine, target = target)
|
||||
return Mode(engine, prefs.getInt("multiplier", 2).coerceIn(2, 4))
|
||||
@@ -43,7 +44,7 @@ object FrameGen {
|
||||
fun label(context: Context): String = label(context, mode(context))
|
||||
|
||||
fun label(context: Context, mode: Mode): String = when {
|
||||
mode.engine == ENGINE_WINFG -> context.getString(R.string.frame_gen_winfg, mode.multiplier)
|
||||
mode.engine == ENGINE_DDFG -> context.getString(R.string.frame_gen_ddfg, mode.multiplier)
|
||||
mode.engine == ENGINE_LSFG && mode.target > 0 -> context.getString(R.string.frame_gen_lsfg_adaptive, mode.target)
|
||||
mode.engine == ENGINE_LSFG -> context.getString(R.string.frame_gen_lsfg, mode.multiplier)
|
||||
else -> context.getString(R.string.frame_gen_off)
|
||||
@@ -68,10 +69,10 @@ object FrameGen {
|
||||
WaylandCompositor.nativeSetFrameGenTuning(FLOW_SCALE_LSFG, refreshHz)
|
||||
WaylandCompositor.nativeSetFrameGenArmed(true, mode.multiplier, mode.target)
|
||||
}
|
||||
ENGINE_WINFG -> {
|
||||
WaylandCompositor.nativeSetFrameGenEngine(WaylandCompositor.FG_ENGINE_WINFG)
|
||||
WaylandCompositor.nativeSetWinFgTuning(WINFG_MODEL, WINFG_PERF_PRESET)
|
||||
WaylandCompositor.nativeSetFrameGenTuning(FLOW_SCALE_WINFG, refreshHz)
|
||||
ENGINE_DDFG -> {
|
||||
WaylandCompositor.nativeSetFrameGenEngine(WaylandCompositor.FG_ENGINE_DDFG)
|
||||
WaylandCompositor.nativeSetDdFgTuning(DDFG_MODEL, DDFG_PERF_PRESET)
|
||||
WaylandCompositor.nativeSetFrameGenTuning(FLOW_SCALE_DDFG, refreshHz)
|
||||
WaylandCompositor.nativeSetFrameGenArmed(true, mode.multiplier, 0)
|
||||
}
|
||||
else -> WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0)
|
||||
|
||||
@@ -14,6 +14,8 @@ data class GpuInfo(
|
||||
val oneUi8Gen2: Boolean,
|
||||
val kgslName: String = "",
|
||||
val modelSource: String = "",
|
||||
val kgslChipId: String = "",
|
||||
val platformGpuModel: Int = 0,
|
||||
) {
|
||||
enum class Family(val label: String) {
|
||||
A8XX("Adreno 8xx"),
|
||||
@@ -42,9 +44,12 @@ data class GpuInfo(
|
||||
|
||||
enum class Support { TESTED, UNTESTED, UNSUPPORTED }
|
||||
|
||||
private val isA850: Boolean get() = model == 850 || platformGpuModel == 850
|
||||
|
||||
val support: Support
|
||||
get() = when {
|
||||
family == Family.NOT_ADRENO -> Support.UNSUPPORTED
|
||||
isA850 -> Support.UNTESTED
|
||||
family == Family.A6XX && model == 650 -> Support.TESTED
|
||||
family == Family.A8XX -> Support.TESTED
|
||||
family == Family.A7XX && model >= 725 -> Support.TESTED
|
||||
@@ -54,12 +59,13 @@ data class GpuInfo(
|
||||
val supportText: String
|
||||
get() = when (support) {
|
||||
Support.TESTED -> "Supported"
|
||||
Support.UNTESTED -> when (family) {
|
||||
Family.POWERVR -> if (name.contains("Xclipse", ignoreCase = true)) "Experimental: RADV or the system Xclipse driver"
|
||||
Support.UNTESTED -> when {
|
||||
family == Family.POWERVR -> if (name.contains("Xclipse", ignoreCase = true)) "Experimental: RADV or the system Xclipse driver"
|
||||
else "System PowerVR driver: compatibility varies by device"
|
||||
Family.MALI_OTHER -> "Experimental: Mali through the phone's own driver (Venus)"
|
||||
Family.MALI_JM, Family.MALI_G610_G710, Family.MALI_G615, Family.MALI_G720, Family.MALI_LATEST -> "Experimental: open Mali driver (test builds)"
|
||||
Family.A7XX_LOW -> "Experimental: its drivers are test builds"
|
||||
family == Family.MALI_OTHER -> "Experimental: Mali through the phone's own driver (Venus)"
|
||||
family.mali -> "Experimental: open Mali driver (test builds)"
|
||||
isA850 -> "Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device"
|
||||
family == Family.A7XX_LOW -> "Experimental: its drivers are test builds"
|
||||
else -> "Outside tested hardware (Adreno 650, 725 and newer): it may not run"
|
||||
}
|
||||
Support.UNSUPPORTED -> "Not supported: no compatible graphics backend is available for this GPU"
|
||||
@@ -67,10 +73,10 @@ data class GpuInfo(
|
||||
|
||||
fun supportText(context: Context): String = context.getString(when (support) {
|
||||
Support.TESTED -> R.string.gpuinfo_supported
|
||||
Support.UNTESTED -> when (family) {
|
||||
Family.POWERVR -> if (name.contains("Xclipse", ignoreCase = true)) R.string.gpuinfo_xclipse else R.string.gpuinfo_powervr
|
||||
Family.A7XX_LOW -> R.string.gpuinfo_experimental
|
||||
Family.MALI_JM, Family.MALI_G610_G710, Family.MALI_G615, Family.MALI_G720, Family.MALI_LATEST, Family.MALI_OTHER -> R.string.gpuinfo_experimental
|
||||
Support.UNTESTED -> when {
|
||||
family == Family.POWERVR -> if (name.contains("Xclipse", ignoreCase = true)) R.string.gpuinfo_xclipse else R.string.gpuinfo_powervr
|
||||
isA850 -> R.string.gpuinfo_a850_experimental
|
||||
family == Family.A7XX_LOW || family.mali -> R.string.gpuinfo_experimental
|
||||
else -> R.string.gpuinfo_below_tested
|
||||
}
|
||||
Support.UNSUPPORTED -> R.string.gpuinfo_unsupported
|
||||
@@ -83,19 +89,15 @@ data class GpuInfo(
|
||||
|
||||
fun detect(): GpuInfo {
|
||||
val adreno = File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists()
|
||||
val raw = listOf("/sys/class/kgsl/kgsl-3d0/gpu_model", "/sys/class/kgsl/kgsl-3d0/gpu_chipid")
|
||||
.firstNotNullOfOrNull { FileUtils.readString(File(it))?.trim()?.takeIf(String::isNotEmpty) }
|
||||
val raw = FileUtils.readString(File("/sys/class/kgsl/kgsl-3d0/gpu_model"))?.trim()
|
||||
val chipId = FileUtils.readString(File("/sys/class/kgsl/kgsl-3d0/gpu_chipid"))?.trim().orEmpty()
|
||||
val soc = if (adreno) SocNames.label() else ""
|
||||
val fromKernel = raw?.let { threeDigits(it) } ?: 0
|
||||
val fromVulkan = if (fromKernel > 0 || !adreno) 0 else VulkanInfo.cachedOrNull()?.get("device")?.let { threeDigits(it) } ?: 0
|
||||
val fromPlatform = if (fromKernel > 0 || fromVulkan > 0 || !adreno) 0 else platformModel()
|
||||
val model = maxOf(fromKernel, fromVulkan, fromPlatform)
|
||||
val source = when {
|
||||
fromKernel > 0 -> "kernel"
|
||||
fromVulkan > 0 -> "vulkan"
|
||||
fromPlatform > 0 -> "platform"
|
||||
else -> ""
|
||||
}
|
||||
val platformGpuModel = if (adreno) platformModel() else 0
|
||||
val (model, source) = if (adreno) modelFromNames(
|
||||
raw,
|
||||
{ VulkanInfo.cachedOrNull()?.get("device") },
|
||||
{ platformGpuModel },
|
||||
) else 0 to ""
|
||||
// Mali, named by the vendor's Vulkan driver ("Mali-G720-Immortalis MC12") or by kbase.
|
||||
val mali = if (adreno || !File("/dev/mali0").exists()) null else maliName()
|
||||
val family = familyOf(adreno, model, SystemVulkanDriver.isPowerVr(), mali)
|
||||
@@ -112,19 +114,34 @@ data class GpuInfo(
|
||||
},
|
||||
model = model, family = family, soc = soc,
|
||||
oneUi8Gen2 = samsung && model == 740,
|
||||
kgslName = raw.orEmpty(), modelSource = source,
|
||||
kgslName = raw.orEmpty(), modelSource = source, kgslChipId = chipId,
|
||||
platformGpuModel = platformGpuModel,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun threeDigits(text: String): Int = Regex("""(\d{3})""").find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||
internal fun threeDigits(text: String): Int = Regex("""(?i)\badreno\s*(?:\(TM\)\s*)?(\d{3})(?!\d)""").find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||
|
||||
internal fun modelFromNames(
|
||||
kernelName: String?,
|
||||
vulkanName: () -> String?,
|
||||
fallback: () -> Int,
|
||||
): Pair<Int, String> {
|
||||
kernelName?.let { threeDigits(it) }?.takeIf { it > 0 }?.let { return it to "kernel" }
|
||||
vulkanName()?.let { threeDigits(it) }?.takeIf { it > 0 }?.let { return it to "vulkan" }
|
||||
return fallback().let { if (it > 0) it to "platform" else 0 to "" }
|
||||
}
|
||||
|
||||
private val PLATFORMS = mapOf(
|
||||
"msmnile" to 640, "kona" to 650, "lahaina" to 660, "taro" to 730, "cape" to 730,
|
||||
"kalama" to 740, "pineapple" to 750, "sun" to 830,
|
||||
)
|
||||
|
||||
internal fun platformModel(platform: String = systemProperty("ro.board.platform")): Int =
|
||||
PLATFORMS[platform.lowercase()] ?: PLATFORMS[systemProperty("ro.vendor.qti.soc_name").lowercase()] ?: 0
|
||||
internal fun platformModel(
|
||||
platform: String = systemProperty("ro.board.platform"),
|
||||
qtiSocName: String = systemProperty("ro.vendor.qti.soc_name"),
|
||||
socModel: String = SocNames.model(),
|
||||
): Int = if (SocNames.normalizedModel(socModel) == "SM8975" || SocNames.normalizedModel(qtiSocName) == "SM8975") 850
|
||||
else PLATFORMS[platform.lowercase()] ?: PLATFORMS[qtiSocName.lowercase()] ?: 0
|
||||
|
||||
fun systemProperty(name: String): String = try {
|
||||
Class.forName("android.os.SystemProperties").getMethod("get", String::class.java).invoke(null, name) as? String ?: ""
|
||||
|
||||
@@ -8,6 +8,7 @@ object SocNames {
|
||||
"SM8350" to "Snapdragon 888", "SM8450" to "Snapdragon 8 Gen 1", "SM8475" to "Snapdragon 8+ Gen 1",
|
||||
"SM8550" to "Snapdragon 8 Gen 2", "QCS8550" to "Snapdragon 8 Gen 2", "SM8650" to "Snapdragon 8 Gen 3",
|
||||
"SM8750" to "Snapdragon 8 Elite", "SM8850" to "Snapdragon 8 Elite Gen 5",
|
||||
"SM8975" to "Snapdragon 8 Elite Extreme Gen 6",
|
||||
"SM7325" to "Snapdragon 778G", "SM7450" to "Snapdragon 7 Gen 1", "SM7550" to "Snapdragon 7 Gen 3",
|
||||
"SM6375" to "Snapdragon 695",
|
||||
)
|
||||
@@ -25,8 +26,10 @@ object SocNames {
|
||||
.firstOrNull { it.isNotEmpty() && !Regex("(?i)^msm_\\d").containsMatchIn(it) }.orEmpty()
|
||||
}
|
||||
|
||||
internal fun normalizedModel(model: String): String = model.trim().uppercase().removePrefix("QTI ").trim()
|
||||
|
||||
fun name(model: String, platform: String = GpuInfo.systemProperty("ro.board.platform")): String? =
|
||||
MODELS[model.uppercase()] ?: PLATFORMS[platform.lowercase()]?.let { "$it family" }
|
||||
MODELS[normalizedModel(model)] ?: PLATFORMS[platform.lowercase()]?.let { "$it family" }
|
||||
|
||||
fun label(model: String = model(), platform: String = GpuInfo.systemProperty("ro.board.platform")): String {
|
||||
val name = name(model, platform)
|
||||
|
||||
@@ -11,8 +11,10 @@ object TpUbwcHint {
|
||||
|
||||
// On Adreno 740, TPL1_DBG_ECO_CNTL1.TP_UBWC_FLAG_HINT is shared by every driver on the GPU and must
|
||||
// match the system driver: blob v6xx leaves it off, newer blobs set it. A mismatch corrupts Android UI.
|
||||
internal fun features(model: Int, systemDriverVersion: String?): String? {
|
||||
// Samsung's One UI firmware sets it whatever blob version it reports.
|
||||
internal fun features(model: Int, systemDriverVersion: String?, samsung: Boolean = false): String? {
|
||||
if (model != 740) return null
|
||||
if (samsung) return "enable_tp_ubwc_flag_hint=1"
|
||||
val blob = systemDriverVersion?.split('.')?.getOrNull(1)?.toIntOrNull() ?: return null
|
||||
return "enable_tp_ubwc_flag_hint=" + if (blob >= 700) "1" else "0"
|
||||
}
|
||||
@@ -20,11 +22,12 @@ object TpUbwcHint {
|
||||
@Synchronized
|
||||
fun forThisDevice(): String? {
|
||||
if (resolved) return cached
|
||||
val model = GpuInfo.detect().model
|
||||
val gpu = GpuInfo.detect()
|
||||
val model = gpu.model
|
||||
val version = if (model == 740) VulkanInfo.query()["driver_version"] else null
|
||||
cached = features(model, version)
|
||||
cached = features(model, version, gpu.oneUi8Gen2)
|
||||
resolved = true
|
||||
Log.i(TAG, "Adreno $model, system driver ${version ?: "n/a"}: ${cached ?: "driver default"}")
|
||||
Log.i(TAG, "Adreno $model${if (gpu.oneUi8Gen2) " (One UI)" else ""}, system driver ${version ?: "n/a"}: ${cached ?: "driver default"}")
|
||||
return cached
|
||||
}
|
||||
|
||||
|
||||
@@ -307,27 +307,15 @@ public final class TurnipDriver {
|
||||
Log.i(TAG, "Mali: the compositor uses the system Vulkan driver");
|
||||
return null;
|
||||
}
|
||||
String model = gpuModel();
|
||||
Log.i(TAG, "gpu model: " + (model == null ? "unknown" : model));
|
||||
if (model != null) {
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d)\\d\\d").matcher(model);
|
||||
if (m.find()) {
|
||||
char generation = m.group(1).charAt(0);
|
||||
if (generation >= '8') return DRIVER_A8XX;
|
||||
if (generation == '7') return DRIVER_A7XX;
|
||||
if (generation == '6') return DRIVER_A6XX;
|
||||
}
|
||||
}
|
||||
return android.os.Build.VERSION.SDK_INT >= 36 ? DRIVER_A8XX : DRIVER_A7XX;
|
||||
int model = GpuInfo.Companion.detect().getModel();
|
||||
Log.i(TAG, "gpu model: " + (model == 0 ? "unknown" : model));
|
||||
return chooseByModel(model, android.os.Build.VERSION.SDK_INT);
|
||||
}
|
||||
|
||||
private static String gpuModel() {
|
||||
for (String path : new String[]{
|
||||
"/sys/class/kgsl/kgsl-3d0/gpu_model",
|
||||
"/sys/class/kgsl/kgsl-3d0/gpu_chipid"}) {
|
||||
String value = FileUtils.readString(new File(path));
|
||||
if (value != null && !value.trim().isEmpty()) return value.trim();
|
||||
}
|
||||
return null;
|
||||
static String chooseByModel(int model, int sdk) {
|
||||
if (model >= 800) return DRIVER_A8XX;
|
||||
if (model >= 700) return DRIVER_A7XX;
|
||||
if (model >= 600) return DRIVER_A6XX;
|
||||
return sdk >= 36 ? DRIVER_A8XX : DRIVER_A7XX;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ object TurnipReleases {
|
||||
private const val PREFS = "turnip_releases"
|
||||
private const val KEY_RELEASE = "latest"
|
||||
private const val KEY_DOWNLOADS = "downloads"
|
||||
private const val KEY_PICKS = "autoPicks"
|
||||
|
||||
class Asset(val source: String, val tag: String, val name: String, val url: String, val size: Long, val linux: Boolean, val label: String,
|
||||
val sha256: String = "", val pair: String = "", val bundle: Boolean = false,
|
||||
@@ -76,6 +77,9 @@ object TurnipReleases {
|
||||
private fun sources(): List<Source> =
|
||||
if (com.droiddeck.launcher.core.DeviceSupport.adreno()) SOURCES.filter { it.repo != NON_ADRENO_REPO } else SOURCES
|
||||
|
||||
fun autoPicks(context: Context): List<AutoPicks.Rule> =
|
||||
AutoPicks.parse(prefs(context).getString(KEY_PICKS, null)) ?: AutoPicks.BUILT_IN
|
||||
|
||||
fun cached(context: Context): Check? {
|
||||
val raw = prefs(context).getString(KEY_RELEASE, null) ?: return null
|
||||
return runCatching { parse(JSONObject(raw)) }.getOrNull()
|
||||
@@ -123,6 +127,10 @@ object TurnipReleases {
|
||||
}
|
||||
}
|
||||
if (failed.size == sources.size) throw lastError ?: IOException("no source answered")
|
||||
runCatching { get(context, AutoPicks.URL, AutoPicks.URL) }
|
||||
.onFailure { Log.i(TAG, "auto picks: ${it.message}") }
|
||||
.getOrNull()?.takeIf { AutoPicks.parse(it) != null }
|
||||
?.let { prefs(context).edit().putString(KEY_PICKS, it).apply() }
|
||||
val stored = JSONObject().put("assets", assets).put("latest", latest)
|
||||
.put("failed", JSONArray(failed)).put("checkedAt", System.currentTimeMillis())
|
||||
prefs(context).edit().putString(KEY_RELEASE, stored.toString()).apply()
|
||||
|
||||
@@ -39,8 +39,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -188,9 +186,8 @@ internal class HandCamera(
|
||||
private fun prepare() {
|
||||
if (stopped) return
|
||||
runCatching {
|
||||
val bytes = context.assets.open(MODEL).use { it.readBytes() }
|
||||
val model = ByteBuffer.allocateDirect(bytes.size).order(ByteOrder.nativeOrder()).put(bytes)
|
||||
model.rewind()
|
||||
check(TrackingPack.loadNative(context)) { "hand tracking download is not installed" }
|
||||
val model = TrackingPack.model(context, TrackingPack.POSE)
|
||||
val options = PoseLandmarker.PoseLandmarkerOptions.builder()
|
||||
.setBaseOptions(BaseOptions.builder().setModelAssetBuffer(model).setDelegate(Delegate.CPU).build())
|
||||
.setRunningMode(RunningMode.VIDEO)
|
||||
@@ -305,7 +302,6 @@ internal class HandCamera(
|
||||
|
||||
companion object {
|
||||
private const val TAG = "HandCamera"
|
||||
private const val MODEL = "handtracking/pose_landmarker_lite.task"
|
||||
private const val ANALYSIS_WIDTH = 960
|
||||
private const val ANALYSIS_HEIGHT = 720
|
||||
private const val MIN_CONFIDENCE = 0.5f
|
||||
|
||||
@@ -13,8 +13,6 @@ import com.google.mediapipe.tasks.core.BaseOptions
|
||||
import com.google.mediapipe.tasks.core.Delegate
|
||||
import com.google.mediapipe.tasks.vision.core.RunningMode
|
||||
import com.google.mediapipe.tasks.vision.handlandmarker.HandLandmarker
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -134,7 +132,6 @@ internal class HandModel private constructor(private val landmarkers: List<HandL
|
||||
hypot(fix.x - marks[WRISTS[side]].x * k.width, fix.y - marks[WRISTS[side]].y * k.height)
|
||||
|
||||
companion object {
|
||||
private const val MODEL = "handtracking/hand_landmarker.task"
|
||||
private val WRISTS = intArrayOf(BodyPose.LEFT_WRIST, BodyPose.RIGHT_WRIST)
|
||||
private val PINKIES = intArrayOf(BodyPose.LEFT_PINKY, BodyPose.RIGHT_PINKY)
|
||||
private val INDEXES = intArrayOf(BodyPose.LEFT_INDEX, BodyPose.RIGHT_INDEX)
|
||||
@@ -153,10 +150,9 @@ internal class HandModel private constructor(private val landmarkers: List<HandL
|
||||
private const val MIN_CONFIDENCE = 0.4f
|
||||
|
||||
fun open(context: Context): HandModel {
|
||||
val bytes = context.assets.open(MODEL).use { it.readBytes() }
|
||||
check(TrackingPack.loadNative(context)) { "hand tracking download is not installed" }
|
||||
fun landmarker(mode: RunningMode, hands: Int): HandLandmarker {
|
||||
val model = ByteBuffer.allocateDirect(bytes.size).order(ByteOrder.nativeOrder()).put(bytes)
|
||||
model.rewind()
|
||||
val model = TrackingPack.model(context, TrackingPack.HAND)
|
||||
return HandLandmarker.createFromOptions(context, HandLandmarker.HandLandmarkerOptions.builder()
|
||||
.setBaseOptions(BaseOptions.builder().setModelAssetBuffer(model).setDelegate(Delegate.CPU).build())
|
||||
.setRunningMode(mode)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.droiddeck.launcher.handtracking
|
||||
|
||||
import com.droiddeck.launcher.core.AddonPack
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
@@ -16,9 +17,10 @@ internal object HandTracking {
|
||||
val side: String? = null,
|
||||
val permitted: Boolean = false,
|
||||
val sides: Set<String> = emptySet(),
|
||||
val downloaded: Boolean = false,
|
||||
) {
|
||||
val usable: Boolean get() = sides.isNotEmpty()
|
||||
val ready: Boolean get() = enabled && permitted && usable
|
||||
val ready: Boolean get() = enabled && permitted && usable && downloaded
|
||||
}
|
||||
|
||||
private val state = MutableStateFlow(Settings())
|
||||
@@ -38,8 +40,11 @@ internal object HandTracking {
|
||||
side = prefs.getString(SIDE, null),
|
||||
permitted = permitted(context),
|
||||
sides = if (supported) TrackingCameras.sides(found) else emptySet(),
|
||||
downloaded = TrackingPack.installed(context),
|
||||
)
|
||||
state.value = next
|
||||
if (next.enabled && next.usable && !next.downloaded && TrackingPack.refresh(context) == AddonPack.State.Missing)
|
||||
TrackingPack.ensure(context)
|
||||
return next
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.droiddeck.launcher.handtracking
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.AddonPack
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
|
||||
/** Hand-tracking models and the OpenCV/MediaPipe native libraries, downloaded on first use. */
|
||||
internal object TrackingPack {
|
||||
const val HAND = "hand_landmarker.task"
|
||||
const val HAND_LITE = "hand_landmarker_lite.task"
|
||||
const val POSE = "pose_landmarker_lite.task"
|
||||
private const val OPENCV = "libopencv_java4.so"
|
||||
private const val MEDIAPIPE = "libmediapipe_tasks_jni.so"
|
||||
private const val TAG = "TrackingPack"
|
||||
|
||||
private val pack = AddonPack.TRACKING
|
||||
val state get() = pack.state
|
||||
val size get() = pack.size
|
||||
private val lock = Any()
|
||||
@Volatile private var nativeLoaded = false
|
||||
|
||||
fun installed(context: Context) = pack.installed(context)
|
||||
fun refresh(context: Context) = pack.refresh(context)
|
||||
fun ensure(context: Context) = pack.ensure(context) { HandTracking.load(it) }
|
||||
|
||||
fun file(context: Context, name: String): File = pack.file(context, name)
|
||||
|
||||
fun model(context: Context, name: String): ByteBuffer =
|
||||
FileInputStream(file(context, name)).use { it.channel.map(FileChannel.MapMode.READ_ONLY, 0, it.channel.size()) }
|
||||
|
||||
/**
|
||||
* Loads OpenCV and MediaPipe from the pack. MediaPipe's own System.loadLibrary then resolves to the
|
||||
* already-loaded library by soname, so this must run before any MediaPipe task class initializes.
|
||||
*/
|
||||
fun loadNative(context: Context): Boolean {
|
||||
if (nativeLoaded) return true
|
||||
synchronized(lock) {
|
||||
if (nativeLoaded) return true
|
||||
if (!pack.installed(context)) return false
|
||||
return runCatching {
|
||||
System.load(file(context, OPENCV).absolutePath)
|
||||
System.load(file(context, MEDIAPIPE).absolutePath)
|
||||
nativeLoaded = true
|
||||
true
|
||||
}.onFailure { Log.e(TAG, "native libraries failed to load", it) }.getOrDefault(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** Blocks the calling worker thread until the pack is usable, starting the download if needed. */
|
||||
fun awaitNative(context: Context, running: () -> Boolean, onState: (AddonPack.State) -> Unit): Boolean =
|
||||
pack.await(context, running, onState) && loadNative(context)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.MotionEvent
|
||||
import kotlin.math.abs
|
||||
|
||||
class DirectTouchHold(
|
||||
private val slopPx: Float,
|
||||
private val listener: PointerGestures.Listener,
|
||||
private val cancelTouch: () -> Unit,
|
||||
) {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var x = 0f
|
||||
private var y = 0f
|
||||
private var consumed = false
|
||||
private var pressed = false
|
||||
private val release = Runnable {
|
||||
if (pressed) listener.onButton(PointerGestures.BTN_RIGHT, false, x, y)
|
||||
pressed = false
|
||||
}
|
||||
private val hold = Runnable {
|
||||
cancelTouch()
|
||||
consumed = true
|
||||
listener.onLongPress()
|
||||
listener.onButton(PointerGestures.BTN_RIGHT, true, x, y)
|
||||
pressed = true
|
||||
handler.postDelayed(release, 50L)
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
handler.removeCallbacks(hold)
|
||||
handler.removeCallbacks(release)
|
||||
release.run()
|
||||
consumed = false
|
||||
}
|
||||
|
||||
fun onTouch(event: MotionEvent): Boolean {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
cancel()
|
||||
x = event.x; y = event.y
|
||||
handler.postDelayed(hold, 500L)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> if (event.pointerCount > 1 ||
|
||||
abs(event.x - x) > slopPx || abs(event.y - y) > slopPx) handler.removeCallbacks(hold)
|
||||
MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP,
|
||||
MotionEvent.ACTION_UP -> handler.removeCallbacks(hold)
|
||||
MotionEvent.ACTION_CANCEL -> cancel()
|
||||
}
|
||||
return consumed
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,8 @@ public class FakeInputWriter {
|
||||
private static final int TRITON_REPORT_OFFSET = TRITON_OFFSET + 24;
|
||||
private static final int TRITON_OUTPUT_SEQ_OFFSET = TRITON_OFFSET + 88;
|
||||
private static final int TRITON_OUTPUT_FEATURE_OFFSET = TRITON_OFFSET + 96;
|
||||
private static final int TRITON_OUTPUT_LENGTH_OFFSET = TRITON_OFFSET + 100;
|
||||
private static final int TRITON_OUTPUT_LENGTH_OFFSET = TRITON_OFFSET + 98;
|
||||
private static final int TRITON_OUTPUT_ACK_OFFSET = TRITON_OFFSET + 100;
|
||||
private static final int TRITON_OUTPUT_OFFSET = TRITON_OFFSET + 104;
|
||||
private static final int TRITON_GET_REQUEST_OFFSET = TRITON_OFFSET + 168;
|
||||
private static final int TRITON_GET_REPLY_OFFSET = TRITON_OFFSET + 176;
|
||||
@@ -229,8 +230,9 @@ public class FakeInputWriter {
|
||||
data.putInt(TRITON_REPORT_ID_OFFSET, 0);
|
||||
data.putInt(TRITON_REPORT_LENGTH_OFFSET, 0);
|
||||
data.putLong(TRITON_OUTPUT_SEQ_OFFSET, 0L);
|
||||
data.putInt(TRITON_OUTPUT_FEATURE_OFFSET, 0);
|
||||
data.putInt(TRITON_OUTPUT_LENGTH_OFFSET, 0);
|
||||
data.putShort(TRITON_OUTPUT_FEATURE_OFFSET, (short) 0);
|
||||
data.putShort(TRITON_OUTPUT_LENGTH_OFFSET, (short) 0);
|
||||
data.putInt(TRITON_OUTPUT_ACK_OFFSET, 0);
|
||||
data.putLong(TRITON_GET_REQUEST_OFFSET, 0L);
|
||||
data.putLong(TRITON_GET_REPLY_OFFSET, 0L);
|
||||
}
|
||||
@@ -587,14 +589,18 @@ public class FakeInputWriter {
|
||||
nativeStoreFence();
|
||||
long seq = ring.getLong(TRITON_OUTPUT_SEQ_OFFSET);
|
||||
if ((seq & 1) != 0 || seq <= lastSequence) return null;
|
||||
int length = ring.getInt(TRITON_OUTPUT_LENGTH_OFFSET);
|
||||
int length = ring.getShort(TRITON_OUTPUT_LENGTH_OFFSET) & 0xffff;
|
||||
if (length <= 0 || length > TRITON_REPORT_MAX) return null;
|
||||
byte[] output = new byte[9 + length];
|
||||
ByteBuffer.wrap(output).order(ByteOrder.LITTLE_ENDIAN).putLong(seq);
|
||||
output[8] = (byte) (ring.getInt(TRITON_OUTPUT_FEATURE_OFFSET) != 0 ? 1 : 0);
|
||||
output[8] = (byte) (ring.getShort(TRITON_OUTPUT_FEATURE_OFFSET) != 0 ? 1 : 0);
|
||||
for (int i = 0; i < length; i++) output[9 + i] = ring.get(TRITON_OUTPUT_OFFSET + i);
|
||||
nativeStoreFence();
|
||||
return seq == ring.getLong(TRITON_OUTPUT_SEQ_OFFSET) ? output : null;
|
||||
if (seq != ring.getLong(TRITON_OUTPUT_SEQ_OFFSET)) return null;
|
||||
// Acknowledge the exact report sequence taken so a concurrent timeout overwrite cannot lose its length.
|
||||
ring.putInt(TRITON_OUTPUT_ACK_OFFSET, (int) seq);
|
||||
nativeStoreFence();
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Modified for DroidDeck on 2026-10-15.
|
||||
*/
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
object GestureKeys {
|
||||
val codes: Map<String, Int> = linkedMapOf(
|
||||
"ESC" to 1,
|
||||
"1" to 2,
|
||||
"2" to 3,
|
||||
"3" to 4,
|
||||
"4" to 5,
|
||||
"5" to 6,
|
||||
"6" to 7,
|
||||
"7" to 8,
|
||||
"8" to 9,
|
||||
"9" to 10,
|
||||
"0" to 11,
|
||||
"MINUS" to 12,
|
||||
"EQUAL" to 13,
|
||||
"BKSP" to 14,
|
||||
"TAB" to 15,
|
||||
"Q" to 16,
|
||||
"W" to 17,
|
||||
"E" to 18,
|
||||
"R" to 19,
|
||||
"T" to 20,
|
||||
"Y" to 21,
|
||||
"U" to 22,
|
||||
"I" to 23,
|
||||
"O" to 24,
|
||||
"P" to 25,
|
||||
"BRACKET_LEFT" to 26,
|
||||
"BRACKET_RIGHT" to 27,
|
||||
"ENTER" to 28,
|
||||
"CTRL_L" to 29,
|
||||
"A" to 30,
|
||||
"S" to 31,
|
||||
"D" to 32,
|
||||
"F" to 33,
|
||||
"G" to 34,
|
||||
"H" to 35,
|
||||
"J" to 36,
|
||||
"K" to 37,
|
||||
"L" to 38,
|
||||
"SEMICOLON" to 39,
|
||||
"APOSTROPHE" to 40,
|
||||
"GRAVE" to 41,
|
||||
"SHIFT_L" to 42,
|
||||
"BACKSLASH" to 43,
|
||||
"Z" to 44,
|
||||
"X" to 45,
|
||||
"C" to 46,
|
||||
"V" to 47,
|
||||
"B" to 48,
|
||||
"N" to 49,
|
||||
"M" to 50,
|
||||
"COMMA" to 51,
|
||||
"PERIOD" to 52,
|
||||
"SLASH" to 53,
|
||||
"SHIFT_R" to 54,
|
||||
"KP_MULTIPLY" to 55,
|
||||
"ALT_L" to 56,
|
||||
"SPACE" to 57,
|
||||
"CAPS_LOCK" to 58,
|
||||
"F1" to 59,
|
||||
"F2" to 60,
|
||||
"F3" to 61,
|
||||
"F4" to 62,
|
||||
"F5" to 63,
|
||||
"F6" to 64,
|
||||
"F7" to 65,
|
||||
"F8" to 66,
|
||||
"F9" to 67,
|
||||
"F10" to 68,
|
||||
"NUM_LOCK" to 69,
|
||||
"SCROLL_LOCK" to 70,
|
||||
"KP_7" to 71,
|
||||
"KP_8" to 72,
|
||||
"KP_9" to 73,
|
||||
"KP_SUBTRACT" to 74,
|
||||
"KP_4" to 75,
|
||||
"KP_5" to 76,
|
||||
"KP_6" to 77,
|
||||
"KP_ADD" to 78,
|
||||
"KP_1" to 79,
|
||||
"KP_2" to 80,
|
||||
"KP_3" to 81,
|
||||
"KP_0" to 82,
|
||||
"KP_DEL" to 83,
|
||||
"F11" to 87,
|
||||
"F12" to 88,
|
||||
"KP_ENTER" to 96,
|
||||
"CTRL_R" to 97,
|
||||
"KP_DIVIDE" to 98,
|
||||
"PRTSCN" to 99,
|
||||
"ALT_R" to 100,
|
||||
"HOME" to 102,
|
||||
"UP" to 103,
|
||||
"PRIOR" to 104,
|
||||
"LEFT" to 105,
|
||||
"RIGHT" to 106,
|
||||
"END" to 107,
|
||||
"DOWN" to 108,
|
||||
"NEXT" to 109,
|
||||
"INSERT" to 110,
|
||||
"DEL" to 111,
|
||||
)
|
||||
@JvmStatic fun code(name: String): Int? = codes[name]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/** Global defaults with optional overrides for the currently focused game. */
|
||||
object GesturePrefs {
|
||||
private fun prefs(context: Context) = context.getSharedPreferences("touch_gestures", Context.MODE_PRIVATE)
|
||||
|
||||
fun hasOverride(context: Context, game: String): Boolean = prefs(context).contains("game.$game")
|
||||
|
||||
fun read(context: Context, game: String? = null): TouchGestureConfig {
|
||||
val p = prefs(context)
|
||||
val json = game?.let { p.getString("game.$it", null) } ?: p.getString("default", null)
|
||||
return if (json == null) TouchGestureConfig() else TouchGestureConfig.fromJson(json)
|
||||
}
|
||||
|
||||
fun save(context: Context, game: String?, config: TouchGestureConfig) {
|
||||
prefs(context).edit().putString(game?.let { "game.$it" } ?: "default", config.toJson()).apply()
|
||||
}
|
||||
|
||||
fun clear(context: Context, game: String?) {
|
||||
prefs(context).edit().remove(game?.let { "game.$it" } ?: "default").apply()
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ public final class PadBridge {
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
private boolean open;
|
||||
private volatile boolean selectSteam;
|
||||
private volatile boolean builtinPad = true;
|
||||
private TouchRoute touchRoute;
|
||||
private final PadState routedTouch = new PadState();
|
||||
private boolean systemGuidePressed;
|
||||
@@ -110,6 +111,35 @@ public final class PadBridge {
|
||||
excluded = devices;
|
||||
}
|
||||
|
||||
public synchronized void setBuiltinPad(boolean on) {
|
||||
boolean changed = builtinPad != on;
|
||||
builtinPad = on;
|
||||
if (changed && !on) resetBuiltin();
|
||||
}
|
||||
|
||||
private static volatile int firstSlot;
|
||||
|
||||
public static void setFirstSlot(int slot) {
|
||||
firstSlot = slot;
|
||||
}
|
||||
|
||||
private int firstPlayerSlot() {
|
||||
return firstSlot;
|
||||
}
|
||||
|
||||
private void resetBuiltin() {
|
||||
routedTouch.clear();
|
||||
systemGuidePressed = false;
|
||||
systemQamPressed = false;
|
||||
qamTapPressed = false;
|
||||
qamChordActive = false;
|
||||
chordButtonPressed = false;
|
||||
qamChordGeneration++;
|
||||
chordHandler.removeCallbacksAndMessages(null);
|
||||
state.clear();
|
||||
publish();
|
||||
}
|
||||
|
||||
public synchronized boolean start() {
|
||||
if (!open) {
|
||||
open = writer.open();
|
||||
@@ -146,9 +176,15 @@ public final class PadBridge {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isSystemVirtualInputName(String name) {
|
||||
if (name == null) return false;
|
||||
String lower = name.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
return lower.startsWith("uinput-") || lower.startsWith("uinput_");
|
||||
}
|
||||
|
||||
public static boolean isFromController(InputDevice device) {
|
||||
if (device == null) return false;
|
||||
if (device.isVirtual()) return false;
|
||||
if (device.isVirtual() || isSystemVirtualInputName(device.getName())) return false;
|
||||
int sources = device.getSources();
|
||||
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) return true;
|
||||
if ((sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) return false;
|
||||
@@ -165,7 +201,7 @@ public final class PadBridge {
|
||||
|
||||
public static boolean isControllerKeyEvent(KeyEvent event) {
|
||||
InputDevice device = event.getDevice();
|
||||
if (device == null || device.isVirtual()) return false;
|
||||
if (device == null || device.isVirtual() || isSystemVirtualInputName(device.getName())) return false;
|
||||
if (KeyEvent.isGamepadButton(event.getKeyCode())) {
|
||||
return device.supportsSource(InputDevice.SOURCE_GAMEPAD)
|
||||
|| device.supportsSource(InputDevice.SOURCE_JOYSTICK);
|
||||
@@ -190,6 +226,7 @@ public final class PadBridge {
|
||||
public static boolean isControllerMotionEvent(MotionEvent event) {
|
||||
InputDevice device = event.getDevice();
|
||||
return event.isFromSource(InputDevice.SOURCE_JOYSTICK) && device != null && !device.isVirtual()
|
||||
&& !isSystemVirtualInputName(device.getName())
|
||||
&& (device.supportsSource(InputDevice.SOURCE_GAMEPAD)
|
||||
|| device.supportsSource(InputDevice.SOURCE_JOYSTICK));
|
||||
}
|
||||
@@ -225,7 +262,9 @@ public final class PadBridge {
|
||||
public synchronized void syncConnectedControllers() {
|
||||
for (int deviceId : InputDevice.getDeviceIds()) {
|
||||
InputDevice device = InputDevice.getDevice(deviceId);
|
||||
if (isFromController(device) && !excluded.test(deviceId)) publish(slotFor(device));
|
||||
if (!isFromController(device) || excluded.test(deviceId)) continue;
|
||||
int slot = slotFor(device);
|
||||
if (slot >= 0) publish(slot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +284,7 @@ public final class PadBridge {
|
||||
public synchronized boolean onKeyEvent(KeyEvent event) {
|
||||
if (!isControllerKeyEvent(event) || excluded.test(event.getDeviceId())) return false;
|
||||
int slot = slotFor(event.getDevice());
|
||||
if (slot < 0) return false;
|
||||
int side = JoyCon.side(event.getDevice());
|
||||
if (side >= 0 && partners[slot] != NO_CONTROLLER && JoyCon.isRail(side, event.getScanCode())) return true;
|
||||
PadState pad = stateFor(slot);
|
||||
@@ -293,6 +333,7 @@ public final class PadBridge {
|
||||
if (!isControllerMotionEvent(event) || excluded.test(event.getDeviceId())) return false;
|
||||
if (event.getAction() != MotionEvent.ACTION_MOVE) return false;
|
||||
int slot = slotFor(event.getDevice());
|
||||
if (slot < 0) return false;
|
||||
PadState pad = stateFor(slot);
|
||||
noteDevice(event.getDevice(), slot);
|
||||
statAxes++;
|
||||
@@ -319,16 +360,27 @@ public final class PadBridge {
|
||||
boolean wired = wiredXboxLayout(event);
|
||||
pad.rightX = rightX(event, wired);
|
||||
pad.rightY = rightY(event, wired);
|
||||
float lt = leftTrigger(event, wired);
|
||||
float rt = rightTrigger(event, wired);
|
||||
pad.leftTrigger = lt;
|
||||
pad.rightTrigger = rt;
|
||||
float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
||||
float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
||||
pad.up = hatY < -0.5f;
|
||||
pad.right = hatX > 0.5f;
|
||||
pad.down = hatY > 0.5f;
|
||||
pad.left = hatX < -0.5f;
|
||||
int source = event.getSource();
|
||||
float lt = 0f;
|
||||
float rt = 0f;
|
||||
if (wired || hasAxis(device, source, MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_BRAKE)) {
|
||||
lt = leftTrigger(event, wired);
|
||||
pad.leftTrigger = lt;
|
||||
}
|
||||
if (wired || hasAxis(device, source, MotionEvent.AXIS_RTRIGGER, MotionEvent.AXIS_GAS)) {
|
||||
rt = rightTrigger(event, wired);
|
||||
pad.rightTrigger = rt;
|
||||
}
|
||||
if (hasAxis(device, source, MotionEvent.AXIS_HAT_X)) {
|
||||
float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
||||
pad.right = hatX > 0.5f;
|
||||
pad.left = hatX < -0.5f;
|
||||
}
|
||||
if (hasAxis(device, source, MotionEvent.AXIS_HAT_Y)) {
|
||||
float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
||||
pad.up = hatY < -0.5f;
|
||||
pad.down = hatY > 0.5f;
|
||||
}
|
||||
if (Math.max(Math.max(Math.abs(pad.leftX), Math.abs(pad.leftY)), Math.max(Math.abs(pad.rightX), Math.abs(pad.rightY))) > 0.5f
|
||||
|| lt > 0.5f || rt > 0.5f || pad.up || pad.right || pad.down || pad.left) {
|
||||
notePlayerInput();
|
||||
@@ -337,6 +389,12 @@ public final class PadBridge {
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean hasAxis(InputDevice device, int source, int... axes) {
|
||||
if (device == null) return false;
|
||||
for (int axis : axes) if (device.getMotionRange(axis, source) != null) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface TouchRoute {
|
||||
boolean active();
|
||||
|
||||
@@ -349,6 +407,7 @@ public final class PadBridge {
|
||||
}
|
||||
|
||||
public synchronized void applyTouch(java.util.function.Consumer<PadState> mutation) {
|
||||
if (!builtinPad) return;
|
||||
if (touchRoute != null && touchRoute.active()) {
|
||||
mutation.accept(routedTouch);
|
||||
touchRoute.accept(routedTouch);
|
||||
@@ -388,6 +447,7 @@ public final class PadBridge {
|
||||
}
|
||||
|
||||
public synchronized void setSystemButtons(boolean guidePressed, boolean qamPressed) {
|
||||
if (!builtinPad) return;
|
||||
if (systemGuidePressed == guidePressed && systemQamPressed == qamPressed) return;
|
||||
boolean qamStarted = qamPressed && !systemQamPressed;
|
||||
systemGuidePressed = guidePressed;
|
||||
@@ -419,6 +479,7 @@ public final class PadBridge {
|
||||
}
|
||||
|
||||
public synchronized void triggerQam() {
|
||||
if (!builtinPad) return;
|
||||
if (!nativeQam()) {
|
||||
startChord(PadState.A);
|
||||
return;
|
||||
@@ -437,6 +498,7 @@ public final class PadBridge {
|
||||
}
|
||||
|
||||
public synchronized void triggerSteamKeyboard() {
|
||||
if (!builtinPad) return;
|
||||
startChord(PadState.X);
|
||||
}
|
||||
|
||||
@@ -486,25 +548,25 @@ public final class PadBridge {
|
||||
if (owners[slot] != NO_CONTROLLER && InputDevice.getDevice(owners[slot]) == null) leave(slot, owners[slot]);
|
||||
}
|
||||
int side = JoyCon.side(device);
|
||||
for (int slot = 0; side >= 0 && slot < SLOTS; slot++) {
|
||||
for (int slot = firstPlayerSlot(); side >= 0 && slot < SLOTS; slot++) {
|
||||
if (owners[slot] == NO_CONTROLLER || partners[slot] != NO_CONTROLLER) continue;
|
||||
InputDevice owner = InputDevice.getDevice(owners[slot]);
|
||||
if (JoyCon.side(owner) != 1 - side) continue;
|
||||
partners[slot] = id;
|
||||
slotJoyCons.set(slot * 2 + side, id);
|
||||
slotJoyCons.set(slot * 2 + 1 - side, owner.getId());
|
||||
if (slot == 0) releaseBackPaddles();
|
||||
if (slot == firstPlayerSlot()) releaseBackPaddles();
|
||||
stateFor(slot).clear();
|
||||
Log.i(TAG, "\"" + device.getName() + "\" (id " + id + ") joins \"" + owner.getName() + "\" as player " + (slot + 1));
|
||||
return slot;
|
||||
}
|
||||
for (int slot = 0; slot < SLOTS; slot++) {
|
||||
for (int slot = firstPlayerSlot(); slot < SLOTS; slot++) {
|
||||
if (owners[slot] != NO_CONTROLLER) continue;
|
||||
owners[slot] = id;
|
||||
Log.i(TAG, "\"" + device.getName() + "\" (id " + id + ") is player " + (slot + 1));
|
||||
return slot;
|
||||
}
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private PadState stateFor(int slot) {
|
||||
@@ -513,7 +575,8 @@ public final class PadBridge {
|
||||
|
||||
private boolean isPrimary(KeyEvent event) {
|
||||
int id = event.getDevice().getId();
|
||||
return owners[0] == id || partners[0] == id;
|
||||
int first = firstPlayerSlot();
|
||||
return owners[first] == id || partners[first] == id;
|
||||
}
|
||||
|
||||
private void releaseBackPaddles() {
|
||||
@@ -532,7 +595,7 @@ public final class PadBridge {
|
||||
clearJoyCons(slot);
|
||||
if (slotControllers.get(slot) == id) slotControllers.set(slot, owners[slot]);
|
||||
if (activeControllerId == id) activeControllerId = owners[slot];
|
||||
if (slot == 0) releaseBackPaddles();
|
||||
if (slot == firstPlayerSlot()) releaseBackPaddles();
|
||||
stateFor(slot).clear();
|
||||
publish(slot);
|
||||
}
|
||||
@@ -548,8 +611,8 @@ public final class PadBridge {
|
||||
partners[slot] = NO_CONTROLLER;
|
||||
clearJoyCons(slot);
|
||||
slotControllers.set(slot, NO_CONTROLLER);
|
||||
if (slot == firstPlayerSlot()) releaseBackPaddles();
|
||||
if (slot == 0) {
|
||||
releaseBackPaddles();
|
||||
state.clear();
|
||||
publish();
|
||||
return;
|
||||
@@ -608,6 +671,7 @@ public final class PadBridge {
|
||||
}
|
||||
|
||||
private void publish() {
|
||||
if (!builtinPad && !open && owners[0] == NO_CONTROLLER && partners[0] == NO_CONTROLLER) return;
|
||||
if (!open && !start()) {
|
||||
statDropped++;
|
||||
return;
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.ArrayDeque
|
||||
@@ -51,6 +52,10 @@ class SteamControllerBle(private val context: Context) {
|
||||
|
||||
private fun connect() {
|
||||
if (!running || gatt != null) return
|
||||
if (SessionState.suspended) {
|
||||
handler.postDelayed(::connect, RETRY_MS)
|
||||
return
|
||||
}
|
||||
val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
|
||||
val bonded = runCatching { adapter?.takeIf(BluetoothAdapter::isEnabled)?.bondedDevices }
|
||||
.onFailure { Log.w(TAG, "paired devices unreadable", it) }
|
||||
|
||||
Loaded 100 of 574 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user