mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-20 22:24:31 -04:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9a199285e | ||
|
|
d8de6895c1 | ||
|
|
fd7cb249b1 | ||
|
|
21742403a4 | ||
|
|
46335b7871 | ||
|
|
5e9ebbc7df | ||
|
|
92f2696a0c | ||
|
|
59f752c70d | ||
|
|
828374f236 | ||
|
|
0a6e08d923 | ||
|
|
2f5537acef | ||
|
|
15ef4b064b | ||
|
|
52d351908f | ||
|
|
19a8beea52 | ||
|
|
65c6ca3552 | ||
|
|
220442d634 | ||
|
|
d344b7dcba | ||
|
|
fd6111fc69 | ||
|
|
389bef5ad3 |
No files matched your search
@@ -2,11 +2,3 @@ tools/linuxfs/** text=auto eol=lf
|
||||
tools/mangoapp/** text=auto eol=lf
|
||||
tools/comet/LICENSE whitespace=-blank-at-eof
|
||||
tools/comet/droiddeck.patch whitespace=-blank-at-eol,-blank-at-eof
|
||||
|
||||
# These helpers are packaged verbatim and executed inside the Linux runtime.
|
||||
tools/linuxfs/overlay/** text eol=lf
|
||||
tools/linuxfs/desktop/** text eol=lf
|
||||
tools/mangoapp/mangoapp text eol=lf
|
||||
app/src/main/assets/desktop-removal/*.json text eol=lf
|
||||
app/src/main/assets/desktop-removal/*.txt text eol=lf
|
||||
*.pdf binary
|
||||
@@ -64,7 +64,6 @@ 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
|
||||
@@ -75,8 +74,6 @@ 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:
|
||||
@@ -91,6 +88,5 @@ 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.i18n.txt out/desktop-kde-i18n-*.tar.zst
|
||||
out/desktop-kde.tar.zst out/desktop-kde.sha256 out/desktop-kde.packages.txt out/desktop-kde.kwin.txt
|
||||
gh release edit "$TAG" --latest=false --prerelease=false
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Build FEX (x86-64 Steam client)
|
||||
|
||||
# Checks tools/fex: the same build DroidDeck-Components' build-fex.yml runs from this repository to publish FEX
|
||||
# (its single `fex` release and fex.json, which the app follows). Nothing is published from here.
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'tools/fex/**'
|
||||
- '.github/workflows/build-fex.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
thunks:
|
||||
description: 'Build the host thunks (Vulkan/GL/X11/... to the runtime arm64 libraries)'
|
||||
required: false
|
||||
default: 'false'
|
||||
cxxflags:
|
||||
description: 'Extra CXXFLAGS for a diagnostic build (e.g. -DDEBUG_STRACE)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build in Arch Linux ARM
|
||||
# Inputs reach the shell as environment variables, never pasted into the script.
|
||||
env:
|
||||
FEX_EXTRA_CXXFLAGS: ${{ inputs.cxxflags }}
|
||||
FEX_THUNKS: ${{ inputs.thunks == 'true' && '1' || '0' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm -e FEX_EXTRA_CXXFLAGS="$FEX_EXTRA_CXXFLAGS" -e FEX_THUNKS="$FEX_THUNKS" -v "$PWD:/work" -w /work menci/archlinuxarm:base-devel bash tools/fex/build-in-arch.sh
|
||||
sudo chown -R "$(id -u):$(id -g)" .
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fex
|
||||
path: |
|
||||
fex.tzst
|
||||
fex.tzst.sha256
|
||||
fex-needed.txt
|
||||
fex-files.txt
|
||||
+29
-177
@@ -26,67 +26,10 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
PRELOAD_CACHE_VERSION: v6
|
||||
PRELOAD_CACHE_VERSION: v3
|
||||
|
||||
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 "[email protected]"
|
||||
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:
|
||||
needs: xbox
|
||||
name: Build APK
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -98,7 +41,6 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check co-authors
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -144,17 +86,14 @@ 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 "[email protected]"
|
||||
git fetch --no-tags origin main
|
||||
git fetch --no-tags origin "$BUILD_BASE_SHA"
|
||||
BASE=$(git rev-parse --short FETCH_HEAD)
|
||||
BASE=$(git rev-parse --short origin/main)
|
||||
HEAD=$(git rev-parse --short HEAD)
|
||||
if git merge --no-ff --no-edit FETCH_HEAD >/tmp/merge.out 2>&1; then
|
||||
if git merge --no-ff --no-edit origin/main >/tmp/merge.out 2>&1; then
|
||||
{
|
||||
echo "### Mergeable"
|
||||
echo
|
||||
@@ -201,11 +140,6 @@ 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
|
||||
@@ -250,14 +184,15 @@ 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/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/**', '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/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/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 -o Dir::Etc::sourcelist=sources.list.d/ubuntu.sources \
|
||||
-o Dir::Etc::sourceparts=- -o Acquire::Retries=3 update -qq
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libvulkan1 mesa-vulkan-drivers >/dev/null
|
||||
D=app/src/main/assets/linuxfs
|
||||
mkdir -p "$D"
|
||||
@@ -290,7 +225,7 @@ jobs:
|
||||
done
|
||||
if [ -f tools/gamescope/release.env ]; then
|
||||
. tools/gamescope/release.env
|
||||
curl -fsSL --retry 3 -o gamescope.tzst "https://github.com/${GAMESCOPE_REPO:-${GITHUB_REPOSITORY}}/releases/download/${GAMESCOPE_TAG}/gamescope.tzst"
|
||||
gh release download "${GAMESCOPE_TAG}" -R "${GAMESCOPE_REPO:-${GITHUB_REPOSITORY}}" -p gamescope.tzst -O gamescope.tzst
|
||||
echo "${GAMESCOPE_SHA256} gamescope.tzst" | sha256sum -c -
|
||||
tar --use-compress-program=unzstd -xf gamescope.tzst -C "$D"
|
||||
test -f "$D/usr/local/bin/gamescope"
|
||||
@@ -378,32 +313,27 @@ jobs:
|
||||
set -euo pipefail
|
||||
docker run --rm -v "$PWD:/src" -w /src public.ecr.aws/docker/library/debian:bullseye bash -c '
|
||||
set -euo pipefail
|
||||
install_compilers() {
|
||||
apt-get -o Acquire::Check-Valid-Until=false update -qq &&
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends gcc g++ gcc-multilib g++-multilib binutils >/dev/null
|
||||
}
|
||||
printf "deb http://deb.debian.org/debian bullseye main\ndeb http://security.debian.org/debian-security bullseye-security main\n" > /etc/apt/sources.list
|
||||
if ! install_compilers; then
|
||||
printf "deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian-security bullseye-security main\n" > /etc/apt/sources.list
|
||||
install_compilers
|
||||
fi
|
||||
printf "deb http://archive.debian.org/debian bullseye main\ndeb http://archive.debian.org/debian-security bullseye-security main\n" > /etc/apt/sources.list
|
||||
apt-get -o Acquire::Check-Valid-Until=false update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends gcc g++ gcc-multilib g++-multilib binutils >/dev/null
|
||||
tools/linuxfs/build-x86-preloads.sh app/src/main/assets/linuxfs
|
||||
'
|
||||
sudo chown -R "$(id -u):$(id -g)" app/src/main/assets/linuxfs
|
||||
ls -l app/src/main/assets/linuxfs/x86_64 app/src/main/assets/linuxfs/i386
|
||||
|
||||
- name: Bundle the droiddeck-esync packs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -f tools/droiddeck-esync/release.env ]; then
|
||||
. tools/droiddeck-esync/release.env
|
||||
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"
|
||||
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p "$SYNC_BUNDLE_ASSET" -O sync-bundle.tzst
|
||||
echo "$SYNC_BUNDLE_SHA256 sync-bundle.tzst" | sha256sum -c -
|
||||
mkdir -p app/src/main/assets/droiddeck-esync
|
||||
tar --use-compress-program=unzstd -xf sync-bundle.tzst -C app/src/main/assets/droiddeck-esync
|
||||
rm sync-bundle.tzst
|
||||
curl -fsSL --retry 3 -o sync-index.json "$release_url/index.json"
|
||||
gh release download "$SYNC_BUNDLE_TAG" -R "$SYNC_BUNDLE_REPO" -p index.json -O sync-index.json
|
||||
revoked=$(python3 -c 'import json, sys; print(" ".join(p["id"] for p in json.load(open(sys.argv[1]))["packs"] if p.get("revoked") is True))' sync-index.json)
|
||||
for id in $revoked; do
|
||||
if [ -e "app/src/main/assets/droiddeck-esync/packs/$id.tzst" ]; then
|
||||
@@ -468,12 +398,6 @@ jobs:
|
||||
|
||||
- name: Check Quest tracking IPC
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
||||
VK_INCLUDE=$(ls -d "$NDK"/toolchains/llvm/prebuilt/*/sysroot/usr/include | head -1)
|
||||
mkdir -p build/vr/include
|
||||
cp -R "$VK_INCLUDE/vulkan" build/vr/include/
|
||||
if [ -d "$VK_INCLUDE/vk_video" ]; then cp -R "$VK_INCLUDE/vk_video" build/vr/include/; fi
|
||||
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_tracking.cpp -o /tmp/test-xr-tracking
|
||||
/tmp/test-xr-tracking
|
||||
g++ -std=c++17 -Wall -Wextra -Werror tools/vr/tests/test_xr_touch.cpp -o /tmp/test-xr-touch
|
||||
@@ -527,11 +451,6 @@ 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: |
|
||||
@@ -539,9 +458,6 @@ 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:
|
||||
@@ -580,33 +496,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: python3 -m unittest discover -s tools/tests -p 'test_*.py'
|
||||
|
||||
- name: Unit tests
|
||||
id: unit-tests
|
||||
run: |
|
||||
set -o pipefail
|
||||
mkdir -p app/build
|
||||
./gradlew testReleaseUnitTest --console=plain --info --stacktrace -Drobolectric.logging=stderr -PndkVersion="$NDK_VERSION" 2>&1 | tee app/build/unit-tests.log
|
||||
|
||||
- name: Upload unit test diagnostics
|
||||
id: unit-test-diagnostics
|
||||
if: always() && steps.unit-tests.outcome != 'skipped'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unit-test-results
|
||||
path: |
|
||||
app/build/unit-tests.log
|
||||
app/build/test-results/testReleaseUnitTest/
|
||||
app/build/reports/tests/testReleaseUnitTest/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
run: ./gradlew testReleaseUnitTest --console=plain -PndkVersion="$NDK_VERSION"
|
||||
|
||||
- name: Sign with the testkey (v1 + v2 + v3)
|
||||
run: |
|
||||
@@ -636,23 +530,6 @@ jobs:
|
||||
grep -qi "CN=Android, OU=Android, O=Android" /tmp/sig.txt \
|
||||
|| { echo "::error::apk is not signed with the AOSP testkey"; exit 1; }
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
id: apk-upload
|
||||
with:
|
||||
name: droiddeck-apk
|
||||
path: app/build/outputs/apk/release/*.apk
|
||||
if-no-files-found: error
|
||||
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
|
||||
@@ -661,9 +538,6 @@ jobs:
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
BUILD_OUTCOME: ${{ steps.build.outcome }}
|
||||
UNIT_TEST_OUTCOME: ${{ steps.unit-tests.outcome }}
|
||||
TEST_DIAGNOSTICS_URL: ${{ steps.unit-test-diagnostics.outputs.artifact-url }}
|
||||
APK_OUTCOME: ${{ steps.apk-upload.outcome }}
|
||||
SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
@@ -674,8 +548,8 @@ jobs:
|
||||
echo
|
||||
[ -f /tmp/pr-merge.md ] && cat /tmp/pr-merge.md
|
||||
echo
|
||||
case "$BUILD_OUTCOME:$UNIT_TEST_OUTCOME:$APK_OUTCOME" in
|
||||
success:success:success)
|
||||
case "$BUILD_OUTCOME" in
|
||||
success)
|
||||
echo "### Build passed"
|
||||
echo
|
||||
echo "The \`droiddeck-apk\` artifact is [on this run]($RUN_URL), signed with the public test key."
|
||||
@@ -688,28 +562,7 @@ jobs:
|
||||
echo
|
||||
echo "Fallback: [open **PR Release Publish**]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/workflows/pr-release-publish.yml) on \`main\` and enter PR \`$PR\`."
|
||||
;;
|
||||
success:success:*)
|
||||
echo "### APK publication incomplete"
|
||||
echo
|
||||
echo "Compilation and unit tests passed, but APK publication did not complete. See [the run]($RUN_URL)."
|
||||
;;
|
||||
success:failure:*)
|
||||
echo "### ERROR: Unit tests failed"
|
||||
echo
|
||||
echo "The APK compiled, but unit tests failed. No installable artifact was published."
|
||||
echo
|
||||
if [ -n "$TEST_DIAGNOSTICS_URL" ]; then
|
||||
echo "The [unit-test-results]($TEST_DIAGNOSTICS_URL) artifact contains the test log and any generated reports."
|
||||
else
|
||||
echo "The test diagnostics upload did not complete. See [the test log]($RUN_URL)."
|
||||
fi
|
||||
;;
|
||||
success:*)
|
||||
echo "### Build incomplete"
|
||||
echo
|
||||
echo "The APK compiled, but unit tests did not complete successfully. See [the run]($RUN_URL)."
|
||||
;;
|
||||
failure:*)
|
||||
failure)
|
||||
echo "### ERROR: Build failed"
|
||||
echo
|
||||
echo "The compiler's errors ([full log]($RUN_URL)):"
|
||||
@@ -727,7 +580,7 @@ jobs:
|
||||
echo '```'
|
||||
echo "</details>"
|
||||
;;
|
||||
skipped:*|cancelled:*)
|
||||
skipped|cancelled)
|
||||
echo "### Not built"
|
||||
echo
|
||||
echo "The build did not run (see above, or [the run]($RUN_URL))."
|
||||
@@ -743,6 +596,14 @@ jobs:
|
||||
gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" -F body=@/tmp/pr-report.md >/dev/null
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: droiddeck-apk
|
||||
path: app/build/outputs/apk/release/*.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
|
||||
sign:
|
||||
needs: build
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
@@ -755,17 +616,8 @@ 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, packs]
|
||||
needs: [build, sign]
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -176,27 +176,6 @@ 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 }}
|
||||
@@ -254,7 +233,6 @@ 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 .)
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
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,19 +63,9 @@ 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, packs]
|
||||
needs: [find, sign]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
|
||||
@@ -15,14 +15,10 @@ DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big
|
||||
|
||||
## Requirements and install
|
||||
|
||||
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts. **Stores** (Setup › Stores › "Show Stores in the rail") signs in to GOG, Epic Games and Amazon Games and downloads games into the Games storage. Epic keeps its direct API install path. Opening Epic Stores prepares Epic’s official Linux launcher preview. When installed, Epic games launched from DroidDeck use that client, and games installed in the client are imported into DroidDeck’s library and shortcuts. The x86-64 client runs through FEX on ARM and DroidDeck retains its Proton hooks; this preview and ARM setup are outside Epic’s listed Linux support. The cog on a store’s chip row sets its opening tab and download speed tier.
|
||||
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts. **Stores** (Setup › Stores › "Show Stores in the rail") signs in to GOG, Epic Games and Amazon Games, browses their catalogs and libraries, downloads games into the Games storage and adds each one to the Steam client as a non-Steam game, so it launches through Proton like everything else; the cog on its chip row sets which tab a store opens on and the download speed tier.
|
||||
|
||||
On Android 14 and newer, before Steam launches, you must turn on **Disable child process restrictions** in Developer options. If this option is not available in developer settings (Android 12 and 13 devices), first launch of Steam will present a "Fix it for me" button, which will help automate the setup process.
|
||||
|
||||
## Smart glasses
|
||||
|
||||
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.
|
||||
|
||||
## Community
|
||||
|
||||
Join the [DroidDeck Discord](https://discord.gg/JRGAvawjsm) for help, Preview builds, and device reports. Bug reports go in its **#bug-reports** forum; attach the zip from **Share logs** (Setup, or the session drawer) so the logs come with it.
|
||||
@@ -33,12 +29,6 @@ Run `tools/build_local.sh` with Docker, Java 17, the Android SDK/NDK, Rust throu
|
||||
|
||||
After preparation, ordinary `./gradlew :app:assembleRelease` builds can reuse the verified inputs. If native sources, component pins or staged files change, packaging stops with instructions to rerun the helper. Generated audio stays under `app/build/`; the tracked base bundle is never modified. `./gradlew -PskipRust=true :app:testDebugUnitTest` needs no native preparation. For an APK without store engines, use `DROIDDECK_SKIP_RUST=1 tools/build_local.sh`; it omits cached engine libraries too.
|
||||
|
||||
For the prepared Windows checkout, run `./tools/run_windows.ps1` (optionally with `-Serial <adb-serial>`), or select **DroidDeck XR** in Android Studio and run the **app** module on the connected phone. Machine-specific options live in the ignored `local.properties`: `droiddeck.python` selects a Python executable, `droiddeck.skipRust=true` omits the optional store download engines, and `droiddeck.debugApplicationIdSuffix=.xr` installs **DroidDeck XR Dev** alongside the signed release. The development app has its own runtime and Steam login. The local Java selection is in `.gradle/config.properties`.
|
||||
|
||||
`python tools/prepare_windows_runtime.py` restores this checkout's audited runtime inputs from the pinned CI APK, three locally rebuilt preload libraries, and the checksum-pinned Gamescope archive in `build/windows-setup/`. Start Docker Desktop's Linux engine first: preparation now rebuilds DroidDeck's ARM64 VR driver and Vulkan layer from current source and runs their native regression tests. The compiler image is built automatically on first use. Vulkan headers come from the NDK beside `--llvm-strip`, or from an explicit `--vk-include <NDK-sysroot-include>` directory.
|
||||
|
||||
To reuse that build, pass `--rebuilt-vr build/windows-setup/vr`. Preparation verifies its `vr-build.json` against the current Git revision, source files, Vulkan headers, output checksums, and ARM64 architecture; missing or stale builds fail instead of falling back to the donor's VR libraries. The script checks the actual donor CI merge tree, still rejects unrelated unaudited runtime changes, and records component provenance in `app/build/windows-runtime-provenance.json`. Preserve these ignored preparation files when rebuilding locally; `clean` removes the prepared assets and their verification manifest.
|
||||
|
||||
Run the helper and preload regression suite with `bash tools/test_local.sh`. On macOS it runs in Linux through Docker, matching the helpers' runtime and CI instead of compiling Linux preloads against macOS headers.
|
||||
|
||||
## Limits
|
||||
@@ -47,12 +37,4 @@ Compatibility and performance vary by device; hardware validation is limited. De
|
||||
|
||||
## Credits and licence
|
||||
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). 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.
|
||||
|
||||
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.
|
||||
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. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
+6
-100
@@ -4,32 +4,6 @@ plugins {
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
||||
|
||||
// Machine-specific tooling and optional CI-prepared inputs stay out of version control.
|
||||
def localBuildOptions = new Properties()
|
||||
def localBuildOptionsFile = rootProject.layout.projectDirectory.file('local.properties')
|
||||
if (localBuildOptionsFile.asFile.exists()) {
|
||||
localBuildOptions.load(new StringReader(providers.fileContents(localBuildOptionsFile).asText.get()))
|
||||
}
|
||||
def pythonExecutable = project.findProperty('pythonExecutable')
|
||||
?: localBuildOptions.getProperty('droiddeck.python', 'python3')
|
||||
def vitureSdkDir = project.hasProperty('vitureSdkDir') ? project.property('vitureSdkDir') : localBuildOptions.getProperty('droiddeck.vitureSdk', '')
|
||||
def uvcDir = project.hasProperty('uvcDir') ? project.property('uvcDir') : localBuildOptions.getProperty('droiddeck.uvcDir', '')
|
||||
if (uvcDir) {
|
||||
['arm64-v8a/libusb.so', 'arm64-v8a/libuvc.a', 'include/libusb.h', 'include/libuvc/libuvc.h',
|
||||
'licenses/libusb-COPYING', 'licenses/libuvc-LICENSE.txt'].each { required ->
|
||||
if (!new File(uvcDir, required).isFile()) throw new GradleException("Incomplete UVC build: missing ${required}")
|
||||
}
|
||||
}
|
||||
if (vitureSdkDir) {
|
||||
['android/arm64-v8a/libglasses.so', 'android/arm64-v8a/libcarina_vio.so',
|
||||
'android/arm64-v8a/libcloud_protocol.so', 'include/viture_glasses_provider.h',
|
||||
'android/LICENSE', 'android/NOTICE'].each { required ->
|
||||
if (!new File(vitureSdkDir, required).isFile()) {
|
||||
throw new GradleException("Incomplete VITURE Android SDK: missing ${required}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def gitValue = { List<String> arguments ->
|
||||
try {
|
||||
def output = providers.exec {
|
||||
@@ -84,23 +58,6 @@ 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')
|
||||
@@ -122,8 +79,8 @@ android {
|
||||
applicationId "com.droiddeck.launcher"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 28
|
||||
versionCode 14
|
||||
versionName "0.5.0"
|
||||
versionCode 13
|
||||
versionName "0.4.0"
|
||||
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
|
||||
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
|
||||
buildConfigField "int", "PR_NUMBER", "${prNumber}"
|
||||
@@ -136,10 +93,6 @@ 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'
|
||||
@@ -147,8 +100,6 @@ android {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments '-DANDROID_STL=c++_shared'
|
||||
arguments "-DDROIDDECK_VITURE_SDK_DIR=${vitureSdkDir.toString().replace('\\', '/')}"
|
||||
arguments "-DDROIDDECK_UVC_DIR=${uvcDir.toString().replace('\\', '/')}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,11 +128,6 @@ android {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
signingConfig signingConfigs.testkey
|
||||
def localSuffix = localBuildOptions.getProperty('droiddeck.debugApplicationIdSuffix')
|
||||
if (localSuffix) {
|
||||
applicationIdSuffix localSuffix
|
||||
resValue 'string', 'app_name', 'DroidDeck XR Dev'
|
||||
}
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
@@ -193,11 +139,6 @@ 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']
|
||||
@@ -205,8 +146,6 @@ android {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
pickFirsts += ['**/libtermux.so']
|
||||
// Shipped in the proot and tracking packs (tools/packs) and run or loaded from there.
|
||||
excludes += ['**/libproot.so', '**/libproot-loader.so', '**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,12 +158,6 @@ android {
|
||||
testOptions {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -236,16 +169,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// Vendor binaries remain in the user's SDK directory; never vendor them into Git.
|
||||
if (vitureSdkDir) {
|
||||
android.sourceSets.main.jniLibs.srcDir(new File(vitureSdkDir, 'android'))
|
||||
def stageVitureNotices = tasks.register('stageVitureNotices', Copy) {
|
||||
from(new File(vitureSdkDir, 'android')) { include 'LICENSE', 'NOTICE' }
|
||||
into(layout.buildDirectory.dir('vitureAssets/licenses/viture'))
|
||||
}
|
||||
tasks.named('preBuild').configure { dependsOn(stageVitureNotices) }
|
||||
}
|
||||
|
||||
def stageSessionScripts = tasks.register('stageSessionScripts', Copy) {
|
||||
from(rootProject.file('tools/linuxfs/overlay')) {
|
||||
include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**'
|
||||
@@ -262,21 +185,12 @@ def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
|
||||
into(runtimeAssetsDir)
|
||||
}
|
||||
android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir])
|
||||
if (vitureSdkDir) android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('vitureAssets'))
|
||||
if (uvcDir) {
|
||||
def stageUvcNotices = tasks.register('stageUvcNotices', Copy) {
|
||||
from(new File(uvcDir, 'licenses'))
|
||||
into(layout.buildDirectory.dir('uvcAssets/licenses/uvc'))
|
||||
}
|
||||
tasks.named('preBuild').configure { dependsOn(stageUvcNotices) }
|
||||
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('uvcAssets'))
|
||||
}
|
||||
tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) }
|
||||
|
||||
def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) {
|
||||
mustRunAfter(stageSessionScripts, stageRuntimeAssets)
|
||||
workingDir rootProject.projectDir
|
||||
commandLine pythonExecutable, 'tools/release/runtime_inputs.py', 'verify'
|
||||
commandLine 'python3', 'tools/release/runtime_inputs.py', 'verify'
|
||||
}
|
||||
tasks.configureEach { task ->
|
||||
if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs)
|
||||
@@ -284,8 +198,7 @@ tasks.configureEach { task ->
|
||||
|
||||
def rustCrateDir = layout.projectDirectory.dir('src/main/rust/stores')
|
||||
def rustJniLibsDir = layout.buildDirectory.dir('rustJniLibs')
|
||||
def skipRust = (project.findProperty('skipRust')
|
||||
?: localBuildOptions.getProperty('droiddeck.skipRust', 'false')).toString().toBoolean()
|
||||
def skipRust = (project.findProperty('skipRust') ?: 'false').toString().toBoolean()
|
||||
def buildRustStores = tasks.register('buildRustStores', Exec) {
|
||||
description = 'Cross-compiles the store download engines (libdroiddeckstores.so) with cargo-ndk.'
|
||||
onlyIf { !skipRust }
|
||||
@@ -313,9 +226,8 @@ if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir)
|
||||
tasks.named('preBuild').configure { dependsOn(buildRustStores) }
|
||||
|
||||
dependencies {
|
||||
implementation "org.opencv:opencv:${trackingPack.OPENCV_VERSION}"
|
||||
implementation 'org.khronos.openxr:openxr_loader_for_android:1.1.49'
|
||||
implementation 'com.github.luben:zstd-jni:1.5.7-4@aar'
|
||||
implementation 'com.github.luben:zstd-jni:1.5.2-3@aar'
|
||||
implementation 'org.apache.commons:commons-compress:1.27.1'
|
||||
implementation 'org.tukaani:xz:1.10'
|
||||
implementation platform('androidx.compose:compose-bom:2024.02.00')
|
||||
@@ -330,15 +242,9 @@ dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
implementation 'com.flyfishxu:kadb-android:1.2.1'
|
||||
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 'androidx.webkit:webkit:1.12.1'
|
||||
implementation("com.google.mediapipe:tasks-vision:${trackingPack.MEDIAPIPE_VERSION}") {
|
||||
exclude group: 'com.google.auto.value', module: 'auto-value'
|
||||
}
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||
testImplementation 'androidx.compose.ui:ui-test-junit4'
|
||||
testRuntimeOnly 'com.github.luben:zstd-jni:1.5.7-4@jar'
|
||||
testRuntimeOnly 'com.github.luben:zstd-jni:1.5.2-3@jar'
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
@@ -20,19 +19,14 @@
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.front" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.vr.headtracking" android:version="1" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -44,9 +38,6 @@
|
||||
android:isGame="true">
|
||||
|
||||
<meta-data android:name="android.game_mode_config" android:resource="@xml/game_mode_config" />
|
||||
<service android:name=".glasses.LumaRgbCaptureService" android:exported="false"
|
||||
android:process=":rgb_diagnostic" />
|
||||
<meta-data android:name="com.oculus.supportedDevices" android:value="quest2|quest3|quest3s" />
|
||||
<activity android:name=".QuestXrActivity" android:exported="true" android:launchMode="singleTask"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:resizeableActivity="false" android:screenOrientation="landscape"
|
||||
@@ -65,11 +56,14 @@
|
||||
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" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.CAR_LAUNCHER" />
|
||||
<data android:scheme="droiddeck" android:host="game" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
@@ -79,35 +73,9 @@
|
||||
<intent-filter>
|
||||
<action android:name="com.droiddeck.launcher.LAUNCH_GAME" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.CAR_LAUNCHER" />
|
||||
</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"
|
||||
@@ -147,15 +115,11 @@
|
||||
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"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
@@ -177,25 +141,6 @@
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.SecondScreenActivity"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/SessionTheme"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
|
||||
|
||||
<activity
|
||||
android:name=".gpu.SteamVrRestartActivity"
|
||||
android:exported="false"
|
||||
android:process=":vr_recovery"
|
||||
android:taskAffinity="${applicationId}.vr_recovery"
|
||||
android:excludeFromRecents="true"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density"
|
||||
android:theme="@style/SessionTheme" />
|
||||
|
||||
<service
|
||||
android:name=".session.SessionService"
|
||||
android:exported="false"
|
||||
@@ -235,13 +180,5 @@
|
||||
android:exported="true"
|
||||
android:permission="android.permission.DUMP"
|
||||
android:grantUriPermissions="false" />
|
||||
<activity android:name=".ui.SpatialTrackingActivity" android:exported="false" />
|
||||
<service
|
||||
android:name="com.google.android.datatransport.runtime.backends.TransportBackendDiscovery"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="backend:com.google.android.datatransport.cct.CctBackendFactory"
|
||||
tools:node="remove" />
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,102 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"identity": "luma-ultra-base-v1",
|
||||
"width": 640,
|
||||
"height": 480,
|
||||
"fisheye": false,
|
||||
"error": 1.1209395390660726,
|
||||
"verified": false,
|
||||
"k1": {
|
||||
"rows": 3,
|
||||
"cols": 3,
|
||||
"data": [
|
||||
226.0499636040297,
|
||||
0,
|
||||
321.62348950766386,
|
||||
0,
|
||||
228.36987999988767,
|
||||
242.2674105020684,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
"d1": {
|
||||
"rows": 5,
|
||||
"cols": 1,
|
||||
"data": [
|
||||
-0.28622915867944043,
|
||||
0.1194613930041054,
|
||||
0.007984341063494766,
|
||||
-0.0008220922923559441,
|
||||
-0.025579805296512992
|
||||
]
|
||||
},
|
||||
"k2": {
|
||||
"rows": 3,
|
||||
"cols": 3,
|
||||
"data": [
|
||||
240.3338202496282,
|
||||
0,
|
||||
320.8790079027563,
|
||||
0,
|
||||
241.14047924325277,
|
||||
260.45566640784716,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
"d2": {
|
||||
"rows": 5,
|
||||
"cols": 1,
|
||||
"data": [
|
||||
-0.27073208928390136,
|
||||
0.08065508519805098,
|
||||
-0.001460778493930769,
|
||||
-0.0003694718992635806,
|
||||
-0.011452505620454932
|
||||
]
|
||||
},
|
||||
"r": {
|
||||
"rows": 3,
|
||||
"cols": 3,
|
||||
"data": [
|
||||
0.900501479835182,
|
||||
0.04473903344740378,
|
||||
-0.43254537762047496,
|
||||
-0.08255720039597626,
|
||||
0.9941919046988283,
|
||||
-0.06904176485356042,
|
||||
0.4269442510181227,
|
||||
0.0978819468416306,
|
||||
0.8989648107712929
|
||||
]
|
||||
},
|
||||
"t": {
|
||||
"rows": 3,
|
||||
"cols": 1,
|
||||
"data": [
|
||||
-0.14117171913441395,
|
||||
0.0014051874404824838,
|
||||
-0.007555740272006347
|
||||
]
|
||||
},
|
||||
"cameraToHead": [
|
||||
-0.05607918,
|
||||
0.023585627,
|
||||
-0.07998472,
|
||||
0.01024512,
|
||||
0.9945703,
|
||||
0.005859665,
|
||||
-0.10339546
|
||||
],
|
||||
"deviceModel": "VITURE Luma Ultra",
|
||||
"cameraPair": "left0-right0",
|
||||
"sourceValidation": {
|
||||
"exportDate": "2026-10-16",
|
||||
"sourceDeviceVerified": true,
|
||||
"capturedViews": 25,
|
||||
"scope": "Starting geometry from one calibrated Luma Ultra; other units have not been individually validated."
|
||||
}
|
||||
}
|
||||
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(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_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_HASH_INPUT "")
|
||||
foreach(f ${DDFG_CHAIN_SRC})
|
||||
set(WINFG_HASH_INPUT "")
|
||||
foreach(f ${WINFG_CHAIN_SRC})
|
||||
file(SHA256 "${CMAKE_CURRENT_SOURCE_DIR}/${f}" _h)
|
||||
string(APPEND DDFG_HASH_INPUT "${_h}")
|
||||
string(APPEND WINFG_HASH_INPUT "${_h}")
|
||||
endforeach()
|
||||
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})")
|
||||
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})")
|
||||
|
||||
add_subdirectory(waylandcomp)
|
||||
add_subdirectory(questxr)
|
||||
@@ -38,24 +38,3 @@ add_library(deviceinfo SHARED deviceinfo/vkinfo.c)
|
||||
target_link_libraries(deviceinfo dl)
|
||||
|
||||
add_library(termux SHARED termux/termux.c)
|
||||
|
||||
# The optional proprietary SDK is supplied locally; its headers/libraries are not vendored.
|
||||
if(DROIDDECK_VITURE_SDK_DIR)
|
||||
add_library(droiddeck_viture SHARED viture/sdk_bridge.cpp)
|
||||
target_compile_features(droiddeck_viture PRIVATE cxx_std_17)
|
||||
target_include_directories(droiddeck_viture PRIVATE "${DROIDDECK_VITURE_SDK_DIR}/include")
|
||||
target_link_directories(droiddeck_viture PRIVATE "${DROIDDECK_VITURE_SDK_DIR}/android/${ANDROID_ABI}")
|
||||
target_link_libraries(droiddeck_viture PRIVATE glasses log)
|
||||
endif()
|
||||
|
||||
if(DROIDDECK_UVC_DIR)
|
||||
add_library(rgb_usb SHARED IMPORTED)
|
||||
set_target_properties(rgb_usb PROPERTIES IMPORTED_LOCATION "${DROIDDECK_UVC_DIR}/${ANDROID_ABI}/libusb.so")
|
||||
add_library(rgb_uvc STATIC IMPORTED)
|
||||
set_target_properties(rgb_uvc PROPERTIES IMPORTED_LOCATION "${DROIDDECK_UVC_DIR}/${ANDROID_ABI}/libuvc.a")
|
||||
add_library(droiddeck_uvc SHARED viture/uvc_bridge.cpp)
|
||||
target_compile_features(droiddeck_uvc PRIVATE cxx_std_17)
|
||||
target_include_directories(droiddeck_uvc PRIVATE "${DROIDDECK_UVC_DIR}/include")
|
||||
target_link_libraries(droiddeck_uvc PRIVATE rgb_uvc rgb_usb log)
|
||||
target_link_options(droiddeck_uvc PRIVATE -Wl,-z,max-page-size=16384)
|
||||
endif()
|
||||
@@ -21,9 +21,6 @@
|
||||
|
||||
#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>
|
||||
@@ -78,16 +75,7 @@ static constexpr uint8_t GAMEPAD_AXIS_COUNT = 8;
|
||||
static constexpr uint8_t GAMEPAD_BUTTON_COUNT = 11;
|
||||
static constexpr uint32_t FAKE_INPUT_RING_MAGIC = 0x46494252;
|
||||
static constexpr uint32_t FAKE_INPUT_RING_VERSION = 2;
|
||||
// Rings hold the 64-bit input_event layout that the arm64 writers use; 32-bit guests convert on read.
|
||||
struct RingEvent {
|
||||
int64_t sec;
|
||||
int64_t usec;
|
||||
uint16_t type;
|
||||
uint16_t code;
|
||||
int32_t value;
|
||||
};
|
||||
static_assert(sizeof(RingEvent) == 24, "ring events must stay ABI-stable");
|
||||
static constexpr uint32_t FAKE_INPUT_EVENT_SIZE = sizeof(RingEvent);
|
||||
static constexpr uint32_t FAKE_INPUT_EVENT_SIZE = sizeof(struct input_event);
|
||||
static constexpr uint32_t FAKE_INPUT_RING_CAPACITY = 4096;
|
||||
static constexpr unsigned int FAKE_INPUT_MAJOR = 13;
|
||||
static constexpr unsigned int FAKE_INPUT_EVENT_MINOR_BASE = 64;
|
||||
@@ -383,17 +371,6 @@ send_vibration(int strong, int weak, uint16_t duration_ms, uint16_t slot) {
|
||||
syscall(SYS_close, sock);
|
||||
}
|
||||
|
||||
// Must match RumbleComponent.HOLD_MS: play until the slot's next effect or stop.
|
||||
static constexpr uint16_t RUMBLE_HOLD_MS = 0xffff;
|
||||
|
||||
__attribute__((visibility("hidden"))) static void send_ff_effect(const struct ff_effect &effect, uint16_t slot) {
|
||||
uint16_t duration = effect.replay.length ? effect.replay.length : RUMBLE_HOLD_MS;
|
||||
if (effect.type == FF_RUMBLE)
|
||||
send_vibration(effect.u.rumble.strong_magnitude, effect.u.rumble.weak_magnitude, duration, slot);
|
||||
else if (effect.type == FF_PERIODIC)
|
||||
send_vibration(effect.u.periodic.magnitude, effect.u.periodic.magnitude, duration, slot);
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static void
|
||||
check_ff_event(const struct input_event *ev, uint16_t slot) {
|
||||
if (ev->type != EV_FF)
|
||||
@@ -404,7 +381,15 @@ check_ff_event(const struct input_event *ev, uint16_t slot) {
|
||||
auto it = ff_effects().find(id);
|
||||
if (it == ff_effects().end())
|
||||
return;
|
||||
send_ff_effect(it->second, slot);
|
||||
|
||||
uint16_t duration = it->second.replay.length;
|
||||
if (it->second.type == FF_RUMBLE) {
|
||||
send_vibration(it->second.u.rumble.strong_magnitude,
|
||||
it->second.u.rumble.weak_magnitude, duration, slot);
|
||||
} else if (it->second.type == FF_PERIODIC) {
|
||||
send_vibration(it->second.u.periodic.magnitude,
|
||||
it->second.u.periodic.magnitude, duration, slot);
|
||||
}
|
||||
} else {
|
||||
send_vibration(0, 0, 0, slot);
|
||||
}
|
||||
@@ -940,13 +925,14 @@ __attribute__((visibility("hidden"))) static void flush_uinput(UinputWriter &w)
|
||||
return;
|
||||
}
|
||||
FakeInputRingHeader *ring = w.ring;
|
||||
auto *events = reinterpret_cast<RingEvent *>(reinterpret_cast<uint8_t *>(ring) +
|
||||
FAKE_INPUT_RING_HEADER_SIZE);
|
||||
auto *events = reinterpret_cast<struct input_event *>(reinterpret_cast<uint8_t *>(ring) +
|
||||
FAKE_INPUT_RING_HEADER_SIZE);
|
||||
uint64_t write_seq = __atomic_load_n(&ring->write_seq, __ATOMIC_RELAXED);
|
||||
struct timeval now = {};
|
||||
gettimeofday(&now, nullptr);
|
||||
for (const struct input_event &ev : w.pending) {
|
||||
events[write_seq % FAKE_INPUT_RING_CAPACITY] = {now.tv_sec, now.tv_usec, ev.type, ev.code, ev.value};
|
||||
for (struct input_event ev : w.pending) {
|
||||
ev.time = now;
|
||||
events[write_seq % FAKE_INPUT_RING_CAPACITY] = ev;
|
||||
write_seq++;
|
||||
if (ev.type == EV_KEY) {
|
||||
for (size_t i = 0; i < kSnapshotButtonCount; i++)
|
||||
@@ -3483,7 +3469,16 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) {
|
||||
if (effect->id == -1)
|
||||
effect->id = next_ff_id++;
|
||||
ff_effects()[effect->id] = *effect;
|
||||
send_ff_effect(*effect, static_cast<uint16_t>(event_number));
|
||||
|
||||
uint16_t duration = effect->replay.length;
|
||||
uint16_t slot = static_cast<uint16_t>(event_number);
|
||||
if (effect->type == FF_RUMBLE) {
|
||||
send_vibration(effect->u.rumble.strong_magnitude,
|
||||
effect->u.rumble.weak_magnitude, duration, slot);
|
||||
} else if (effect->type == FF_PERIODIC) {
|
||||
send_vibration(effect->u.periodic.magnitude, effect->u.periodic.magnitude,
|
||||
duration, slot);
|
||||
}
|
||||
return 0;
|
||||
} else if (type == 0x45 && number == 0x81) {
|
||||
int id = (intptr_t)argp;
|
||||
@@ -3522,12 +3517,6 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) {
|
||||
} else if (type == 0x45 && number == 0x90) {
|
||||
Logger::log("Hooking ioctl EVIOCGRAB for event %s\n", event);
|
||||
return 0;
|
||||
} else if (type == 0x6A && !strncmp(event, "event", 5)) {
|
||||
// Older SDL2 builds treat any JSIOCGNAME answer as a joydev node and skip EVIOCGID, then drop the pad as 0000:0000.
|
||||
Logger::log("Rejecting joydev ioctl %d for event %s\n", number, event);
|
||||
guard.unlock();
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
} else if (type == 0x6A && number == 0x1) {
|
||||
Logger::log("Hooking ioctl JSIOCGVERSION for event %s\n", event);
|
||||
int version = JS_VERSION;
|
||||
@@ -3580,21 +3569,7 @@ EXPORT int close(int fd) {
|
||||
static constexpr int kMaxRingWaitMs = 2;
|
||||
static constexpr int kLowLatencyRingWaitUs = 250;
|
||||
|
||||
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) {
|
||||
EXPORT ssize_t read(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);
|
||||
@@ -3604,7 +3579,7 @@ static ssize_t read_input(int fd, void *buf, size_t count) {
|
||||
}
|
||||
auto handle = controller->second;
|
||||
FakeController &fake = *handle;
|
||||
if (count < sizeof(struct input_event)) {
|
||||
if (count < FAKE_INPUT_EVENT_SIZE) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
@@ -3622,7 +3597,7 @@ static ssize_t read_input(int fd, void *buf, size_t count) {
|
||||
return -1;
|
||||
}
|
||||
for (int attempt = 0; attempt < 8; ++attempt) {
|
||||
size_t requested = count / sizeof(struct input_event);
|
||||
size_t requested = count / FAKE_INPUT_EVENT_SIZE;
|
||||
if (fake.keyframe_remaining == 0) {
|
||||
SnapshotState snap;
|
||||
if (!read_snapshot(fake.ring, snap)) break;
|
||||
@@ -3639,44 +3614,38 @@ static ssize_t read_input(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 = 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;
|
||||
struct input_event ev;
|
||||
memcpy(&ev, ring_events + index * FAKE_INPUT_EVENT_SIZE, FAKE_INPUT_EVENT_SIZE);
|
||||
if (ev.type == EV_ABS) ev.code = presented_abs_code(fake, ev.code);
|
||||
memcpy(static_cast<uint8_t *>(buf) + i * FAKE_INPUT_EVENT_SIZE, &ev,
|
||||
FAKE_INPUT_EVENT_SIZE);
|
||||
}
|
||||
__atomic_thread_fence(__ATOMIC_ACQUIRE);
|
||||
if (snap.sequence != __atomic_load_n(&fake.ring->snapshot_seq, __ATOMIC_RELAXED))
|
||||
continue;
|
||||
ssize_t copied = copy_input_events(buf, input_events.data(), events);
|
||||
if (copied > 0) fake.read_seq += copied / sizeof(struct input_event);
|
||||
return copied;
|
||||
fake.read_seq += events;
|
||||
return static_cast<ssize_t>(events * FAKE_INPUT_EVENT_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
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 + i;
|
||||
struct input_event &ev = input_events[i];
|
||||
size_t index = kNeutralEventCount - fake.keyframe_remaining;
|
||||
struct input_event ev = {};
|
||||
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 * FAKE_INPUT_EVENT_SIZE,
|
||||
&ev, FAKE_INPUT_EVENT_SIZE);
|
||||
--fake.keyframe_remaining;
|
||||
}
|
||||
ssize_t copied = copy_input_events(buf, input_events, events);
|
||||
if (copied > 0) fake.keyframe_remaining -= copied / sizeof(struct input_event);
|
||||
return copied;
|
||||
return static_cast<ssize_t>(events * FAKE_INPUT_EVENT_SIZE);
|
||||
}
|
||||
if (nonblock) {
|
||||
errno = EAGAIN;
|
||||
@@ -3692,52 +3661,6 @@ static ssize_t read_input(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"));
|
||||
|
||||
@@ -4168,92 +4091,4 @@ 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 ddfg { class Engine; }
|
||||
namespace winfg { 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 setDdFgTuning(int model, int perfPreset);
|
||||
void setWinFgTuning(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<ddfg::Engine> ddfgEngine_;
|
||||
bool ddfgEngineTried_ = false;
|
||||
std::unique_ptr<winfg::Engine> winfgEngine_;
|
||||
bool winfgEngineTried_ = false;
|
||||
std::atomic<int> fgEngineKind_{0};
|
||||
std::atomic<int> fgModel_{4};
|
||||
std::atomic<int> fgPerfPreset_{1};
|
||||
bool ensureDdFgEngine();
|
||||
bool ensureWinFgEngine();
|
||||
bool fgCapsOk() const;
|
||||
|
||||
uint32_t syncSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; }
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
#include "vk_dispatch.hpp"
|
||||
struct VkTable;
|
||||
bool ddfgVkdInit(const VkTable& table, ddfg::DeviceDispatch& dd, ddfg::InstanceDispatch& id);
|
||||
@@ -1,12 +0,0 @@
|
||||
#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)
|
||||
File renamed without changes.
File renamed without changes.
+36
-36
@@ -6,7 +6,7 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace ddfg {
|
||||
namespace winfg {
|
||||
|
||||
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("DD_FG_CONF")) return p;
|
||||
if (const char* p = std::getenv("WIN_FG_CONF")) return p;
|
||||
const char* home = std::getenv("HOME");
|
||||
return home ? std::string(home) + "/.config/dd-fg/conf.toml" : std::string();
|
||||
return home ? std::string(home) + "/.config/win-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/ddfg-capture";
|
||||
return std::string("/tmp/ddfg-capture");
|
||||
if (const char* home = std::getenv("HOME")) return std::string(home) + "/.cache/winfg-capture";
|
||||
return std::string("/tmp/winfg-capture");
|
||||
}
|
||||
|
||||
static inline Config load_config() {
|
||||
Config c;
|
||||
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")) {
|
||||
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")) {
|
||||
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/dd-fg/conf.toml");
|
||||
if (const char* p = std::getenv("DD_FG_CONF")) apply_toml(c, p);
|
||||
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);
|
||||
c.sanitize();
|
||||
return c;
|
||||
}
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
namespace ddfg { namespace embedded {
|
||||
namespace winfg { 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 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;
|
||||
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;
|
||||
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, DDFG_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, WFG_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 DDFG_SYNTH: return { DDFG_SYNTH_SPV, DDFG_SYNTH_LEN };
|
||||
case WFG_SYNTH: return { WFG_SYNTH_SPV, WFG_SYNTH_LEN };
|
||||
default: return { nullptr, 0 };
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -4,9 +4,9 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
namespace ddfg {
|
||||
namespace winfg {
|
||||
|
||||
#define VKOK(x) do { if ((x) != VK_SUCCESS) { DDFG_LOGE("vk fail: %s", #x); return false; } } while (0)
|
||||
#define VKOK(x) do { if ((x) != VK_SUCCESS) { WFG_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::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)",
|
||||
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)",
|
||||
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;
|
||||
DDFG_LOGI("framegen resized to %ux%u (%d pyramid levels, kFlowFinest=%d perf_preset=%d)",
|
||||
WFG_LOGI("framegen resized to %ux%u (%d pyramid levels, kFlowFinest=%d perf_preset=%d)",
|
||||
extent.width, extent.height, kLevels, flowFinest_, cfg_.perfPreset);
|
||||
return true;
|
||||
}
|
||||
+2
-4
@@ -3,7 +3,7 @@
|
||||
#include "config.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace ddfg {
|
||||
namespace winfg {
|
||||
|
||||
class FrameGen {
|
||||
public:
|
||||
@@ -12,8 +12,7 @@ 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,
|
||||
uint32_t synthSlot = 0, bool reuseFlow = false);
|
||||
VkImageView outView, float alpha, uint32_t gmSlot = 0);
|
||||
void destroy();
|
||||
bool valid() const { return device_ != VK_NULL_HANDLE && ready_; }
|
||||
const Config& config() const { return cfg_; }
|
||||
@@ -26,7 +25,6 @@ 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 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)
|
||||
+144
-161
@@ -1,34 +1,31 @@
|
||||
|
||||
namespace {
|
||||
struct ScratchState {
|
||||
VkBuffer synthUbo[ddfg::FrameGen::kSynthSlots]{};
|
||||
VkDeviceMemory synthUboMem[ddfg::FrameGen::kSynthSlots]{};
|
||||
void* synthUboPtr[ddfg::FrameGen::kSynthSlots]{};
|
||||
VkBuffer synthUbo = VK_NULL_HANDLE; VkDeviceMemory synthUboMem = VK_NULL_HANDLE; void* synthUboPtr = nullptr;
|
||||
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[ddfg::FrameGen::kGmSlots]{};
|
||||
VkDeviceMemory gmUboMem[ddfg::FrameGen::kGmSlots]{};
|
||||
void* gmUboPtr[ddfg::FrameGen::kGmSlots]{};
|
||||
VkBuffer gmUbo[winfg::FrameGen::kGmSlots]{};
|
||||
VkDeviceMemory gmUboMem[winfg::FrameGen::kGmSlots]{};
|
||||
void* gmUboPtr[winfg::FrameGen::kGmSlots]{};
|
||||
VkDescriptorSet sLumaCurr = VK_NULL_HANDLE, sLumaPrev = VK_NULL_HANDLE;
|
||||
std::vector<VkDescriptorSet> sDownA, sDownB, sFlow;
|
||||
VkDescriptorSet sExpand[ddfg::FrameGen::kGmSlots]{};
|
||||
VkDescriptorSet sExpand[winfg::FrameGen::kGmSlots]{};
|
||||
|
||||
|
||||
|
||||
VkDescriptorSet sFlowReg0 = VK_NULL_HANDLE, sFlowRegAB = VK_NULL_HANDLE, sFlowRegBA = VK_NULL_HANDLE;
|
||||
VkDescriptorSet sSynth[ddfg::FrameGen::kSynthSlots]{};
|
||||
VkDescriptorSet sGmReduce[ddfg::FrameGen::kGmSlots]{};
|
||||
std::vector<VkDescriptorSet> sGmPrewarp[ddfg::FrameGen::kGmSlots];
|
||||
VkDescriptorSet sSynth = VK_NULL_HANDLE;
|
||||
VkDescriptorSet sGmReduce[winfg::FrameGen::kGmSlots]{};
|
||||
std::vector<VkDescriptorSet> sGmPrewarp[winfg::FrameGen::kGmSlots];
|
||||
VkExtent2D builtFor{0,0};
|
||||
bool built = false;
|
||||
bool flowInited = false;
|
||||
bool flowReady = false;
|
||||
};
|
||||
ScratchState g_scratch;
|
||||
}
|
||||
|
||||
static bool makeUbo(const ddfg::DeviceDispatch* dd, VkDevice dev,
|
||||
static bool makeUbo(const winfg::DeviceDispatch* dd, VkDevice dev,
|
||||
const VkPhysicalDeviceMemoryProperties& mp,
|
||||
VkDeviceSize size, VkBuffer& buf, VkDeviceMemory& mem, void*& ptr) {
|
||||
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
@@ -44,21 +41,21 @@ static bool makeUbo(const ddfg::DeviceDispatch* dd, VkDevice dev,
|
||||
return dd->MapMemory(dev, mem, 0, size, 0, &ptr) == VK_SUCCESS;
|
||||
}
|
||||
|
||||
static VkDescriptorSet allocSet(const ddfg::DeviceDispatch* dd, VkDevice dev,
|
||||
static VkDescriptorSet allocSet(const winfg::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 ddfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
|
||||
static void wImg(const winfg::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 ddfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
|
||||
static void wUbo(const winfg::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};
|
||||
@@ -66,7 +63,7 @@ static void wUbo(const ddfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet s
|
||||
w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &bi;
|
||||
dd->UpdateDescriptorSets(dev, 1, &w, 0, nullptr);
|
||||
}
|
||||
static void wSsbo(const ddfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet set, uint32_t binding,
|
||||
static void wSsbo(const winfg::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};
|
||||
@@ -120,7 +117,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 ddfg {
|
||||
namespace winfg {
|
||||
|
||||
static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
const VkPhysicalDeviceMemoryProperties& mp, VkDescriptorPool pool,
|
||||
@@ -143,7 +140,7 @@ static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorP
|
||||
b = VK_NULL_HANDLE; m = VK_NULL_HANDLE; p = nullptr;
|
||||
};
|
||||
auto& S = g_scratch;
|
||||
for (int s = 0; s < FrameGen::kSynthSlots; ++s) freeUbo(S.synthUbo[s], S.synthUboMem[s], S.synthUboPtr[s]);
|
||||
freeUbo(S.synthUbo, S.synthUboMem, S.synthUboPtr);
|
||||
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]);
|
||||
@@ -157,7 +154,7 @@ void FrameGen::configure(const Config& c) {
|
||||
int want = flowFinestForPreset(cfg_.perfPreset);
|
||||
const int maxFinest = kLevels - 2;
|
||||
if (want > maxFinest) {
|
||||
DDFG_LOGE("perf-preset=%d wants kFlowFinest=%d but only %d pyramid levels exist "
|
||||
WFG_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;
|
||||
}
|
||||
@@ -166,7 +163,7 @@ void FrameGen::configure(const Config& c) {
|
||||
const int prevFinest = flowFinest_;
|
||||
flowFinest_ = want;
|
||||
const bool live = (flowFinest_ != prevFinest) && ready_;
|
||||
DDFG_LOGI("perf-preset=%d -> kFlowFinest=%d (was %d) live-rebuild=%d ready=%d",
|
||||
WFG_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;
|
||||
@@ -177,12 +174,12 @@ void FrameGen::configure(const Config& c) {
|
||||
dd_->DeviceWaitIdle(device_);
|
||||
destroyScratch(dd_, device_, descPool_);
|
||||
if (!onResize(extent_, VK_FORMAT_UNDEFINED, true))
|
||||
DDFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u - FG disabled until next resize",
|
||||
WFG_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;
|
||||
DDFG_LOGI("perf-preset live rebuild DONE (kFlowFinest=%d, flow predictor reset)", flowFinest_);
|
||||
WFG_LOGI("perf-preset live rebuild DONE (kFlowFinest=%d, flow predictor reset)", flowFinest_);
|
||||
}
|
||||
|
||||
bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) {
|
||||
@@ -252,10 +249,9 @@ bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) {
|
||||
}
|
||||
|
||||
void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView currView,
|
||||
VkImageView outView, float alpha, uint32_t gmSlot, uint32_t synthSlot, bool reuseFlow) {
|
||||
VkImageView outView, float alpha, uint32_t gmSlot) {
|
||||
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_;
|
||||
@@ -267,42 +263,35 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur
|
||||
pFlowReg_, flowRegA_, flowRegB_, gmSsbo_, kLevels, cfg_);
|
||||
}
|
||||
|
||||
// Every generation of one real frame is recorded into the same command buffer, so the flow
|
||||
// between prev and curr is computed once and each generation gets its own synth UBO and set.
|
||||
if (reuseFlow && !g_scratch.flowReady) return;
|
||||
if (!reuseFlow) {
|
||||
bool gmEngaged = updateGlobalMotion(gmSlot, extent_);
|
||||
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));
|
||||
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];
|
||||
}
|
||||
{
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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[synthSlot]) std::memcpy(g_scratch.synthUboPtr[synthSlot], &su, sizeof(su));
|
||||
if (g_scratch.synthUboPtr) std::memcpy(g_scratch.synthUboPtr, &su, sizeof(su));
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
auto disp = [&](const Pipe& p, VkDescriptorSet set, VkExtent2D e) {
|
||||
dd_->CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, p.pipeline);
|
||||
@@ -311,109 +300,106 @@ 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); };
|
||||
|
||||
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 (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) {
|
||||
disp(pGmPrewarp_, g_scratch.sGmPrewarp[gmSlot][l], levelExtent(extent_, l));
|
||||
bar(pyrAs_[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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
disp(expandP, g_scratch.sExpand[gmSlot], levelExtent(extent_, 0));
|
||||
bar(flowExpA_); bar(flowExpB_);
|
||||
g_scratch.flowReady = true;
|
||||
}
|
||||
disp(pSynth_, synthSet, levelExtent(extent_, 0));
|
||||
|
||||
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) {
|
||||
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));
|
||||
}
|
||||
|
||||
static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
@@ -432,8 +418,7 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev,
|
||||
S = ScratchState{};
|
||||
const auto GEN = VK_IMAGE_LAYOUT_GENERAL;
|
||||
|
||||
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(SynthUBO), S.synthUbo, S.synthUboMem, S.synthUboPtr);
|
||||
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);
|
||||
@@ -524,12 +509,10 @@ 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);
|
||||
}
|
||||
|
||||
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.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);
|
||||
|
||||
S.builtFor = ext; S.built = true;
|
||||
return true;
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
#include <vulkan/vk_layer.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace ddfg {
|
||||
namespace winfg {
|
||||
|
||||
static inline void* dispatch_key(void* handle) { return *reinterpret_cast<void**>(handle); }
|
||||
|
||||
+10
-13
@@ -1,10 +1,10 @@
|
||||
#include "ddfg_engine.h"
|
||||
#include "ddfg_vkd.h"
|
||||
#include "winfg_engine.h"
|
||||
#include "winfg_vkd.h"
|
||||
#include "log.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace ddfg {
|
||||
namespace winfg {
|
||||
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 (!ddfgVkdInit(table, dd_, id_)) {
|
||||
DDFG_LOGE("native: dispatch incomplete; frame generation unavailable");
|
||||
if (!winfgVkdInit(table, dd_, id_)) {
|
||||
WFG_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_)) {
|
||||
DDFG_LOGE("native: FrameGen::init failed");
|
||||
WFG_LOGE("native: FrameGen::init failed");
|
||||
unavailable_ = true;
|
||||
return false;
|
||||
}
|
||||
DDFG_LOGI("native: chain ready (10 pipelines, embedded shaders)");
|
||||
WFG_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)) {
|
||||
DDFG_LOGE("native: onResize %ux%u failed; frame generation unavailable", w, h);
|
||||
WFG_LOGE("native: onResize %ux%u failed; frame generation unavailable", w, h);
|
||||
unavailable_ = true;
|
||||
return false;
|
||||
}
|
||||
@@ -71,13 +71,11 @@ bool Engine::prepare(uint32_t w, uint32_t h, VkFormat fmt) {
|
||||
}
|
||||
builtW_ = w; builtH_ = h; builtFmt_ = fmt;
|
||||
history_ = 0; curr_ = 0; frames_ = 0;
|
||||
DDFG_LOGI("native: built %ux%u fmt=%d model=%d preset=%d flow=%.2f",
|
||||
WFG_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);
|
||||
@@ -133,8 +131,7 @@ 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_,
|
||||
generation, generation > 0);
|
||||
fg_.record(cmd, prev.view, curr.view, targetView, alphaFor(generation, count), gmSlot_);
|
||||
if (generation + 1 == count) gmSlot_ = (gmSlot_ + 1) % (uint32_t)FrameGen::kGmSlots;
|
||||
|
||||
(void)w; (void)h;
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
struct VkTable;
|
||||
|
||||
namespace ddfg {
|
||||
namespace winfg {
|
||||
|
||||
constexpr uint32_t kMaxGenerations = 3;
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
#include "ddfg_vkd.h"
|
||||
#include "winfg_vkd.h"
|
||||
#include "../VulkanRendererContext.h"
|
||||
#include <android/log.h>
|
||||
|
||||
bool ddfgVkdInit(const VkTable& table, ddfg::DeviceDispatch& dd, ddfg::InstanceDispatch& id) {
|
||||
bool winfgVkdInit(const VkTable& table, winfg::DeviceDispatch& dd, winfg::InstanceDispatch& id) {
|
||||
bool ok = true;
|
||||
#define COPY(dst, fn) \
|
||||
do { \
|
||||
dst.fn = table.fn; \
|
||||
if (!dst.fn) { \
|
||||
__android_log_print(ANDROID_LOG_ERROR, "DdFgVkd", \
|
||||
__android_log_print(ANDROID_LOG_ERROR, "WinFgVkd", \
|
||||
"entry point vk" #fn " did not resolve"); \
|
||||
ok = false; \
|
||||
} \
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include "vk_dispatch.hpp"
|
||||
struct VkTable;
|
||||
bool winfgVkdInit(const VkTable& table, winfg::DeviceDispatch& dd, winfg::InstanceDispatch& id);
|
||||
@@ -1,8 +1,5 @@
|
||||
#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) {
|
||||
@@ -10,10 +7,3 @@ 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;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Our controller uses the standard display-mode vocabulary, even for Beast.
|
||||
// Native mode reuses some numbers with different meanings; never pass them through.
|
||||
namespace droiddeck::viture {
|
||||
constexpr int to_native_mode(int standard) {
|
||||
switch (standard) {
|
||||
case 0x31: return 0x31; // 1080p 60
|
||||
case 0x33: return 0x32; // 1080p 90
|
||||
case 0x34: return 0x33; // 1080p 120
|
||||
case 0x32: return 0x37; // 1080p SBS 60
|
||||
case 0x35: return 0x38; // 1080p SBS 90
|
||||
case 0x41: return 0x34; // 1200p 60
|
||||
case 0x43: return 0x35; // 1200p 90
|
||||
case 0x44: return 0x36; // 1200p 120
|
||||
case 0x42: return 0x3a; // 1200p SBS 60
|
||||
case 0x45: return 0x3b; // 1200p SBS 90
|
||||
default: return -4;
|
||||
}
|
||||
}
|
||||
constexpr int from_native_mode(int native) {
|
||||
switch (native) {
|
||||
case 0x31: return 0x31;
|
||||
case 0x32: return 0x33;
|
||||
case 0x33: return 0x34;
|
||||
case 0x37: return 0x32;
|
||||
case 0x38: return 0x35;
|
||||
case 0x34: return 0x41;
|
||||
case 0x35: return 0x43;
|
||||
case 0x36: return 0x44;
|
||||
case 0x3a: return 0x42;
|
||||
case 0x3b: return 0x45;
|
||||
default: return -4; // Ultrawide is not stereo.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
inline std::size_t mjpeg_payload_size(const uint8_t* bytes, std::size_t size) {
|
||||
if (!bytes || size < 4 || size > 4*1024*1024 || bytes[0] != 0xff || bytes[1] != 0xd8) return 0;
|
||||
std::size_t end = size;
|
||||
// Luma Ultra UVC frames can pad the JPEG to an eight-byte boundary with zeros.
|
||||
while (end > 4 && size-end < 7 && bytes[end-1] == 0) --end;
|
||||
return bytes[end-2] == 0xff && bytes[end-1] == 0xd9 ? end : 0;
|
||||
}
|
||||
@@ -1,556 +0,0 @@
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cerrno>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/usbdevice_fs.h>
|
||||
#include <time.h>
|
||||
#include "viture_glasses_provider.h"
|
||||
#include "viture_device.h"
|
||||
#include "viture_device_carina.h"
|
||||
#include "viture_camera_provider.h"
|
||||
#include "viture_protocol_public.h"
|
||||
#include "viture_result.h"
|
||||
#include "viture_version.h"
|
||||
#include "display_modes.h"
|
||||
#include "stereo_frame_gate.h"
|
||||
|
||||
namespace {
|
||||
// SDK callbacks have no user-data parameter. Only one provider may exist at a time.
|
||||
std::mutex api_mutex;
|
||||
std::mutex pose_mutex;
|
||||
std::array<float, 4> latest_pose{};
|
||||
int64_t latest_time = 0;
|
||||
bool accepting_pose = false;
|
||||
bool accepting_state = false;
|
||||
std::array<int, 6> state_values{};
|
||||
unsigned state_pending = 0;
|
||||
|
||||
void on_state(int id, int value) {
|
||||
std::lock_guard<std::mutex> lock(pose_mutex);
|
||||
if (!accepting_state || id < 0 || id >= int(state_values.size())) return;
|
||||
state_values[id] = value;
|
||||
state_pending |= 1u << id;
|
||||
}
|
||||
|
||||
void clear_state(bool accept) {
|
||||
std::lock_guard<std::mutex> lock(pose_mutex);
|
||||
accepting_state = accept;
|
||||
state_pending = 0;
|
||||
}
|
||||
|
||||
int64_t now_ns() {
|
||||
timespec ts{};
|
||||
clock_gettime(CLOCK_BOOTTIME, &ts);
|
||||
return int64_t(ts.tv_sec) * 1000000000LL + ts.tv_nsec;
|
||||
}
|
||||
|
||||
void on_pose(float* data, uint64_t) {
|
||||
if (!data) return;
|
||||
std::lock_guard<std::mutex> lock(pose_mutex);
|
||||
if (!accepting_pose) return;
|
||||
std::copy_n(data + 3, 4, latest_pose.begin());
|
||||
// SDK device ticks are not Android's elapsed-realtime clock.
|
||||
latest_time = now_ns();
|
||||
}
|
||||
|
||||
void clear_pose(bool accept) {
|
||||
std::lock_guard<std::mutex> lock(pose_mutex);
|
||||
accepting_pose = accept;
|
||||
latest_time = 0;
|
||||
}
|
||||
|
||||
int64_t monotonic_ns() {
|
||||
timespec ts{}; clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return int64_t(ts.tv_sec) * 1000000000LL + ts.tv_nsec;
|
||||
}
|
||||
struct CameraPose { double time; std::array<float, 7> pose; };
|
||||
std::mutex camera_mutex;
|
||||
std::deque<CameraPose> camera_poses;
|
||||
std::vector<uint8_t> camera_left, camera_right;
|
||||
bool camera_accepting = false, camera_wanted = false;
|
||||
int camera_width = 0, camera_height = 0;
|
||||
uint64_t camera_sequence = 0, camera_consumed = 0;
|
||||
StereoFrameGate camera_gate;
|
||||
uint64_t camera_input_sequence = 0;
|
||||
int64_t camera_receipt = 0;
|
||||
int64_t camera_pose_receipt = 0;
|
||||
double camera_source = 0;
|
||||
const CameraPose *matched_camera_pose(double source) {
|
||||
const CameraPose *best = nullptr;
|
||||
double distance = 0.005;
|
||||
for (const auto &p : camera_poses) if (std::abs(p.time-source) < distance) {
|
||||
best = &p; distance = std::abs(p.time-source);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
void on_carina_pose(float *pose, double stamp) {
|
||||
if (!pose || !std::isfinite(stamp)) return;
|
||||
std::lock_guard<std::mutex> lock(camera_mutex);
|
||||
if (!camera_accepting) return;
|
||||
camera_pose_receipt = monotonic_ns();
|
||||
CameraPose p{stamp, {}}; std::copy_n(pose, 7, p.pose.begin());
|
||||
camera_poses.push_back(p);
|
||||
while (camera_poses.size() > 64) camera_poses.pop_front();
|
||||
}
|
||||
void on_camera(char *left, char *right, char *, char *, double stamp, int width, int height) {
|
||||
if (!left || !right || width <= 0 || height <= 0 || width > 2048 || height > 2048 || !std::isfinite(stamp)) return;
|
||||
const auto received = monotonic_ns();
|
||||
std::lock_guard<std::mutex> lock(camera_mutex);
|
||||
if (!camera_accepting || !camera_wanted) return;
|
||||
if (!camera_gate.Accept(stamp)) return;
|
||||
const size_t size = size_t(width) * height;
|
||||
camera_left.resize(size); camera_right.resize(size);
|
||||
memcpy(camera_left.data(), left, size); memcpy(camera_right.data(), right, size);
|
||||
camera_width = width; camera_height = height; camera_source = stamp; camera_receipt = received;
|
||||
camera_input_sequence = camera_gate.received;
|
||||
++camera_sequence;
|
||||
}
|
||||
void clear_camera(bool enabled) {
|
||||
std::lock_guard<std::mutex> lock(camera_mutex);
|
||||
camera_accepting = enabled; camera_wanted = false;
|
||||
camera_poses.clear(); camera_left.clear(); camera_right.clear();
|
||||
camera_sequence = camera_consumed = 0;
|
||||
camera_gate = {}; camera_input_sequence = 0;
|
||||
camera_pose_receipt = 0;
|
||||
}
|
||||
|
||||
struct Session {
|
||||
XRDeviceProviderHandle device = nullptr;
|
||||
int pid = 0;
|
||||
int type = -1;
|
||||
bool initialized = false;
|
||||
bool started = false;
|
||||
bool tracking = false;
|
||||
bool native_dof = false;
|
||||
bool switched_native = false;
|
||||
int original_bypass_display = -1;
|
||||
int saved_dof = -1;
|
||||
int64_t consumed_time = 0;
|
||||
jlong token = 0;
|
||||
int spatial_status = 1;
|
||||
};
|
||||
std::unique_ptr<Session> session;
|
||||
jlong next_token = 0;
|
||||
|
||||
void report(const char* step, int rc) {
|
||||
if (rc < 0) __android_log_print(ANDROID_LOG_WARN, "VitureSdk", "%s: %d", step, rc);
|
||||
}
|
||||
|
||||
int suspend_native_dof(Session& s) {
|
||||
if (!s.native_dof) return 0;
|
||||
int mode = xr_device_provider_native_get_mode(s.device);
|
||||
if (mode < 0) return mode;
|
||||
if (mode != 1) return 0;
|
||||
if (s.saved_dof < 0) {
|
||||
int dof = xr_device_provider_native_get_dof(s.device);
|
||||
if (dof < 0) return dof;
|
||||
s.saved_dof = dof;
|
||||
}
|
||||
return xr_device_provider_native_set_dof(s.device, VITURE_NATIVE_DOF_0);
|
||||
}
|
||||
|
||||
void restore_native_dof(Session& s) {
|
||||
if (s.saved_dof >= 0 && xr_device_provider_native_get_mode(s.device) == 1) {
|
||||
report("restore native DOF", xr_device_provider_native_set_dof(s.device, s.saved_dof));
|
||||
}
|
||||
s.saved_dof = -1;
|
||||
}
|
||||
|
||||
int tracking(Session& s, bool enabled) {
|
||||
if (s.tracking == enabled) return 0;
|
||||
if (!enabled) {
|
||||
clear_pose(false);
|
||||
int rc = s.type == XR_DEVICE_TYPE_VITURE_CARINA ? 0 :
|
||||
xr_device_provider_close_imu(s.device, VITURE_IMU_MODE_POSE);
|
||||
s.tracking = false;
|
||||
restore_native_dof(s);
|
||||
return rc;
|
||||
}
|
||||
int rc = suspend_native_dof(s);
|
||||
if (rc < 0) return rc;
|
||||
clear_pose(true);
|
||||
s.consumed_time = 0;
|
||||
if (s.type != XR_DEVICE_TYPE_VITURE_CARINA) {
|
||||
int frequency = VITURE_IMU_FREQUENCY_MEDIUM_HIGH;
|
||||
while (frequency >= VITURE_IMU_FREQUENCY_LOW &&
|
||||
!xr_device_provider_is_product_support_imu_frequency(s.pid, VITURE_IMU_MODE_POSE, frequency)) --frequency;
|
||||
rc = frequency < 0 ? VITURE_GLASSES_ERROR_NOT_SUPPORTED :
|
||||
xr_device_provider_open_imu(s.device, VITURE_IMU_MODE_POSE, frequency);
|
||||
}
|
||||
if (rc == 0) s.tracking = true;
|
||||
else { clear_pose(false); restore_native_dof(s); }
|
||||
return rc;
|
||||
}
|
||||
|
||||
void destroy(Session& s) {
|
||||
clear_camera(false);
|
||||
clear_pose(false);
|
||||
clear_state(false);
|
||||
if (s.device) {
|
||||
if (s.tracking) report("close tracking", tracking(s, false));
|
||||
if (s.switched_native && s.original_bypass_display >= 0) {
|
||||
int rc = xr_device_provider_native_set_mode(s.device, 0);
|
||||
if (rc == 0) rc = xr_device_provider_set_display_mode(s.device, s.original_bypass_display);
|
||||
report("restore bypass mode", rc);
|
||||
}
|
||||
if (s.started) report("stop", xr_device_provider_stop(s.device));
|
||||
if (s.initialized) report("shutdown", xr_device_provider_shutdown(s.device));
|
||||
xr_device_provider_destroy(s.device);
|
||||
s.device = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int get_display(Session& s) {
|
||||
if (s.native_dof) {
|
||||
const int mode = xr_device_provider_native_get_mode(s.device);
|
||||
if (mode < 0) return mode;
|
||||
if (mode == 1) {
|
||||
const int value = xr_device_provider_native_get_display_mode(s.device);
|
||||
return value < 0 ? value : droiddeck::viture::from_native_mode(value);
|
||||
}
|
||||
}
|
||||
return xr_device_provider_get_display_mode(s.device);
|
||||
}
|
||||
|
||||
int set_display(Session& s, int standard) {
|
||||
if (!s.native_dof) return xr_device_provider_set_display_mode(s.device, standard);
|
||||
int mode = xr_device_provider_native_get_mode(s.device);
|
||||
if (mode < 0) return mode;
|
||||
if (mode == 0) {
|
||||
int rc = xr_device_provider_set_display_mode(s.device, standard);
|
||||
if (rc != VITURE_GLASSES_ERROR_NOT_SUPPORTED) return rc;
|
||||
// Older Beast firmware exposes fewer bypass modes. Fall back to its
|
||||
// separate native table only when the SDK explicitly rejects the mode.
|
||||
}
|
||||
const int native = droiddeck::viture::to_native_mode(standard);
|
||||
if (native < 0) return native;
|
||||
if (mode == 0) {
|
||||
if (!s.switched_native) {
|
||||
s.original_bypass_display = xr_device_provider_get_display_mode(s.device);
|
||||
if (s.original_bypass_display < 0) return s.original_bypass_display;
|
||||
}
|
||||
int rc = xr_device_provider_native_set_mode(s.device, 1);
|
||||
if (rc < 0) return rc;
|
||||
s.switched_native = true;
|
||||
}
|
||||
int rc = xr_device_provider_native_set_display_mode(s.device, native);
|
||||
if (rc == 0 && s.tracking) rc = suspend_native_dof(s);
|
||||
if (rc < 0 && mode == 0) {
|
||||
restore_native_dof(s);
|
||||
int rollback = xr_device_provider_native_set_mode(s.device, 0);
|
||||
if (rollback == 0) rollback = xr_device_provider_set_display_mode(s.device, s.original_bypass_display);
|
||||
report("rollback display mode", rollback);
|
||||
if (rollback == 0) s.switched_native = false;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
#define JNI_METHOD(name) Java_com_droiddeck_launcher_glasses_VitureSdk_##name
|
||||
extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nativeAvailable)(JNIEnv*, jobject) {
|
||||
__android_log_print(ANDROID_LOG_INFO, "VitureSdk", "SDK %s", GetVersionString());
|
||||
return JNI_TRUE;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL JNI_METHOD(nativeMarketName)(JNIEnv* env, jobject, jint pid) {
|
||||
std::lock_guard<std::mutex> lock(api_mutex);
|
||||
if (!xr_device_provider_is_product_id_valid(pid)) return nullptr;
|
||||
char name[64]{};
|
||||
int length = sizeof(name) - 1;
|
||||
if (xr_device_provider_get_market_name(pid, name, &length) != 0) return nullptr;
|
||||
name[sizeof(name) - 1] = 0;
|
||||
return env->NewStringUTF(name);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jlong JNICALL JNI_METHOD(nativeOpen)(JNIEnv* env, jobject, jint pid, jint fd, jstring cache, jboolean spatial) {
|
||||
std::lock_guard<std::mutex> lock(api_mutex);
|
||||
if (session || fd < 0 || !cache || !xr_device_provider_is_product_id_valid(pid)) return 0;
|
||||
auto s = std::make_unique<Session>();
|
||||
s->pid = pid;
|
||||
s->device = xr_device_provider_create(pid, fd);
|
||||
if (!s->device) return 0;
|
||||
s->type = xr_device_provider_get_device_type(s->device);
|
||||
s->native_dof = xr_device_provider_is_product_support_native_dof(pid) == 1;
|
||||
int rc = s->type < 0 ? s->type : 0;
|
||||
if (rc == 0) {
|
||||
clear_state(true);
|
||||
rc = xr_device_provider_register_state_callback(s->device, on_state);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = s->type == XR_DEVICE_TYPE_VITURE_CARINA ?
|
||||
xr_device_provider_set_dof_type_carina(s->device, spatial ? 1 : 0) :
|
||||
xr_device_provider_register_imu_pose_callback(s->device, on_pose);
|
||||
}
|
||||
if (rc == 0) {
|
||||
const char* directory = env->GetStringUTFChars(cache, nullptr);
|
||||
if (!directory) { destroy(*s); return 0; }
|
||||
rc = xr_device_provider_initialize(s->device, nullptr, directory);
|
||||
env->ReleaseStringUTFChars(cache, directory);
|
||||
s->initialized = rc == 0;
|
||||
}
|
||||
if (rc == 0 && s->type == XR_DEVICE_TYPE_VITURE_CARINA && spatial) {
|
||||
clear_camera(true);
|
||||
rc = xr_device_provider_register_callbacks_carina(s->device, on_carina_pose, nullptr, nullptr, on_camera);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = xr_device_provider_start(s->device);
|
||||
s->started = rc == 0;
|
||||
}
|
||||
if (rc != 0) { report("initialize/start", rc); destroy(*s); return 0; }
|
||||
s->token = ++next_token;
|
||||
session = std::move(s);
|
||||
return session->token;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL JNI_METHOD(nativeType)(JNIEnv*, jobject, jlong token) {
|
||||
std::lock_guard<std::mutex> lock(api_mutex);
|
||||
return session && session->token == token ? session->type : -1;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL JNI_METHOD(nativeTracking)(JNIEnv*, jobject, jlong token, jboolean enabled) {
|
||||
std::lock_guard<std::mutex> lock(api_mutex);
|
||||
return session && session->token == token ? tracking(*session, enabled) : -1;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL JNI_METHOD(nativeCommand)(JNIEnv*, jobject, jlong token, jint command, jint value) {
|
||||
std::lock_guard<std::mutex> lock(api_mutex);
|
||||
if (!session || session->token != token) return -1;
|
||||
auto& s = *session;
|
||||
switch (command) {
|
||||
case 1: return get_display(s);
|
||||
case 2: return set_display(s, value);
|
||||
case 3: return xr_device_provider_get_brightness_level(s.device);
|
||||
case 4: return xr_device_provider_set_brightness_level(s.device, value);
|
||||
case 5: return xr_device_provider_get_volume_level(s.device);
|
||||
case 6: return xr_device_provider_set_volume_level(s.device, value);
|
||||
case 7: return xr_device_provider_set_film_mode(s.device, value ? 1.0f : 0.0f);
|
||||
default: return VITURE_GLASSES_ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jlong JNICALL JNI_METHOD(nativePose)(JNIEnv* env, jobject, jlong token, jfloatArray output) {
|
||||
std::lock_guard<std::mutex> lock(api_mutex);
|
||||
if (!session || session->token != token || !output || env->GetArrayLength(output) != 4) return -1;
|
||||
auto& s = *session;
|
||||
if (!s.tracking) return 0;
|
||||
if (s.type == XR_DEVICE_TYPE_VITURE_CARINA) {
|
||||
float pose[7]{};
|
||||
int status = 1;
|
||||
int rc = xr_device_provider_get_gl_pose_carina(s.device, pose, 0.0, &status);
|
||||
if (rc < 0) return rc;
|
||||
if (status != 0) return 0;
|
||||
env->SetFloatArrayRegion(output, 0, 4, pose + 3);
|
||||
return now_ns();
|
||||
}
|
||||
std::lock_guard<std::mutex> pose_lock(pose_mutex);
|
||||
if (!latest_time || latest_time == s.consumed_time) return 0;
|
||||
env->SetFloatArrayRegion(output, 0, 4, latest_pose.data());
|
||||
s.consumed_time = latest_time;
|
||||
return latest_time;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL JNI_METHOD(nativeNextState)(JNIEnv* env, jobject, jlong token, jintArray output) {
|
||||
std::lock_guard<std::mutex> lock(api_mutex);
|
||||
if (!session || session->token != token || !output || env->GetArrayLength(output) != 2) return -1;
|
||||
std::lock_guard<std::mutex> state_lock(pose_mutex);
|
||||
for (int id = 0; id < int(state_values.size()); ++id) {
|
||||
if (!(state_pending & (1u << id))) continue;
|
||||
state_pending &= ~(1u << id);
|
||||
const jint value[] = {id, state_values[id]};
|
||||
env->SetIntArrayRegion(output, 0, 2, value);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nativeClose)(JNIEnv*, jobject, jlong token) {
|
||||
std::lock_guard<std::mutex> lock(api_mutex);
|
||||
if (session && session->token == token) { destroy(*session); session.reset(); }
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jlong JNICALL JNI_METHOD(nativeSpatialPose)(JNIEnv* env, jobject, jlong token,
|
||||
jfloatArray output, jintArray state) {
|
||||
std::lock_guard<std::mutex> lock(api_mutex);
|
||||
if (!session || session->token != token || !session->tracking || session->type != XR_DEVICE_TYPE_VITURE_CARINA ||
|
||||
!output || !state || env->GetArrayLength(output) != 7 || env->GetArrayLength(state) != 1) return 0;
|
||||
float pose[7]{}; int status = 1;
|
||||
int rc = xr_device_provider_get_gl_pose_carina(session->device, pose, 0.0, &status);
|
||||
session->spatial_status = rc == 0 ? status : rc;
|
||||
jint valid = rc == 0 ? status : rc;
|
||||
if (valid == 0) {
|
||||
std::lock_guard<std::mutex> lock(camera_mutex);
|
||||
if (monotonic_ns()-camera_pose_receipt > 100000000LL) valid = 1;
|
||||
}
|
||||
env->SetIntArrayRegion(state, 0, 1, &valid);
|
||||
env->SetFloatArrayRegion(output, 0, 7, pose);
|
||||
return monotonic_ns();
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nativeCapture)(JNIEnv*, jobject, jlong token, jboolean wanted, jint ratePermille) {
|
||||
std::lock_guard<std::mutex> api(api_mutex);
|
||||
if (!session || session->token != token) return;
|
||||
std::lock_guard<std::mutex> lock(camera_mutex);
|
||||
camera_wanted = wanted;
|
||||
camera_gate.SetRatePermille(wanted ? ratePermille : 1000);
|
||||
if (!wanted) camera_consumed = camera_sequence;
|
||||
}
|
||||
extern "C" JNIEXPORT jstring JNICALL JNI_METHOD(nativeFirmware)(JNIEnv* env, jobject, jlong token) {
|
||||
std::lock_guard<std::mutex> api(api_mutex);
|
||||
if (!session || session->token != token) return nullptr;
|
||||
char value[256]{}; int length = 255;
|
||||
if (xr_device_provider_get_glasses_version(session->device, value, &length) != 0) return nullptr;
|
||||
value[255] = 0;
|
||||
return env->NewStringUTF(value);
|
||||
}
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL JNI_METHOD(nativeSerialHash)(JNIEnv* env, jobject, jlong token) {
|
||||
std::lock_guard<std::mutex> api(api_mutex);
|
||||
if (!session || session->token != token) return nullptr;
|
||||
uint8_t hash[32]{};
|
||||
const int rc = xr_device_provider_get_sn_hash(session->device, hash);
|
||||
if (rc != 0) { report("read device identity", rc); return nullptr; }
|
||||
auto result = env->NewByteArray(32);
|
||||
if (result) env->SetByteArrayRegion(result, 0, 32, reinterpret_cast<const jbyte*>(hash));
|
||||
return result;
|
||||
}
|
||||
extern "C" JNIEXPORT jdoubleArray JNICALL JNI_METHOD(nativeSpatialDiagnostics)(JNIEnv* env, jobject, jlong token) {
|
||||
std::lock_guard<std::mutex> api(api_mutex);
|
||||
if (!session || session->token != token) return nullptr;
|
||||
std::lock_guard<std::mutex> lock(camera_mutex);
|
||||
double values[3] = {double(session->spatial_status), camera_pose_receipt ? (monotonic_ns()-camera_pose_receipt)/1e6 : -1.0,
|
||||
camera_poses.empty() ? -1.0 : (camera_poses.back().time-camera_source)*1000.0};
|
||||
auto result=env->NewDoubleArray(3);
|
||||
if(result)env->SetDoubleArrayRegion(result,0,3,values);
|
||||
return result;
|
||||
}
|
||||
extern "C" JNIEXPORT jlong JNICALL JNI_METHOD(nativeStereo)(JNIEnv* env, jobject, jlong token,
|
||||
jobject left, jobject right, jdoubleArray metadata, jfloatArray head, jboolean waitForPose) {
|
||||
std::lock_guard<std::mutex> api(api_mutex);
|
||||
if (!session || session->token != token || !left || !right || !metadata || !head ||
|
||||
env->GetArrayLength(metadata) != 9 || env->GetArrayLength(head) != 7) return 0;
|
||||
std::lock_guard<std::mutex> lock(camera_mutex);
|
||||
if (!camera_wanted || camera_sequence == camera_consumed || camera_left.empty()) return 0;
|
||||
const auto size = camera_left.size();
|
||||
if (env->GetDirectBufferCapacity(left) < jlong(size) || env->GetDirectBufferCapacity(right) < jlong(size)) return -1;
|
||||
auto l = env->GetDirectBufferAddress(left), r = env->GetDirectBufferAddress(right);
|
||||
if (!l || !r) return -1;
|
||||
const CameraPose *best = matched_camera_pose(camera_source);
|
||||
if (waitForPose && !best && monotonic_ns()-camera_receipt < 35000000LL) return 0;
|
||||
memcpy(l, camera_left.data(), size); memcpy(r, camera_right.data(), size);
|
||||
double meta[9] = {double(camera_width), double(camera_height), camera_source, double(camera_receipt),
|
||||
double(token), double(camera_sequence), best ? 1.0 : 0.0,
|
||||
double(camera_input_sequence), double(camera_gate.skipped)};
|
||||
env->SetDoubleArrayRegion(metadata, 0, 9, meta);
|
||||
if (best) env->SetFloatArrayRegion(head, 0, 7, best->pose.data());
|
||||
camera_consumed = camera_sequence;
|
||||
return jlong(camera_sequence);
|
||||
}
|
||||
extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nativeCameraSynchronized)(JNIEnv*, jobject, jlong token, jdouble source) {
|
||||
std::lock_guard<std::mutex> api(api_mutex);
|
||||
if (!session || session->token != token || !std::isfinite(source)) return JNI_FALSE;
|
||||
std::lock_guard<std::mutex> lock(camera_mutex);
|
||||
return camera_accepting && matched_camera_pose(source) != nullptr;
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct RgbUsbLog {
|
||||
int savedStderr;
|
||||
bool hadLevel;
|
||||
std::string oldLevel;
|
||||
explicit RgbUsbLog(int fd) : savedStderr(dup(STDERR_FILENO)),
|
||||
hadLevel(getenv("LIBUSB_DEBUG") != nullptr), oldLevel(hadLevel ? getenv("LIBUSB_DEBUG") : "") {
|
||||
fflush(stderr);
|
||||
if (savedStderr >= 0) dup2(fd, STDERR_FILENO);
|
||||
setenv("LIBUSB_DEBUG", "4", 1);
|
||||
}
|
||||
~RgbUsbLog() {
|
||||
fflush(stderr);
|
||||
if (savedStderr >= 0) { dup2(savedStderr, STDERR_FILENO); close(savedStderr); }
|
||||
if (hadLevel) setenv("LIBUSB_DEBUG", oldLevel.c_str(), 1); else unsetenv("LIBUSB_DEBUG");
|
||||
}
|
||||
};
|
||||
|
||||
void rgb_usb_state(int fd, const char* stage) {
|
||||
uint8_t alternate = 0;
|
||||
usbdevfs_ctrltransfer request{};
|
||||
request.bRequestType=0x81; request.bRequest=0x0a; request.wIndex=1;
|
||||
request.wLength=1; request.timeout=200; request.data=&alternate;
|
||||
int result=ioctl(fd, USBDEVFS_CONTROL, &request);
|
||||
fprintf(stderr,"RGB %s GET_INTERFACE result=%d errno=%d alternate=%u\n",stage,result,result<0 ? errno : 0,alternate);
|
||||
for (int selector : {1, 2}) {
|
||||
uint8_t size[2]{};
|
||||
request.bRequestType=0xa1; request.bRequest=0x85; request.wValue=selector << 8;
|
||||
request.wLength=2; request.data=size;
|
||||
result=ioctl(fd,USBDEVFS_CONTROL,&request);
|
||||
const unsigned length=result==2 ? unsigned(size[0]) | unsigned(size[1]) << 8 : 26;
|
||||
uint8_t bytes[64]{};
|
||||
request.bRequest=0x81; request.wLength=std::clamp(length,26u,64u); request.data=bytes;
|
||||
result=ioctl(fd,USBDEVFS_CONTROL,&request);
|
||||
fprintf(stderr,"RGB %s %s result=%d errno=%d bytes=",stage,selector==1 ? "PROBE" : "COMMIT",result,result<0 ? errno : 0);
|
||||
for (int i=0;i<std::max(0,result);++i) fprintf(stderr,"%02x",bytes[i]);
|
||||
fprintf(stderr,"\n");
|
||||
}
|
||||
fflush(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL JNI_METHOD(nativeRgbSnapshot)(JNIEnv* env, jobject,
|
||||
jint vid, jint pid, jint fd, jlongArray metadata, jint logFd) {
|
||||
if (fd < 0 || !metadata || env->GetArrayLength(metadata) != 7 ||
|
||||
!xr_camera_provider_is_valid_camera(vid, pid)) return nullptr;
|
||||
RgbUsbLog usbLog(logFd);
|
||||
rgb_usb_state(fd,"before-create");
|
||||
struct Capture {
|
||||
std::mutex mutex;
|
||||
std::condition_variable ready;
|
||||
std::vector<uint8_t> jpeg;
|
||||
unsigned frames = 0;
|
||||
jlong values[7]{};
|
||||
} capture;
|
||||
auto camera = xr_camera_provider_create(vid, pid, fd);
|
||||
if (!camera) { fprintf(stderr,"RGB provider create failed\n"); return nullptr; }
|
||||
const int result = xr_camera_provider_start(camera, [](const XRCameraFrame* frame, void* data) {
|
||||
auto& c = *static_cast<Capture*>(data);
|
||||
std::lock_guard<std::mutex> lock(c.mutex);
|
||||
++c.frames;
|
||||
if (!frame || !frame->data || frame->format != XR_CAMERA_FORMAT_MJPEG ||
|
||||
frame->size < 4 || frame->size > 4 * 1024 * 1024) return;
|
||||
c.jpeg.assign(frame->data, frame->data + frame->size);
|
||||
c.values[0] = frame->width; c.values[1] = frame->height;
|
||||
c.values[2] = frame->timestamp; c.values[3] = frame->sequence;
|
||||
c.values[6] = monotonic_ns();
|
||||
c.ready.notify_one();
|
||||
}, &capture);
|
||||
fprintf(stderr,"RGB start result=%d streaming=%d\n",result,xr_camera_provider_is_streaming(camera));
|
||||
rgb_usb_state(fd,"after-start");
|
||||
if (result == 0) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(capture.mutex);
|
||||
capture.ready.wait_for(lock, std::chrono::seconds(5), [&] { return capture.frames >= 10 && !capture.jpeg.empty(); });
|
||||
}
|
||||
fprintf(stderr,"RGB stop result=%d\n",xr_camera_provider_stop(camera));
|
||||
}
|
||||
xr_camera_provider_destroy(camera);
|
||||
rgb_usb_state(fd,"after-destroy");
|
||||
capture.values[4] = result;
|
||||
capture.values[5] = capture.frames;
|
||||
env->SetLongArrayRegion(metadata, 0, 7, capture.values);
|
||||
if (capture.jpeg.empty()) return nullptr;
|
||||
auto bytes = env->NewByteArray(static_cast<jsize>(capture.jpeg.size()));
|
||||
if (bytes) env->SetByteArrayRegion(bytes, 0, static_cast<jsize>(capture.jpeg.size()),
|
||||
reinterpret_cast<const jbyte*>(capture.jpeg.data()));
|
||||
return bytes;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
struct StereoFrameGate {
|
||||
uint64_t received = 0, skipped = 0;
|
||||
void SetRatePermille(int rate) {
|
||||
if (rate != 500 && rate != 600 && rate != 800 && rate != 1000) rate = 500;
|
||||
if (ratePermille_ != rate) phase_ = 1000-rate;
|
||||
ratePermille_ = rate;
|
||||
}
|
||||
void SetHalfRate(bool enabled) {
|
||||
SetRatePermille(enabled ? 500 : 1000);
|
||||
}
|
||||
bool Accept(double source) {
|
||||
if (!std::isfinite(source) || source <= 0) return false;
|
||||
++received;
|
||||
if (source <= lastSource_) phase_ = 1000-ratePermille_;
|
||||
lastSource_ = source;
|
||||
phase_ += ratePermille_;
|
||||
const bool copy = phase_ >= 1000;
|
||||
if (copy) phase_ -= 1000;
|
||||
if (!copy) ++skipped;
|
||||
return copy;
|
||||
}
|
||||
private:
|
||||
int ratePermille_ = 1000, phase_ = 0;
|
||||
double lastSource_ = 0;
|
||||
};
|
||||
@@ -1,134 +0,0 @@
|
||||
#include <jni.h>
|
||||
#include <libusb.h>
|
||||
#include <libuvc/libuvc.h>
|
||||
#include "mjpeg_payload.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
struct UsbLog {
|
||||
int saved;
|
||||
bool hadLevel;
|
||||
std::string level;
|
||||
explicit UsbLog(int fd) : saved(dup(STDERR_FILENO)), hadLevel(getenv("LIBUSB_DEBUG") != nullptr),
|
||||
level(hadLevel ? getenv("LIBUSB_DEBUG") : "") {
|
||||
fflush(stderr);
|
||||
if (saved >= 0) dup2(fd, STDERR_FILENO);
|
||||
setenv("LIBUSB_DEBUG", "3", 1);
|
||||
libusb_set_log_cb(nullptr, [](libusb_context*, libusb_log_level, const char* text) {
|
||||
fputs(text, stderr);
|
||||
}, LIBUSB_LOG_CB_GLOBAL);
|
||||
}
|
||||
~UsbLog() {
|
||||
libusb_set_log_cb(nullptr, nullptr, LIBUSB_LOG_CB_GLOBAL);
|
||||
fflush(stderr);
|
||||
if (saved >= 0) { dup2(saved, STDERR_FILENO); close(saved); }
|
||||
if (hadLevel) setenv("LIBUSB_DEBUG", level.c_str(), 1); else unsetenv("LIBUSB_DEBUG");
|
||||
}
|
||||
};
|
||||
|
||||
struct Capture {
|
||||
std::mutex mutex;
|
||||
std::condition_variable ready;
|
||||
std::vector<uint8_t> jpeg;
|
||||
jlong values[14]{};
|
||||
bool accepting = true;
|
||||
};
|
||||
|
||||
jlong monotonic_ns() {
|
||||
timespec now{};
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return now.tv_sec*1000000000LL+now.tv_nsec;
|
||||
}
|
||||
|
||||
void finish_observation(Capture& capture) {
|
||||
std::lock_guard<std::mutex> lock(capture.mutex);
|
||||
if (!capture.accepting) return;
|
||||
capture.accepting = false;
|
||||
capture.values[12] = monotonic_ns();
|
||||
if (capture.values[6]) capture.values[13] = capture.values[12]-capture.values[6];
|
||||
capture.values[8] = std::max(capture.values[8],capture.values[13]);
|
||||
}
|
||||
|
||||
void frame_received(uvc_frame_t* frame, void* user) {
|
||||
auto& capture = *static_cast<Capture*>(user);
|
||||
std::lock_guard<std::mutex> lock(capture.mutex);
|
||||
if (!capture.accepting) return;
|
||||
++capture.values[5];
|
||||
const auto* bytes = frame ? static_cast<const uint8_t*>(frame->data) : nullptr;
|
||||
const size_t size = frame ? frame->data_bytes : 0;
|
||||
const size_t jpegSize = frame && frame->frame_format == UVC_FRAME_FORMAT_MJPEG ? mjpeg_payload_size(bytes,size) : 0;
|
||||
if (!jpegSize) {
|
||||
++capture.values[10];
|
||||
return;
|
||||
}
|
||||
const jlong time = monotonic_ns();
|
||||
capture.values[0]=frame->width; capture.values[1]=frame->height; capture.values[3]=frame->sequence;
|
||||
if (!capture.values[7]) capture.values[7]=time;
|
||||
if (capture.values[6]) capture.values[8]=std::max(capture.values[8],time-capture.values[6]);
|
||||
capture.values[6]=time;
|
||||
++capture.values[9];
|
||||
capture.values[11]+=jpegSize;
|
||||
capture.jpeg.assign(bytes,bytes+jpegSize);
|
||||
capture.ready.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_droiddeck_launcher_glasses_UvcRgbDiagnostic_nativeSnapshot(JNIEnv* env, jobject,
|
||||
jint fd, jint width, jint height, jint fps, jint durationMs, jlongArray metadata, jint logFd) {
|
||||
if (fd<0 || logFd<0 || !metadata || env->GetArrayLength(metadata)!=14 || width<160 || width>1920 ||
|
||||
height<120 || height>1080 || fps<5 || fps>30 || durationMs<1000 || durationMs>30000) return nullptr;
|
||||
UsbLog log(logFd);
|
||||
Capture capture;
|
||||
uvc_context_t* context=nullptr;
|
||||
uvc_device_handle_t* camera=nullptr;
|
||||
libusb_set_option(nullptr,LIBUSB_OPTION_NO_DEVICE_DISCOVERY);
|
||||
int result=uvc_init(&context,nullptr);
|
||||
fprintf(stderr,"UVC init=%d\n",result);
|
||||
if (result==0) {
|
||||
result=uvc_wrap(fd,context,&camera);
|
||||
fprintf(stderr,"UVC wrap=%d\n",result);
|
||||
}
|
||||
if (result==0) {
|
||||
uvc_print_diag(camera,stderr);
|
||||
uvc_stream_ctrl_t control{};
|
||||
result=uvc_get_stream_ctrl_format_size(camera,&control,UVC_FRAME_FORMAT_MJPEG,width,height,fps);
|
||||
fprintf(stderr,"UVC negotiate=%d requested=%dx%d@%d\n",result,width,height,fps);
|
||||
if (result==0) {
|
||||
uvc_print_stream_ctrl(&control,stderr);
|
||||
result=uvc_start_streaming(camera,&control,frame_received,&capture,0);
|
||||
fprintf(stderr,"UVC start=%d\n",result);
|
||||
if (result==0) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(capture.mutex);
|
||||
if (capture.ready.wait_for(lock,std::chrono::seconds(5),[&] { return !capture.jpeg.empty(); }))
|
||||
capture.ready.wait_for(lock,std::chrono::milliseconds(durationMs),[] { return false; });
|
||||
}
|
||||
finish_observation(capture);
|
||||
uvc_stop_streaming(camera);
|
||||
}
|
||||
}
|
||||
}
|
||||
finish_observation(capture);
|
||||
if (camera) uvc_close(camera);
|
||||
if (context) uvc_exit(context);
|
||||
capture.values[4]=result;
|
||||
fprintf(stderr,"UVC result=%d callbacks=%lld complete=%lld invalid=%lld maxGapMs=%.3f tailMs=%.3f\n",result,
|
||||
static_cast<long long>(capture.values[5]),static_cast<long long>(capture.values[9]),
|
||||
static_cast<long long>(capture.values[10]),capture.values[8]/1e6,capture.values[13]/1e6);
|
||||
env->SetLongArrayRegion(metadata,0,14,capture.values);
|
||||
if (result!=0 || capture.jpeg.empty()) return nullptr;
|
||||
auto bytes=env->NewByteArray(static_cast<jsize>(capture.jpeg.size()));
|
||||
if (bytes) env->SetByteArrayRegion(bytes,0,static_cast<jsize>(capture.jpeg.size()),
|
||||
reinterpret_cast<const jbyte*>(capture.jpeg.data()));
|
||||
return bytes;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ add_library(droiddeckwayland SHARED
|
||||
src/vk_present.c
|
||||
src/effects_chain.c
|
||||
src/cardboard.c
|
||||
src/spatialpose.c
|
||||
src/vk_loader.c
|
||||
src/sc_layer.c
|
||||
src/ahb_swapchain.c
|
||||
@@ -43,10 +42,10 @@ target_include_directories(droiddeckwayland PRIVATE
|
||||
)
|
||||
|
||||
set(DROIDDECK_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg)
|
||||
set(DROIDDECK_FRAMEGEN_DDFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/ddfg)
|
||||
set(DROIDDECK_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/winfg)
|
||||
if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp
|
||||
AND EXISTS ${DROIDDECK_FRAMEGEN_DDFG_DIR}/ddfg_engine.cpp)
|
||||
message(STATUS "droiddeckwayland: frame-generation engines built in (LSFG Native, DD-FG Native)")
|
||||
AND EXISTS ${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp)
|
||||
message(STATUS "droiddeckwayland: frame-generation engines built in (LSFG Native, Win-FG Native)")
|
||||
target_sources(droiddeckwayland PRIVATE
|
||||
src/framegen_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_probe.cpp
|
||||
@@ -66,9 +65,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_DDFG_DIR}/framegen.cpp
|
||||
${DROIDDECK_FRAMEGEN_DDFG_DIR}/ddfg_vkd.cpp
|
||||
${DROIDDECK_FRAMEGEN_DDFG_DIR}/ddfg_engine.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/framegen.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_vkd.cpp
|
||||
${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp
|
||||
)
|
||||
target_include_directories(droiddeckwayland PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
@@ -77,8 +76,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
|
||||
DDFG_UPSTREAM="${DDFG_UPSTREAM}"
|
||||
DDFG_CHAIN_HASH="${DDFG_CHAIN_HASH}"
|
||||
WINFG_UPSTREAM="${WINFG_UPSTREAM}"
|
||||
WINFG_CHAIN_HASH="${WINFG_CHAIN_HASH}"
|
||||
)
|
||||
target_link_libraries(droiddeckwayland dxbc atomic)
|
||||
else()
|
||||
|
||||
@@ -28,25 +28,22 @@ static float clampf(float v, float lo, float hi) { return v < lo ? lo : v > hi ?
|
||||
|
||||
void cardboard_clamp(struct cardboard_params *p) {
|
||||
p->enabled = p->enabled ? 1 : 0;
|
||||
if (p->source < CARDBOARD_SOURCE_SCREEN || p->source > CARDBOARD_SOURCE_FLAT) p->source = CARDBOARD_SOURCE_SCREEN;
|
||||
p->source = p->source == CARDBOARD_SOURCE_SBS ? CARDBOARD_SOURCE_SBS : CARDBOARD_SOURCE_SCREEN;
|
||||
p->ipd_m = clampf(p->ipd_m, 0.0f, 0.1f);
|
||||
p->screen_dist_m = clampf(p->screen_dist_m, 0.5f, 50.0f);
|
||||
p->screen_width_m = clampf(p->screen_width_m, 0.2f, 60.0f);
|
||||
p->k1 = clampf(p->k1, -1.0f, 2.0f);
|
||||
p->k2 = clampf(p->k2, -1.0f, 2.0f);
|
||||
// Optical vertical FOV on supported glasses can be as low as ~22 degrees.
|
||||
p->fov_deg = clampf(p->fov_deg, 20.0f, 140.0f);
|
||||
p->fov_deg = clampf(p->fov_deg, 30.0f, 140.0f);
|
||||
p->lens_inset = clampf(p->lens_inset, -0.25f, 0.25f);
|
||||
p->chroma = clampf(p->chroma, 0.0f, 0.05f);
|
||||
p->mono = p->mono ? 1 : 0;
|
||||
p->display_hz = clampf(p->display_hz, 0.0f, 240.0f);
|
||||
}
|
||||
|
||||
void cardboard_push_constants(const struct cardboard_params *p, const float pose[9],
|
||||
int src_w, int src_h, int out_w, int out_h, float out[24]) {
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 3; c++) out[r * 4 + c] = pose[r * 3 + c];
|
||||
out[3] = p->mono ? 0.0f : p->ipd_m * 0.5f;
|
||||
out[3] = p->ipd_m * 0.5f;
|
||||
out[7] = p->screen_dist_m;
|
||||
out[11] = p->screen_width_m * 0.5f;
|
||||
out[12] = p->k1;
|
||||
@@ -57,17 +54,13 @@ void cardboard_push_constants(const struct cardboard_params *p, const float pose
|
||||
out[17] = (src_w > 0 && src_h > 0) ? (float)src_w / (float)src_h : 16.0f / 9.0f;
|
||||
if (p->source == CARDBOARD_SOURCE_SBS) out[17] *= 0.5f;
|
||||
out[18] = (float)p->source;
|
||||
out[19] = (out_w > 0 && out_h > 0) ? ((float)out_w * (p->mono ? 1.0f : 0.5f)) / (float)out_h : 1.0f;
|
||||
out[20] = p->mono ? 1.0f : 0.0f;
|
||||
out[21] = out[22] = out[23] = 0.0f;
|
||||
}
|
||||
|
||||
void cardboard_push_translation(const struct cardboard_params *p, const float position[3], float out[24]) {
|
||||
for (int i = 0; i < 3; ++i) out[21+i] = p->source == CARDBOARD_SOURCE_SCREEN && isfinite(position[i]) ? position[i] : 0.0f;
|
||||
out[19] = (out_w > 0 && out_h > 0) ? ((float)out_w * 0.5f) / (float)out_h : 1.0f;
|
||||
out[20] = out[21] = out[22] = out[23] = 0.0f;
|
||||
}
|
||||
|
||||
#ifndef CARDBOARD_HOST_TEST
|
||||
|
||||
|
||||
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static struct cardboard_params g_set = {
|
||||
.enabled = 0, .source = CARDBOARD_SOURCE_SCREEN, .ipd_m = 0.064f, .screen_dist_m = 3.0f,
|
||||
@@ -75,7 +68,6 @@ static struct cardboard_params g_set = {
|
||||
static float g_pose[9] = {1, 0, 0, 0, 1, 0, 0, 0, 1};
|
||||
static volatile int g_changed = 1;
|
||||
|
||||
static float g_position[3];
|
||||
static int g_live;
|
||||
static void shm_publish(int live);
|
||||
|
||||
@@ -89,22 +81,13 @@ void vkp_cardboard_set(const struct cardboard_params *p) {
|
||||
shm_publish(g_live);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
if (was != c.enabled || c.enabled)
|
||||
droiddeck_log("cardboard", "%s: %s%s, ipd %.0f mm, screen %.1f m wide at %.1f m, lens k1 %.2f k2 %.2f, fov %.0f, inset %.3f, chroma %.3f",
|
||||
c.enabled ? "on" : "off", c.source == CARDBOARD_SOURCE_SBS ? "side-by-side source" :
|
||||
c.source == CARDBOARD_SOURCE_FLAT ? "flat image in both eyes" : "virtual screen",
|
||||
c.mono ? " (one eye)" : "", c.ipd_m * 1000.0f, c.screen_width_m, c.screen_dist_m, c.k1, c.k2, c.fov_deg, c.lens_inset, c.chroma);
|
||||
droiddeck_log("cardboard", "%s: %s, ipd %.0f mm, screen %.1f m wide at %.1f m, lens k1 %.2f k2 %.2f, fov %.0f, inset %.3f, chroma %.3f",
|
||||
c.enabled ? "on" : "off", c.source == CARDBOARD_SOURCE_SBS ? "side-by-side source" : "virtual screen",
|
||||
c.ipd_m * 1000.0f, c.screen_width_m, c.screen_dist_m, c.k1, c.k2, c.fov_deg, c.lens_inset, c.chroma);
|
||||
}
|
||||
|
||||
static struct droiddeck_headpose *g_shm;
|
||||
static int g_eye_w, g_eye_h;
|
||||
static int g_render_height;
|
||||
|
||||
void vkp_cardboard_set_render_height(int height) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
g_render_height = height > 0 ? height : 0;
|
||||
shm_publish(g_live);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
|
||||
void vkp_cardboard_set_pose_file(const char *path) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
@@ -118,7 +101,6 @@ void vkp_cardboard_set_pose_file(const char *path) {
|
||||
g_shm->magic = DROIDDECK_HEADPOSE_MAGIC;
|
||||
g_shm->version = DROIDDECK_HEADPOSE_VERSION;
|
||||
memcpy(g_shm->rot, g_pose, sizeof(g_shm->rot));
|
||||
shm_publish(g_live);
|
||||
}
|
||||
}
|
||||
if (fd >= 0) close(fd);
|
||||
@@ -137,10 +119,7 @@ static void shm_publish(int live) {
|
||||
atomic_thread_fence(memory_order_release);
|
||||
memcpy(g_shm->rot, g_pose, sizeof(g_shm->rot));
|
||||
g_shm->ipd_m = g_set.ipd_m;
|
||||
g_shm->flags = (live ? DROIDDECK_HEADPOSE_LIVE : 0u) | (g_set.enabled ? DROIDDECK_HEADPOSE_ENABLED : 0u) |
|
||||
(g_set.mono ? DROIDDECK_HEADPOSE_MONO : 0u);
|
||||
g_shm->display_hz = g_set.display_hz;
|
||||
g_shm->render_height = (uint32_t)g_render_height;
|
||||
g_shm->flags = (live ? DROIDDECK_HEADPOSE_LIVE : 0u) | (g_set.enabled ? DROIDDECK_HEADPOSE_ENABLED : 0u);
|
||||
g_shm->time_ns = (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
|
||||
g_shm->tan_half_fov = tanf(g_set.fov_deg * 0.5f * (float)M_PI / 180.0f);
|
||||
g_shm->eye_w = (uint32_t)(g_eye_w > 0 ? g_eye_w : 0);
|
||||
@@ -151,31 +130,22 @@ static void shm_publish(int live) {
|
||||
}
|
||||
|
||||
|
||||
void vkp_cardboard_set_spatial_pose(const float m[9], const float position[3], int live) {
|
||||
void vkp_cardboard_set_pose(const float m[9], int live) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
memcpy(g_pose, m, sizeof(g_pose));
|
||||
memcpy(g_position, position, sizeof(g_position));
|
||||
g_live = live;
|
||||
if (g_set.enabled && g_set.source == CARDBOARD_SOURCE_SCREEN) g_changed = 1;
|
||||
shm_publish(live);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
|
||||
void vkp_cardboard_set_pose(const float m[9], int live) {
|
||||
const float zero[3] = {0};
|
||||
vkp_cardboard_set_spatial_pose(m, zero, live);
|
||||
}
|
||||
|
||||
|
||||
static struct cardboard_params g_cur;
|
||||
static float g_cur_pose[9];
|
||||
static float g_cur_position[3];
|
||||
|
||||
static void snapshot(void) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
g_cur = g_set;
|
||||
memcpy(g_cur_pose, g_pose, sizeof(g_cur_pose));
|
||||
memcpy(g_cur_position, g_position, sizeof(g_cur_position));
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
|
||||
@@ -431,18 +401,16 @@ VkImage vkp_cardboard_run(VkCommandBuffer cmd, VkImage src, int src_w, int src_h
|
||||
if (!said) { said = 1; droiddeck_log("error", "cardboard: no descriptor for the frame; shown flat"); }
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
snapshot();
|
||||
int eye_w = g_cur.mono ? out_w : out_w / 2;
|
||||
if (g_eye_w != eye_w || g_eye_h != out_h) {
|
||||
if (g_eye_w != out_w / 2 || g_eye_h != out_h) {
|
||||
pthread_mutex_lock(&g_lock);
|
||||
g_eye_w = eye_w;
|
||||
g_eye_w = out_w / 2;
|
||||
g_eye_h = out_h;
|
||||
shm_publish(g_live);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
snapshot();
|
||||
struct { float ndc[4]; float rest[24]; } pc = {{-1.0f, -1.0f, 1.0f, 1.0f}, {0}};
|
||||
cardboard_push_constants(&g_cur, g_cur_pose, src_w, src_h, out_w, out_h, pc.rest);
|
||||
cardboard_push_translation(&g_cur, g_cur_position, pc.rest);
|
||||
|
||||
VkRenderPassBeginInfo rpi = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .renderPass = g_rp,
|
||||
.framebuffer = g_t.fb, .renderArea = {{0, 0}, {(uint32_t)out_w, (uint32_t)out_h}}};
|
||||
|
||||
@@ -10,7 +10,7 @@ layout(push_constant) uniform PC {
|
||||
vec4 rot2;
|
||||
vec4 lens;
|
||||
vec4 view;
|
||||
vec4 extra;
|
||||
|
||||
} pc;
|
||||
|
||||
const vec3 BACKDROP = vec3(0.015, 0.015, 0.02);
|
||||
@@ -24,21 +24,14 @@ vec3 eyeSample(vec2 tan, float eye) {
|
||||
if (pc.view.z > 0.5) {
|
||||
float tanH = pc.lens.z;
|
||||
vec2 s = vec2(tan.x / (tanH * pc.view.w), tan.y / tanH) * 0.5 + 0.5;
|
||||
if (pc.view.z > 1.5) {
|
||||
vec2 fit = max(vec2(pc.view.w / pc.view.y, pc.view.y / pc.view.w), vec2(1.0));
|
||||
s = (s - 0.5) * fit + 0.5;
|
||||
if (any(lessThan(s, vec2(0.0))) || any(greaterThan(s, vec2(1.0)))) return vec3(0.0);
|
||||
return texture(Source, s).rgb;
|
||||
}
|
||||
if (any(lessThan(s, vec2(0.0))) || any(greaterThan(s, vec2(1.0)))) return vec3(0.0);
|
||||
return texture(Source, vec2((s.x + eye) * 0.5, s.y)).rgb;
|
||||
}
|
||||
mat3 R = transpose(mat3(pc.rot0.xyz, pc.rot1.xyz, pc.rot2.xyz));
|
||||
vec3 d = R * normalize(vec3(tan.x, -tan.y, -1.0));
|
||||
vec3 o = pc.extra.yzw + R * vec3((eye < 0.5 ? -1.0 : 1.0) * pc.rot0.w, 0.0, 0.0);
|
||||
vec3 o = R * vec3((eye < 0.5 ? -1.0 : 1.0) * pc.rot0.w, 0.0, 0.0);
|
||||
if (d.z > -1e-4) return BACKDROP;
|
||||
float t = (-pc.rot1.w - o.z) / d.z;
|
||||
if (t <= 0.0) return BACKDROP;
|
||||
vec3 hit = o + d * t;
|
||||
float halfW = pc.rot2.w, halfH = halfW / pc.view.y;
|
||||
vec2 s = vec2(hit.x / halfW, -hit.y / halfH) * 0.5 + 0.5;
|
||||
@@ -47,10 +40,9 @@ vec3 eyeSample(vec2 tan, float eye) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
bool mono = pc.extra.x > 0.5;
|
||||
float eye = mono || uv.x < 0.5 ? 0.0 : 1.0;
|
||||
vec2 local = mono ? uv : vec2(uv.x * 2.0 - eye, uv.y);
|
||||
float cx = mono ? 0.5 : 0.5 + (eye < 0.5 ? pc.view.x : -pc.view.x);
|
||||
float eye = uv.x < 0.5 ? 0.0 : 1.0;
|
||||
vec2 local = vec2(uv.x * 2.0 - eye, uv.y);
|
||||
float cx = 0.5 + (eye < 0.5 ? pc.view.x : -pc.view.x);
|
||||
vec2 p = local - vec2(cx, 0.5);
|
||||
float tanH = pc.lens.z;
|
||||
vec2 t = vec2(p.x * pc.view.w, p.y) * 2.0 * tanH;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#define VK_USE_PLATFORM_ANDROID_KHR
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
enum { CARDBOARD_SOURCE_SCREEN = 0, CARDBOARD_SOURCE_SBS = 1, CARDBOARD_SOURCE_FLAT = 2 };
|
||||
enum { CARDBOARD_SOURCE_SCREEN = 0, CARDBOARD_SOURCE_SBS = 1 };
|
||||
|
||||
struct cardboard_params {
|
||||
int enabled;
|
||||
@@ -15,15 +15,11 @@ struct cardboard_params {
|
||||
float fov_deg;
|
||||
float lens_inset;
|
||||
float chroma;
|
||||
int mono;
|
||||
float display_hz;
|
||||
};
|
||||
|
||||
void vkp_cardboard_set(const struct cardboard_params *p);
|
||||
void vkp_cardboard_set_pose(const float m[9], int live);
|
||||
void vkp_cardboard_set_spatial_pose(const float m[9], const float position[3], int live);
|
||||
void vkp_cardboard_set_pose_file(const char *path);
|
||||
void vkp_cardboard_set_render_height(int height);
|
||||
|
||||
void vkp_cardboard_bind_device(VkDevice dev, const VkPhysicalDeviceMemoryProperties *memprops);
|
||||
int vkp_cardboard_active(void);
|
||||
@@ -34,6 +30,5 @@ VkImage vkp_cardboard_run(VkCommandBuffer cmd, VkImage src, int src_w, int src_h
|
||||
void cardboard_clamp(struct cardboard_params *p);
|
||||
void cardboard_push_constants(const struct cardboard_params *p, const float pose[9],
|
||||
int src_w, int src_h, int out_w, int out_h, float out[24]);
|
||||
void cardboard_push_translation(const struct cardboard_params *p, const float position[3], float out[24]);
|
||||
|
||||
#endif
|
||||
@@ -1,9 +1,8 @@
|
||||
// 1115.1.0
|
||||
#pragma once
|
||||
const uint32_t cardboard_frag_code[] = {
|
||||
0x07230203,0x00010000,0x0008000b,0x000001ba,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
0x07230203,0x00010000,0x0008000b,0x0000016d,0x00000000,0x00020011,0x00000001,0x0006000b,
|
||||
0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e,0x00000000,0x00000001,
|
||||
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x0000012d,0x000001b2,0x00030010,
|
||||
0x0007000f,0x00000004,0x00000004,0x6e69616d,0x00000000,0x000000ee,0x00000165,0x00030010,
|
||||
0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004,0x6e69616d,
|
||||
0x00000000,0x00070005,0x0000000d,0x69646e75,0x726f7473,0x66762874,0x31663b32,0x0000003b,
|
||||
0x00030005,0x0000000b,0x00000074,0x00040005,0x0000000c,0x6c616373,0x00000065,0x00070005,
|
||||
@@ -12,306 +11,255 @@ const uint32_t cardboard_frag_code[] = {
|
||||
0x0000001e,0x00004350,0x00040006,0x0000001e,0x00000000,0x0063646e,0x00050006,0x0000001e,
|
||||
0x00000001,0x30746f72,0x00000000,0x00050006,0x0000001e,0x00000002,0x31746f72,0x00000000,
|
||||
0x00050006,0x0000001e,0x00000003,0x32746f72,0x00000000,0x00050006,0x0000001e,0x00000004,
|
||||
0x736e656c,0x00000000,0x00050006,0x0000001e,0x00000005,0x77656976,0x00000000,0x00050006,
|
||||
0x0000001e,0x00000006,0x72747865,0x00000061,0x00030005,0x00000020,0x00006370,0x00040005,
|
||||
0x0000003f,0x486e6174,0x00000000,0x00030005,0x00000042,0x00000073,0x00030005,0x00000059,
|
||||
0x00746966,0x00040005,0x00000082,0x72756f53,0x00006563,0x00030005,0x000000a3,0x00000052,
|
||||
0x00030005,0x000000c0,0x00000064,0x00030005,0x000000cb,0x0000006f,0x00030005,0x000000e4,
|
||||
0x00000074,0x00030005,0x000000f3,0x00746968,0x00040005,0x000000f9,0x666c6168,0x00000057,
|
||||
0x00040005,0x000000fc,0x666c6168,0x00000048,0x00030005,0x00000101,0x00000073,0x00040005,
|
||||
0x00000123,0x6f6e6f6d,0x00000000,0x00030005,0x00000127,0x00657965,0x00030005,0x0000012d,
|
||||
0x00007675,0x00040005,0x00000134,0x61636f6c,0x0000006c,0x00030005,0x00000145,0x00007863,
|
||||
0x00030005,0x00000159,0x00000070,0x00040005,0x0000015e,0x486e6174,0x00000000,0x00030005,
|
||||
0x00000161,0x00000074,0x00030005,0x00000172,0x00000063,0x00040005,0x00000176,0x61726170,
|
||||
0x0000006d,0x00040005,0x00000178,0x61726170,0x0000006d,0x00040005,0x0000017a,0x61726170,
|
||||
0x0000006d,0x00040005,0x0000017b,0x61726170,0x0000006d,0x00040005,0x00000180,0x61726170,
|
||||
0x0000006d,0x00040005,0x00000182,0x61726170,0x0000006d,0x00040005,0x00000184,0x61726170,
|
||||
0x0000006d,0x00040005,0x00000185,0x61726170,0x0000006d,0x00040005,0x0000018d,0x61726170,
|
||||
0x0000006d,0x00040005,0x0000018f,0x61726170,0x0000006d,0x00040005,0x00000191,0x61726170,
|
||||
0x0000006d,0x00040005,0x00000192,0x61726170,0x0000006d,0x00040005,0x00000198,0x61726170,
|
||||
0x0000006d,0x00040005,0x0000019a,0x61726170,0x0000006d,0x00040005,0x0000019c,0x61726170,
|
||||
0x0000006d,0x00040005,0x0000019d,0x61726170,0x0000006d,0x00030005,0x000001a0,0x00000065,
|
||||
0x00040005,0x000001a6,0x65646166,0x00000000,0x00050005,0x000001b2,0x4374756f,0x726f6c6f,
|
||||
0x00000000,0x00030047,0x0000001e,0x00000002,0x00050048,0x0000001e,0x00000000,0x00000023,
|
||||
0x00000000,0x00050048,0x0000001e,0x00000001,0x00000023,0x00000010,0x00050048,0x0000001e,
|
||||
0x00000002,0x00000023,0x00000020,0x00050048,0x0000001e,0x00000003,0x00000023,0x00000030,
|
||||
0x00050048,0x0000001e,0x00000004,0x00000023,0x00000040,0x00050048,0x0000001e,0x00000005,
|
||||
0x00000023,0x00000050,0x00050048,0x0000001e,0x00000006,0x00000023,0x00000060,0x00040047,
|
||||
0x00000082,0x00000021,0x00000000,0x00040047,0x00000082,0x00000022,0x00000000,0x00040047,
|
||||
0x0000012d,0x0000001e,0x00000000,0x00040047,0x000001b2,0x0000001e,0x00000000,0x00020013,
|
||||
0x00000002,0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,
|
||||
0x00000007,0x00000006,0x00000002,0x00040020,0x00000008,0x00000007,0x00000007,0x00040020,
|
||||
0x00000009,0x00000007,0x00000006,0x00050021,0x0000000a,0x00000007,0x00000008,0x00000009,
|
||||
0x00040017,0x0000000f,0x00000006,0x00000003,0x00050021,0x00000010,0x0000000f,0x00000008,
|
||||
0x00000009,0x0004002b,0x00000006,0x0000001c,0x3f800000,0x00040017,0x0000001d,0x00000006,
|
||||
0x00000004,0x0009001e,0x0000001e,0x0000001d,0x0000001d,0x0000001d,0x0000001d,0x0000001d,
|
||||
0x0000001d,0x0000001d,0x00040020,0x0000001f,0x00000009,0x0000001e,0x0004003b,0x0000001f,
|
||||
0x00000020,0x00000009,0x00040015,0x00000021,0x00000020,0x00000001,0x0004002b,0x00000021,
|
||||
0x00000022,0x00000004,0x00040015,0x00000023,0x00000020,0x00000000,0x0004002b,0x00000023,
|
||||
0x00000024,0x00000000,0x00040020,0x00000025,0x00000009,0x00000006,0x0004002b,0x00000023,
|
||||
0x0000002b,0x00000001,0x0004002b,0x00000021,0x00000036,0x00000005,0x0004002b,0x00000023,
|
||||
0x00000037,0x00000002,0x0004002b,0x00000006,0x0000003a,0x3f000000,0x00020014,0x0000003b,
|
||||
0x0004002b,0x00000023,0x00000046,0x00000003,0x0004002b,0x00000006,0x00000055,0x3fc00000,
|
||||
0x0005002c,0x00000007,0x00000065,0x0000001c,0x0000001c,0x0004002b,0x00000006,0x0000006f,
|
||||
0x00000000,0x0005002c,0x00000007,0x00000070,0x0000006f,0x0000006f,0x00040017,0x00000071,
|
||||
0x0000003b,0x00000002,0x0006002c,0x0000000f,0x0000007d,0x0000006f,0x0000006f,0x0000006f,
|
||||
0x00090019,0x0000007f,0x00000006,0x00000001,0x00000000,0x00000000,0x00000000,0x00000001,
|
||||
0x00000000,0x0003001b,0x00000080,0x0000007f,0x00040020,0x00000081,0x00000000,0x00000080,
|
||||
0x0004003b,0x00000081,0x00000082,0x00000000,0x00040018,0x000000a1,0x0000000f,0x00000003,
|
||||
0x00040020,0x000000a2,0x00000007,0x000000a1,0x0004002b,0x00000021,0x000000a4,0x00000001,
|
||||
0x00040020,0x000000a5,0x00000009,0x0000001d,0x0004002b,0x00000021,0x000000a9,0x00000002,
|
||||
0x0004002b,0x00000021,0x000000ad,0x00000003,0x00040020,0x000000bf,0x00000007,0x0000000f,
|
||||
0x0004002b,0x00000006,0x000000c7,0xbf800000,0x0004002b,0x00000021,0x000000cc,0x00000006,
|
||||
0x0004002b,0x00000006,0x000000dc,0xb8d1b717,0x0004002b,0x00000006,0x000000e0,0x3c75c28f,
|
||||
0x0004002b,0x00000006,0x000000e1,0x3ca3d70a,0x0006002c,0x0000000f,0x000000e2,0x000000e0,
|
||||
0x000000e0,0x000000e1,0x00040020,0x00000122,0x00000007,0x0000003b,0x00040020,0x0000012c,
|
||||
0x00000001,0x00000007,0x0004003b,0x0000012c,0x0000012d,0x00000001,0x00040020,0x0000012e,
|
||||
0x00000001,0x00000006,0x0004002b,0x00000006,0x0000013d,0x40000000,0x0004002b,0x00000006,
|
||||
0x000001a7,0x3cf5c28f,0x00040020,0x000001b1,0x00000003,0x0000001d,0x0004003b,0x000001b1,
|
||||
0x000001b2,0x00000003,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,
|
||||
0x00000005,0x0004003b,0x00000122,0x00000123,0x00000007,0x0004003b,0x00000009,0x00000127,
|
||||
0x00000007,0x0004003b,0x00000008,0x00000134,0x00000007,0x0004003b,0x00000008,0x00000136,
|
||||
0x00000007,0x0004003b,0x00000009,0x00000145,0x00000007,0x0004003b,0x00000009,0x00000147,
|
||||
0x00000007,0x0004003b,0x00000009,0x0000014d,0x00000007,0x0004003b,0x00000008,0x00000159,
|
||||
0x00000007,0x0004003b,0x00000009,0x0000015e,0x00000007,0x0004003b,0x00000008,0x00000161,
|
||||
0x00000007,0x0004003b,0x000000bf,0x00000172,0x00000007,0x0004003b,0x00000008,0x00000176,
|
||||
0x00000007,0x0004003b,0x00000009,0x00000178,0x00000007,0x0004003b,0x00000008,0x0000017a,
|
||||
0x00000007,0x0004003b,0x00000009,0x0000017b,0x00000007,0x0004003b,0x00000008,0x00000180,
|
||||
0x00000007,0x0004003b,0x00000009,0x00000182,0x00000007,0x0004003b,0x00000008,0x00000184,
|
||||
0x00000007,0x0004003b,0x00000009,0x00000185,0x00000007,0x0004003b,0x00000008,0x0000018d,
|
||||
0x00000007,0x0004003b,0x00000009,0x0000018f,0x00000007,0x0004003b,0x00000008,0x00000191,
|
||||
0x00000007,0x0004003b,0x00000009,0x00000192,0x00000007,0x0004003b,0x00000008,0x00000198,
|
||||
0x00000007,0x0004003b,0x00000009,0x0000019a,0x00000007,0x0004003b,0x00000008,0x0000019c,
|
||||
0x00000007,0x0004003b,0x00000009,0x0000019d,0x00000007,0x0004003b,0x00000008,0x000001a0,
|
||||
0x00000007,0x0004003b,0x00000009,0x000001a6,0x00000007,0x00060041,0x00000025,0x00000124,
|
||||
0x00000020,0x000000cc,0x00000024,0x0004003d,0x00000006,0x00000125,0x00000124,0x000500ba,
|
||||
0x0000003b,0x00000126,0x00000125,0x0000003a,0x0003003e,0x00000123,0x00000126,0x0004003d,
|
||||
0x0000003b,0x00000128,0x00000123,0x000400a8,0x0000003b,0x00000129,0x00000128,0x000300f7,
|
||||
0x0000012b,0x00000000,0x000400fa,0x00000129,0x0000012a,0x0000012b,0x000200f8,0x0000012a,
|
||||
0x00050041,0x0000012e,0x0000012f,0x0000012d,0x00000024,0x0004003d,0x00000006,0x00000130,
|
||||
0x0000012f,0x000500b8,0x0000003b,0x00000131,0x00000130,0x0000003a,0x000200f9,0x0000012b,
|
||||
0x000200f8,0x0000012b,0x000700f5,0x0000003b,0x00000132,0x00000128,0x00000005,0x00000131,
|
||||
0x0000012a,0x000600a9,0x00000006,0x00000133,0x00000132,0x0000006f,0x0000001c,0x0003003e,
|
||||
0x00000127,0x00000133,0x0004003d,0x0000003b,0x00000135,0x00000123,0x000300f7,0x00000138,
|
||||
0x00000000,0x000400fa,0x00000135,0x00000137,0x0000013a,0x000200f8,0x00000137,0x0004003d,
|
||||
0x00000007,0x00000139,0x0000012d,0x0003003e,0x00000136,0x00000139,0x000200f9,0x00000138,
|
||||
0x000200f8,0x0000013a,0x00050041,0x0000012e,0x0000013b,0x0000012d,0x00000024,0x0004003d,
|
||||
0x00000006,0x0000013c,0x0000013b,0x00050085,0x00000006,0x0000013e,0x0000013c,0x0000013d,
|
||||
0x0004003d,0x00000006,0x0000013f,0x00000127,0x00050083,0x00000006,0x00000140,0x0000013e,
|
||||
0x0000013f,0x00050041,0x0000012e,0x00000141,0x0000012d,0x0000002b,0x0004003d,0x00000006,
|
||||
0x00000142,0x00000141,0x00050050,0x00000007,0x00000143,0x00000140,0x00000142,0x0003003e,
|
||||
0x00000136,0x00000143,0x000200f9,0x00000138,0x000200f8,0x00000138,0x0004003d,0x00000007,
|
||||
0x00000144,0x00000136,0x0003003e,0x00000134,0x00000144,0x0004003d,0x0000003b,0x00000146,
|
||||
0x00000123,0x000300f7,0x00000149,0x00000000,0x000400fa,0x00000146,0x00000148,0x0000014a,
|
||||
0x000200f8,0x00000148,0x0003003e,0x00000147,0x0000003a,0x000200f9,0x00000149,0x000200f8,
|
||||
0x0000014a,0x0004003d,0x00000006,0x0000014b,0x00000127,0x000500b8,0x0000003b,0x0000014c,
|
||||
0x0000014b,0x0000003a,0x000300f7,0x0000014f,0x00000000,0x000400fa,0x0000014c,0x0000014e,
|
||||
0x00000152,0x000200f8,0x0000014e,0x00060041,0x00000025,0x00000150,0x00000020,0x00000036,
|
||||
0x00000024,0x0004003d,0x00000006,0x00000151,0x00000150,0x0003003e,0x0000014d,0x00000151,
|
||||
0x000200f9,0x0000014f,0x000200f8,0x00000152,0x00060041,0x00000025,0x00000153,0x00000020,
|
||||
0x00000036,0x00000024,0x0004003d,0x00000006,0x00000154,0x00000153,0x0004007f,0x00000006,
|
||||
0x00000155,0x00000154,0x0003003e,0x0000014d,0x00000155,0x000200f9,0x0000014f,0x000200f8,
|
||||
0x0000014f,0x0004003d,0x00000006,0x00000156,0x0000014d,0x00050081,0x00000006,0x00000157,
|
||||
0x0000003a,0x00000156,0x0003003e,0x00000147,0x00000157,0x000200f9,0x00000149,0x000200f8,
|
||||
0x00000149,0x0004003d,0x00000006,0x00000158,0x00000147,0x0003003e,0x00000145,0x00000158,
|
||||
0x0004003d,0x00000007,0x0000015a,0x00000134,0x0004003d,0x00000006,0x0000015b,0x00000145,
|
||||
0x00050050,0x00000007,0x0000015c,0x0000015b,0x0000003a,0x00050083,0x00000007,0x0000015d,
|
||||
0x0000015a,0x0000015c,0x0003003e,0x00000159,0x0000015d,0x00060041,0x00000025,0x0000015f,
|
||||
0x00000020,0x00000022,0x00000037,0x0004003d,0x00000006,0x00000160,0x0000015f,0x0003003e,
|
||||
0x0000015e,0x00000160,0x00050041,0x00000009,0x00000162,0x00000159,0x00000024,0x0004003d,
|
||||
0x00000006,0x00000163,0x00000162,0x00060041,0x00000025,0x00000164,0x00000020,0x00000036,
|
||||
0x00000046,0x0004003d,0x00000006,0x00000165,0x00000164,0x00050085,0x00000006,0x00000166,
|
||||
0x00000163,0x00000165,0x00050041,0x00000009,0x00000167,0x00000159,0x0000002b,0x0004003d,
|
||||
0x00000006,0x00000168,0x00000167,0x00050050,0x00000007,0x00000169,0x00000166,0x00000168,
|
||||
0x0005008e,0x00000007,0x0000016a,0x00000169,0x0000013d,0x0004003d,0x00000006,0x0000016b,
|
||||
0x0000015e,0x0005008e,0x00000007,0x0000016c,0x0000016a,0x0000016b,0x0003003e,0x00000161,
|
||||
0x0000016c,0x00060041,0x00000025,0x0000016d,0x00000020,0x00000022,0x00000046,0x0004003d,
|
||||
0x00000006,0x0000016e,0x0000016d,0x000500ba,0x0000003b,0x0000016f,0x0000016e,0x0000006f,
|
||||
0x000300f7,0x00000171,0x00000000,0x000400fa,0x0000016f,0x00000170,0x00000197,0x000200f8,
|
||||
0x00000170,0x00060041,0x00000025,0x00000173,0x00000020,0x00000022,0x00000046,0x0004003d,
|
||||
0x00000006,0x00000174,0x00000173,0x00050083,0x00000006,0x00000175,0x0000001c,0x00000174,
|
||||
0x0004003d,0x00000007,0x00000177,0x00000161,0x0003003e,0x00000176,0x00000177,0x0003003e,
|
||||
0x00000178,0x00000175,0x00060039,0x00000007,0x00000179,0x0000000d,0x00000176,0x00000178,
|
||||
0x0003003e,0x0000017a,0x00000179,0x0004003d,0x00000006,0x0000017c,0x00000127,0x0003003e,
|
||||
0x0000017b,0x0000017c,0x00060039,0x0000000f,0x0000017d,0x00000013,0x0000017a,0x0000017b,
|
||||
0x00050051,0x00000006,0x0000017e,0x0000017d,0x00000000,0x00050041,0x00000009,0x0000017f,
|
||||
0x00000172,0x00000024,0x0003003e,0x0000017f,0x0000017e,0x0004003d,0x00000007,0x00000181,
|
||||
0x00000161,0x0003003e,0x00000180,0x00000181,0x0003003e,0x00000182,0x0000001c,0x00060039,
|
||||
0x00000007,0x00000183,0x0000000d,0x00000180,0x00000182,0x0003003e,0x00000184,0x00000183,
|
||||
0x0004003d,0x00000006,0x00000186,0x00000127,0x0003003e,0x00000185,0x00000186,0x00060039,
|
||||
0x0000000f,0x00000187,0x00000013,0x00000184,0x00000185,0x00050051,0x00000006,0x00000188,
|
||||
0x00000187,0x00000001,0x00050041,0x00000009,0x00000189,0x00000172,0x0000002b,0x0003003e,
|
||||
0x00000189,0x00000188,0x00060041,0x00000025,0x0000018a,0x00000020,0x00000022,0x00000046,
|
||||
0x0004003d,0x00000006,0x0000018b,0x0000018a,0x00050081,0x00000006,0x0000018c,0x0000001c,
|
||||
0x0000018b,0x0004003d,0x00000007,0x0000018e,0x00000161,0x0003003e,0x0000018d,0x0000018e,
|
||||
0x0003003e,0x0000018f,0x0000018c,0x00060039,0x00000007,0x00000190,0x0000000d,0x0000018d,
|
||||
0x0000018f,0x0003003e,0x00000191,0x00000190,0x0004003d,0x00000006,0x00000193,0x00000127,
|
||||
0x0003003e,0x00000192,0x00000193,0x00060039,0x0000000f,0x00000194,0x00000013,0x00000191,
|
||||
0x00000192,0x00050051,0x00000006,0x00000195,0x00000194,0x00000002,0x00050041,0x00000009,
|
||||
0x00000196,0x00000172,0x00000037,0x0003003e,0x00000196,0x00000195,0x000200f9,0x00000171,
|
||||
0x000200f8,0x00000197,0x0004003d,0x00000007,0x00000199,0x00000161,0x0003003e,0x00000198,
|
||||
0x00000199,0x0003003e,0x0000019a,0x0000001c,0x00060039,0x00000007,0x0000019b,0x0000000d,
|
||||
0x00000198,0x0000019a,0x0003003e,0x0000019c,0x0000019b,0x0004003d,0x00000006,0x0000019e,
|
||||
0x00000127,0x0003003e,0x0000019d,0x0000019e,0x00060039,0x0000000f,0x0000019f,0x00000013,
|
||||
0x0000019c,0x0000019d,0x0003003e,0x00000172,0x0000019f,0x000200f9,0x00000171,0x000200f8,
|
||||
0x00000171,0x0004003d,0x00000007,0x000001a1,0x00000134,0x0004003d,0x00000007,0x000001a2,
|
||||
0x00000134,0x00050050,0x00000007,0x000001a3,0x0000001c,0x0000001c,0x00050083,0x00000007,
|
||||
0x000001a4,0x000001a3,0x000001a2,0x0007000c,0x00000007,0x000001a5,0x00000001,0x00000025,
|
||||
0x000001a1,0x000001a4,0x0003003e,0x000001a0,0x000001a5,0x00050041,0x00000009,0x000001a8,
|
||||
0x000001a0,0x00000024,0x0004003d,0x00000006,0x000001a9,0x000001a8,0x00060041,0x00000025,
|
||||
0x000001aa,0x00000020,0x00000036,0x00000046,0x0004003d,0x00000006,0x000001ab,0x000001aa,
|
||||
0x00050085,0x00000006,0x000001ac,0x000001a9,0x000001ab,0x00050041,0x00000009,0x000001ad,
|
||||
0x000001a0,0x0000002b,0x0004003d,0x00000006,0x000001ae,0x000001ad,0x0007000c,0x00000006,
|
||||
0x000001af,0x00000001,0x00000025,0x000001ac,0x000001ae,0x0008000c,0x00000006,0x000001b0,
|
||||
0x00000001,0x00000031,0x0000006f,0x000001a7,0x000001af,0x0003003e,0x000001a6,0x000001b0,
|
||||
0x0004003d,0x0000000f,0x000001b3,0x00000172,0x0004003d,0x00000006,0x000001b4,0x000001a6,
|
||||
0x0005008e,0x0000000f,0x000001b5,0x000001b3,0x000001b4,0x00050051,0x00000006,0x000001b6,
|
||||
0x000001b5,0x00000000,0x00050051,0x00000006,0x000001b7,0x000001b5,0x00000001,0x00050051,
|
||||
0x00000006,0x000001b8,0x000001b5,0x00000002,0x00070050,0x0000001d,0x000001b9,0x000001b6,
|
||||
0x000001b7,0x000001b8,0x0000001c,0x0003003e,0x000001b2,0x000001b9,0x000100fd,0x00010038,
|
||||
0x00050036,0x00000007,0x0000000d,0x00000000,0x0000000a,0x00030037,0x00000008,0x0000000b,
|
||||
0x00030037,0x00000009,0x0000000c,0x000200f8,0x0000000e,0x0004003b,0x00000009,0x00000015,
|
||||
0x00000007,0x0004003d,0x00000007,0x00000016,0x0000000b,0x0004003d,0x00000007,0x00000017,
|
||||
0x0000000b,0x00050094,0x00000006,0x00000018,0x00000016,0x00000017,0x0003003e,0x00000015,
|
||||
0x00000018,0x0004003d,0x00000007,0x00000019,0x0000000b,0x0004003d,0x00000006,0x0000001a,
|
||||
0x0000000c,0x0005008e,0x00000007,0x0000001b,0x00000019,0x0000001a,0x00060041,0x00000025,
|
||||
0x00000026,0x00000020,0x00000022,0x00000024,0x0004003d,0x00000006,0x00000027,0x00000026,
|
||||
0x0004003d,0x00000006,0x00000028,0x00000015,0x00050085,0x00000006,0x00000029,0x00000027,
|
||||
0x00000028,0x00050081,0x00000006,0x0000002a,0x0000001c,0x00000029,0x00060041,0x00000025,
|
||||
0x0000002c,0x00000020,0x00000022,0x0000002b,0x0004003d,0x00000006,0x0000002d,0x0000002c,
|
||||
0x0004003d,0x00000006,0x0000002e,0x00000015,0x00050085,0x00000006,0x0000002f,0x0000002d,
|
||||
0x0000002e,0x0004003d,0x00000006,0x00000030,0x00000015,0x00050085,0x00000006,0x00000031,
|
||||
0x0000002f,0x00000030,0x00050081,0x00000006,0x00000032,0x0000002a,0x00000031,0x0005008e,
|
||||
0x00000007,0x00000033,0x0000001b,0x00000032,0x000200fe,0x00000033,0x00010038,0x00050036,
|
||||
0x0000000f,0x00000013,0x00000000,0x00000010,0x00030037,0x00000008,0x00000011,0x00030037,
|
||||
0x00000009,0x00000012,0x000200f8,0x00000014,0x0004003b,0x00000009,0x0000003f,0x00000007,
|
||||
0x0004003b,0x00000008,0x00000042,0x00000007,0x0004003b,0x00000008,0x00000059,0x00000007,
|
||||
0x0004003b,0x000000a2,0x000000a3,0x00000007,0x0004003b,0x000000bf,0x000000c0,0x00000007,
|
||||
0x0004003b,0x000000bf,0x000000cb,0x00000007,0x0004003b,0x00000009,0x000000e4,0x00000007,
|
||||
0x0004003b,0x000000bf,0x000000f3,0x00000007,0x0004003b,0x00000009,0x000000f9,0x00000007,
|
||||
0x0004003b,0x00000009,0x000000fc,0x00000007,0x0004003b,0x00000008,0x00000101,0x00000007,
|
||||
0x00060041,0x00000025,0x00000038,0x00000020,0x00000036,0x00000037,0x0004003d,0x00000006,
|
||||
0x00000039,0x00000038,0x000500ba,0x0000003b,0x0000003c,0x00000039,0x0000003a,0x000300f7,
|
||||
0x0000003e,0x00000000,0x000400fa,0x0000003c,0x0000003d,0x0000003e,0x000200f8,0x0000003d,
|
||||
0x00060041,0x00000025,0x00000040,0x00000020,0x00000022,0x00000037,0x0004003d,0x00000006,
|
||||
0x00000041,0x00000040,0x0003003e,0x0000003f,0x00000041,0x00050041,0x00000009,0x00000043,
|
||||
0x00000011,0x00000024,0x0004003d,0x00000006,0x00000044,0x00000043,0x0004003d,0x00000006,
|
||||
0x00000045,0x0000003f,0x00060041,0x00000025,0x00000047,0x00000020,0x00000036,0x00000046,
|
||||
0x0004003d,0x00000006,0x00000048,0x00000047,0x00050085,0x00000006,0x00000049,0x00000045,
|
||||
0x00000048,0x00050088,0x00000006,0x0000004a,0x00000044,0x00000049,0x00050041,0x00000009,
|
||||
0x0000004b,0x00000011,0x0000002b,0x0004003d,0x00000006,0x0000004c,0x0000004b,0x0004003d,
|
||||
0x00000006,0x0000004d,0x0000003f,0x00050088,0x00000006,0x0000004e,0x0000004c,0x0000004d,
|
||||
0x00050050,0x00000007,0x0000004f,0x0000004a,0x0000004e,0x0005008e,0x00000007,0x00000050,
|
||||
0x0000004f,0x0000003a,0x00050050,0x00000007,0x00000051,0x0000003a,0x0000003a,0x00050081,
|
||||
0x00000007,0x00000052,0x00000050,0x00000051,0x0003003e,0x00000042,0x00000052,0x00060041,
|
||||
0x00000025,0x00000053,0x00000020,0x00000036,0x00000037,0x0004003d,0x00000006,0x00000054,
|
||||
0x00000053,0x000500ba,0x0000003b,0x00000056,0x00000054,0x00000055,0x000300f7,0x00000058,
|
||||
0x00000000,0x000400fa,0x00000056,0x00000057,0x00000058,0x000200f8,0x00000057,0x00060041,
|
||||
0x00000025,0x0000005a,0x00000020,0x00000036,0x00000046,0x0004003d,0x00000006,0x0000005b,
|
||||
0x0000005a,0x00060041,0x00000025,0x0000005c,0x00000020,0x00000036,0x0000002b,0x0004003d,
|
||||
0x00000006,0x0000005d,0x0000005c,0x00050088,0x00000006,0x0000005e,0x0000005b,0x0000005d,
|
||||
0x00060041,0x00000025,0x0000005f,0x00000020,0x00000036,0x0000002b,0x0004003d,0x00000006,
|
||||
0x00000060,0x0000005f,0x00060041,0x00000025,0x00000061,0x00000020,0x00000036,0x00000046,
|
||||
0x0004003d,0x00000006,0x00000062,0x00000061,0x00050088,0x00000006,0x00000063,0x00000060,
|
||||
0x00000062,0x00050050,0x00000007,0x00000064,0x0000005e,0x00000063,0x0007000c,0x00000007,
|
||||
0x00000066,0x00000001,0x00000028,0x00000064,0x00000065,0x0003003e,0x00000059,0x00000066,
|
||||
0x0004003d,0x00000007,0x00000067,0x00000042,0x00050050,0x00000007,0x00000068,0x0000003a,
|
||||
0x0000003a,0x00050083,0x00000007,0x00000069,0x00000067,0x00000068,0x0004003d,0x00000007,
|
||||
0x0000006a,0x00000059,0x00050085,0x00000007,0x0000006b,0x00000069,0x0000006a,0x00050050,
|
||||
0x00000007,0x0000006c,0x0000003a,0x0000003a,0x00050081,0x00000007,0x0000006d,0x0000006b,
|
||||
0x0000006c,0x0003003e,0x00000042,0x0000006d,0x0004003d,0x00000007,0x0000006e,0x00000042,
|
||||
0x000500b8,0x00000071,0x00000072,0x0000006e,0x00000070,0x0004009a,0x0000003b,0x00000073,
|
||||
0x00000072,0x000400a8,0x0000003b,0x00000074,0x00000073,0x000300f7,0x00000076,0x00000000,
|
||||
0x000400fa,0x00000074,0x00000075,0x00000076,0x000200f8,0x00000075,0x0004003d,0x00000007,
|
||||
0x00000077,0x00000042,0x000500ba,0x00000071,0x00000078,0x00000077,0x00000065,0x0004009a,
|
||||
0x0000003b,0x00000079,0x00000078,0x000200f9,0x00000076,0x000200f8,0x00000076,0x000700f5,
|
||||
0x0000003b,0x0000007a,0x00000073,0x00000057,0x00000079,0x00000075,0x000300f7,0x0000007c,
|
||||
0x00000000,0x000400fa,0x0000007a,0x0000007b,0x0000007c,0x000200f8,0x0000007b,0x000200fe,
|
||||
0x0000007d,0x000200f8,0x0000007c,0x0004003d,0x00000080,0x00000083,0x00000082,0x0004003d,
|
||||
0x00000007,0x00000084,0x00000042,0x00050057,0x0000001d,0x00000085,0x00000083,0x00000084,
|
||||
0x0008004f,0x0000000f,0x00000086,0x00000085,0x00000085,0x00000000,0x00000001,0x00000002,
|
||||
0x000200fe,0x00000086,0x000200f8,0x00000058,0x0004003d,0x00000007,0x00000088,0x00000042,
|
||||
0x000500b8,0x00000071,0x00000089,0x00000088,0x00000070,0x0004009a,0x0000003b,0x0000008a,
|
||||
0x00000089,0x000400a8,0x0000003b,0x0000008b,0x0000008a,0x000300f7,0x0000008d,0x00000000,
|
||||
0x000400fa,0x0000008b,0x0000008c,0x0000008d,0x000200f8,0x0000008c,0x0004003d,0x00000007,
|
||||
0x0000008e,0x00000042,0x000500ba,0x00000071,0x0000008f,0x0000008e,0x00000065,0x0004009a,
|
||||
0x0000003b,0x00000090,0x0000008f,0x000200f9,0x0000008d,0x000200f8,0x0000008d,0x000700f5,
|
||||
0x0000003b,0x00000091,0x0000008a,0x00000058,0x00000090,0x0000008c,0x000300f7,0x00000093,
|
||||
0x00000000,0x000400fa,0x00000091,0x00000092,0x00000093,0x000200f8,0x00000092,0x000200fe,
|
||||
0x0000007d,0x000200f8,0x00000093,0x0004003d,0x00000080,0x00000095,0x00000082,0x00050041,
|
||||
0x00000009,0x00000096,0x00000042,0x00000024,0x0004003d,0x00000006,0x00000097,0x00000096,
|
||||
0x0004003d,0x00000006,0x00000098,0x00000012,0x00050081,0x00000006,0x00000099,0x00000097,
|
||||
0x00000098,0x00050085,0x00000006,0x0000009a,0x00000099,0x0000003a,0x00050041,0x00000009,
|
||||
0x0000009b,0x00000042,0x0000002b,0x0004003d,0x00000006,0x0000009c,0x0000009b,0x00050050,
|
||||
0x00000007,0x0000009d,0x0000009a,0x0000009c,0x00050057,0x0000001d,0x0000009e,0x00000095,
|
||||
0x0000009d,0x0008004f,0x0000000f,0x0000009f,0x0000009e,0x0000009e,0x00000000,0x00000001,
|
||||
0x00000002,0x000200fe,0x0000009f,0x000200f8,0x0000003e,0x00050041,0x000000a5,0x000000a6,
|
||||
0x00000020,0x000000a4,0x0004003d,0x0000001d,0x000000a7,0x000000a6,0x0008004f,0x0000000f,
|
||||
0x000000a8,0x000000a7,0x000000a7,0x00000000,0x00000001,0x00000002,0x00050041,0x000000a5,
|
||||
0x000000aa,0x00000020,0x000000a9,0x0004003d,0x0000001d,0x000000ab,0x000000aa,0x0008004f,
|
||||
0x0000000f,0x000000ac,0x000000ab,0x000000ab,0x00000000,0x00000001,0x00000002,0x00050041,
|
||||
0x000000a5,0x000000ae,0x00000020,0x000000ad,0x0004003d,0x0000001d,0x000000af,0x000000ae,
|
||||
0x0008004f,0x0000000f,0x000000b0,0x000000af,0x000000af,0x00000000,0x00000001,0x00000002,
|
||||
0x00050051,0x00000006,0x000000b1,0x000000a8,0x00000000,0x00050051,0x00000006,0x000000b2,
|
||||
0x000000a8,0x00000001,0x00050051,0x00000006,0x000000b3,0x000000a8,0x00000002,0x00050051,
|
||||
0x00000006,0x000000b4,0x000000ac,0x00000000,0x00050051,0x00000006,0x000000b5,0x000000ac,
|
||||
0x00000001,0x00050051,0x00000006,0x000000b6,0x000000ac,0x00000002,0x00050051,0x00000006,
|
||||
0x000000b7,0x000000b0,0x00000000,0x00050051,0x00000006,0x000000b8,0x000000b0,0x00000001,
|
||||
0x00050051,0x00000006,0x000000b9,0x000000b0,0x00000002,0x00060050,0x0000000f,0x000000ba,
|
||||
0x000000b1,0x000000b2,0x000000b3,0x00060050,0x0000000f,0x000000bb,0x000000b4,0x000000b5,
|
||||
0x000000b6,0x00060050,0x0000000f,0x000000bc,0x000000b7,0x000000b8,0x000000b9,0x00060050,
|
||||
0x000000a1,0x000000bd,0x000000ba,0x000000bb,0x000000bc,0x00040054,0x000000a1,0x000000be,
|
||||
0x000000bd,0x0003003e,0x000000a3,0x000000be,0x0004003d,0x000000a1,0x000000c1,0x000000a3,
|
||||
0x00050041,0x00000009,0x000000c2,0x00000011,0x00000024,0x0004003d,0x00000006,0x000000c3,
|
||||
0x000000c2,0x00050041,0x00000009,0x000000c4,0x00000011,0x0000002b,0x0004003d,0x00000006,
|
||||
0x000000c5,0x000000c4,0x0004007f,0x00000006,0x000000c6,0x000000c5,0x00060050,0x0000000f,
|
||||
0x000000c8,0x000000c3,0x000000c6,0x000000c7,0x0006000c,0x0000000f,0x000000c9,0x00000001,
|
||||
0x00000045,0x000000c8,0x00050091,0x0000000f,0x000000ca,0x000000c1,0x000000c9,0x0003003e,
|
||||
0x000000c0,0x000000ca,0x00050041,0x000000a5,0x000000cd,0x00000020,0x000000cc,0x0004003d,
|
||||
0x0000001d,0x000000ce,0x000000cd,0x0008004f,0x0000000f,0x000000cf,0x000000ce,0x000000ce,
|
||||
0x00000001,0x00000002,0x00000003,0x0004003d,0x000000a1,0x000000d0,0x000000a3,0x0004003d,
|
||||
0x00000006,0x000000d1,0x00000012,0x000500b8,0x0000003b,0x000000d2,0x000000d1,0x0000003a,
|
||||
0x000600a9,0x00000006,0x000000d3,0x000000d2,0x000000c7,0x0000001c,0x00060041,0x00000025,
|
||||
0x000000d4,0x00000020,0x000000a4,0x00000046,0x0004003d,0x00000006,0x000000d5,0x000000d4,
|
||||
0x00050085,0x00000006,0x000000d6,0x000000d3,0x000000d5,0x00060050,0x0000000f,0x000000d7,
|
||||
0x000000d6,0x0000006f,0x0000006f,0x00050091,0x0000000f,0x000000d8,0x000000d0,0x000000d7,
|
||||
0x00050081,0x0000000f,0x000000d9,0x000000cf,0x000000d8,0x0003003e,0x000000cb,0x000000d9,
|
||||
0x00050041,0x00000009,0x000000da,0x000000c0,0x00000037,0x0004003d,0x00000006,0x000000db,
|
||||
0x000000da,0x000500ba,0x0000003b,0x000000dd,0x000000db,0x000000dc,0x000300f7,0x000000df,
|
||||
0x00000000,0x000400fa,0x000000dd,0x000000de,0x000000df,0x000200f8,0x000000de,0x000200fe,
|
||||
0x000000e2,0x000200f8,0x000000df,0x00060041,0x00000025,0x000000e5,0x00000020,0x000000a9,
|
||||
0x00000046,0x0004003d,0x00000006,0x000000e6,0x000000e5,0x0004007f,0x00000006,0x000000e7,
|
||||
0x000000e6,0x00050041,0x00000009,0x000000e8,0x000000cb,0x00000037,0x0004003d,0x00000006,
|
||||
0x000000e9,0x000000e8,0x00050083,0x00000006,0x000000ea,0x000000e7,0x000000e9,0x00050041,
|
||||
0x00000009,0x000000eb,0x000000c0,0x00000037,0x0004003d,0x00000006,0x000000ec,0x000000eb,
|
||||
0x00050088,0x00000006,0x000000ed,0x000000ea,0x000000ec,0x0003003e,0x000000e4,0x000000ed,
|
||||
0x0004003d,0x00000006,0x000000ee,0x000000e4,0x000500bc,0x0000003b,0x000000ef,0x000000ee,
|
||||
0x0000006f,0x000300f7,0x000000f1,0x00000000,0x000400fa,0x000000ef,0x000000f0,0x000000f1,
|
||||
0x000200f8,0x000000f0,0x000200fe,0x000000e2,0x000200f8,0x000000f1,0x0004003d,0x0000000f,
|
||||
0x000000f4,0x000000cb,0x0004003d,0x0000000f,0x000000f5,0x000000c0,0x0004003d,0x00000006,
|
||||
0x000000f6,0x000000e4,0x0005008e,0x0000000f,0x000000f7,0x000000f5,0x000000f6,0x00050081,
|
||||
0x0000000f,0x000000f8,0x000000f4,0x000000f7,0x0003003e,0x000000f3,0x000000f8,0x00060041,
|
||||
0x00000025,0x000000fa,0x00000020,0x000000ad,0x00000046,0x0004003d,0x00000006,0x000000fb,
|
||||
0x000000fa,0x0003003e,0x000000f9,0x000000fb,0x0004003d,0x00000006,0x000000fd,0x000000f9,
|
||||
0x00060041,0x00000025,0x000000fe,0x00000020,0x00000036,0x0000002b,0x0004003d,0x00000006,
|
||||
0x000000ff,0x000000fe,0x00050088,0x00000006,0x00000100,0x000000fd,0x000000ff,0x0003003e,
|
||||
0x000000fc,0x00000100,0x00050041,0x00000009,0x00000102,0x000000f3,0x00000024,0x0004003d,
|
||||
0x00000006,0x00000103,0x00000102,0x0004003d,0x00000006,0x00000104,0x000000f9,0x00050088,
|
||||
0x00000006,0x00000105,0x00000103,0x00000104,0x00050041,0x00000009,0x00000106,0x000000f3,
|
||||
0x0000002b,0x0004003d,0x00000006,0x00000107,0x00000106,0x0004007f,0x00000006,0x00000108,
|
||||
0x00000107,0x0004003d,0x00000006,0x00000109,0x000000fc,0x00050088,0x00000006,0x0000010a,
|
||||
0x00000108,0x00000109,0x00050050,0x00000007,0x0000010b,0x00000105,0x0000010a,0x0005008e,
|
||||
0x00000007,0x0000010c,0x0000010b,0x0000003a,0x00050050,0x00000007,0x0000010d,0x0000003a,
|
||||
0x0000003a,0x00050081,0x00000007,0x0000010e,0x0000010c,0x0000010d,0x0003003e,0x00000101,
|
||||
0x0000010e,0x0004003d,0x00000007,0x0000010f,0x00000101,0x000500b8,0x00000071,0x00000110,
|
||||
0x0000010f,0x00000070,0x0004009a,0x0000003b,0x00000111,0x00000110,0x000400a8,0x0000003b,
|
||||
0x00000112,0x00000111,0x000300f7,0x00000114,0x00000000,0x000400fa,0x00000112,0x00000113,
|
||||
0x00000114,0x000200f8,0x00000113,0x0004003d,0x00000007,0x00000115,0x00000101,0x000500ba,
|
||||
0x00000071,0x00000116,0x00000115,0x00000065,0x0004009a,0x0000003b,0x00000117,0x00000116,
|
||||
0x000200f9,0x00000114,0x000200f8,0x00000114,0x000700f5,0x0000003b,0x00000118,0x00000111,
|
||||
0x000000f1,0x00000117,0x00000113,0x000300f7,0x0000011a,0x00000000,0x000400fa,0x00000118,
|
||||
0x00000119,0x0000011a,0x000200f8,0x00000119,0x000200fe,0x000000e2,0x000200f8,0x0000011a,
|
||||
0x0004003d,0x00000080,0x0000011c,0x00000082,0x0004003d,0x00000007,0x0000011d,0x00000101,
|
||||
0x00050057,0x0000001d,0x0000011e,0x0000011c,0x0000011d,0x0008004f,0x0000000f,0x0000011f,
|
||||
0x0000011e,0x0000011e,0x00000000,0x00000001,0x00000002,0x000200fe,0x0000011f,0x00010038
|
||||
0x736e656c,0x00000000,0x00050006,0x0000001e,0x00000005,0x77656976,0x00000000,0x00030005,
|
||||
0x00000020,0x00006370,0x00040005,0x0000003f,0x486e6174,0x00000000,0x00030005,0x00000042,
|
||||
0x00000073,0x00040005,0x00000068,0x72756f53,0x00006563,0x00030005,0x00000077,0x00000052,
|
||||
0x00030005,0x00000094,0x00000064,0x00030005,0x0000009f,0x0000006f,0x00030005,0x000000b3,
|
||||
0x00000074,0x00030005,0x000000bd,0x00746968,0x00040005,0x000000c3,0x666c6168,0x00000057,
|
||||
0x00040005,0x000000c6,0x666c6168,0x00000048,0x00030005,0x000000cb,0x00000073,0x00030005,
|
||||
0x000000ec,0x00657965,0x00030005,0x000000ee,0x00007675,0x00040005,0x000000f4,0x61636f6c,
|
||||
0x0000006c,0x00030005,0x000000fe,0x00007863,0x00030005,0x0000010c,0x00000070,0x00040005,
|
||||
0x00000111,0x486e6174,0x00000000,0x00030005,0x00000114,0x00000074,0x00030005,0x00000125,
|
||||
0x00000063,0x00040005,0x00000129,0x61726170,0x0000006d,0x00040005,0x0000012b,0x61726170,
|
||||
0x0000006d,0x00040005,0x0000012d,0x61726170,0x0000006d,0x00040005,0x0000012e,0x61726170,
|
||||
0x0000006d,0x00040005,0x00000133,0x61726170,0x0000006d,0x00040005,0x00000135,0x61726170,
|
||||
0x0000006d,0x00040005,0x00000137,0x61726170,0x0000006d,0x00040005,0x00000138,0x61726170,
|
||||
0x0000006d,0x00040005,0x00000140,0x61726170,0x0000006d,0x00040005,0x00000142,0x61726170,
|
||||
0x0000006d,0x00040005,0x00000144,0x61726170,0x0000006d,0x00040005,0x00000145,0x61726170,
|
||||
0x0000006d,0x00040005,0x0000014b,0x61726170,0x0000006d,0x00040005,0x0000014d,0x61726170,
|
||||
0x0000006d,0x00040005,0x0000014f,0x61726170,0x0000006d,0x00040005,0x00000150,0x61726170,
|
||||
0x0000006d,0x00030005,0x00000153,0x00000065,0x00040005,0x00000159,0x65646166,0x00000000,
|
||||
0x00050005,0x00000165,0x4374756f,0x726f6c6f,0x00000000,0x00030047,0x0000001e,0x00000002,
|
||||
0x00050048,0x0000001e,0x00000000,0x00000023,0x00000000,0x00050048,0x0000001e,0x00000001,
|
||||
0x00000023,0x00000010,0x00050048,0x0000001e,0x00000002,0x00000023,0x00000020,0x00050048,
|
||||
0x0000001e,0x00000003,0x00000023,0x00000030,0x00050048,0x0000001e,0x00000004,0x00000023,
|
||||
0x00000040,0x00050048,0x0000001e,0x00000005,0x00000023,0x00000050,0x00040047,0x00000068,
|
||||
0x00000021,0x00000000,0x00040047,0x00000068,0x00000022,0x00000000,0x00040047,0x000000ee,
|
||||
0x0000001e,0x00000000,0x00040047,0x00000165,0x0000001e,0x00000000,0x00020013,0x00000002,
|
||||
0x00030021,0x00000003,0x00000002,0x00030016,0x00000006,0x00000020,0x00040017,0x00000007,
|
||||
0x00000006,0x00000002,0x00040020,0x00000008,0x00000007,0x00000007,0x00040020,0x00000009,
|
||||
0x00000007,0x00000006,0x00050021,0x0000000a,0x00000007,0x00000008,0x00000009,0x00040017,
|
||||
0x0000000f,0x00000006,0x00000003,0x00050021,0x00000010,0x0000000f,0x00000008,0x00000009,
|
||||
0x0004002b,0x00000006,0x0000001c,0x3f800000,0x00040017,0x0000001d,0x00000006,0x00000004,
|
||||
0x0008001e,0x0000001e,0x0000001d,0x0000001d,0x0000001d,0x0000001d,0x0000001d,0x0000001d,
|
||||
0x00040020,0x0000001f,0x00000009,0x0000001e,0x0004003b,0x0000001f,0x00000020,0x00000009,
|
||||
0x00040015,0x00000021,0x00000020,0x00000001,0x0004002b,0x00000021,0x00000022,0x00000004,
|
||||
0x00040015,0x00000023,0x00000020,0x00000000,0x0004002b,0x00000023,0x00000024,0x00000000,
|
||||
0x00040020,0x00000025,0x00000009,0x00000006,0x0004002b,0x00000023,0x0000002b,0x00000001,
|
||||
0x0004002b,0x00000021,0x00000036,0x00000005,0x0004002b,0x00000023,0x00000037,0x00000002,
|
||||
0x0004002b,0x00000006,0x0000003a,0x3f000000,0x00020014,0x0000003b,0x0004002b,0x00000023,
|
||||
0x00000046,0x00000003,0x0004002b,0x00000006,0x00000054,0x00000000,0x0005002c,0x00000007,
|
||||
0x00000055,0x00000054,0x00000054,0x00040017,0x00000056,0x0000003b,0x00000002,0x0005002c,
|
||||
0x00000007,0x0000005d,0x0000001c,0x0000001c,0x0006002c,0x0000000f,0x00000063,0x00000054,
|
||||
0x00000054,0x00000054,0x00090019,0x00000065,0x00000006,0x00000001,0x00000000,0x00000000,
|
||||
0x00000000,0x00000001,0x00000000,0x0003001b,0x00000066,0x00000065,0x00040020,0x00000067,
|
||||
0x00000000,0x00000066,0x0004003b,0x00000067,0x00000068,0x00000000,0x00040018,0x00000075,
|
||||
0x0000000f,0x00000003,0x00040020,0x00000076,0x00000007,0x00000075,0x0004002b,0x00000021,
|
||||
0x00000078,0x00000001,0x00040020,0x00000079,0x00000009,0x0000001d,0x0004002b,0x00000021,
|
||||
0x0000007d,0x00000002,0x0004002b,0x00000021,0x00000081,0x00000003,0x00040020,0x00000093,
|
||||
0x00000007,0x0000000f,0x0004002b,0x00000006,0x0000009b,0xbf800000,0x0004002b,0x00000006,
|
||||
0x000000ab,0xb8d1b717,0x0004002b,0x00000006,0x000000af,0x3c75c28f,0x0004002b,0x00000006,
|
||||
0x000000b0,0x3ca3d70a,0x0006002c,0x0000000f,0x000000b1,0x000000af,0x000000af,0x000000b0,
|
||||
0x00040020,0x000000ed,0x00000001,0x00000007,0x0004003b,0x000000ed,0x000000ee,0x00000001,
|
||||
0x00040020,0x000000ef,0x00000001,0x00000006,0x0004002b,0x00000006,0x000000f7,0x40000000,
|
||||
0x0004002b,0x00000006,0x0000015a,0x3cf5c28f,0x00040020,0x00000164,0x00000003,0x0000001d,
|
||||
0x0004003b,0x00000164,0x00000165,0x00000003,0x00050036,0x00000002,0x00000004,0x00000000,
|
||||
0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000009,0x000000ec,0x00000007,0x0004003b,
|
||||
0x00000008,0x000000f4,0x00000007,0x0004003b,0x00000009,0x000000fe,0x00000007,0x0004003b,
|
||||
0x00000009,0x00000101,0x00000007,0x0004003b,0x00000008,0x0000010c,0x00000007,0x0004003b,
|
||||
0x00000009,0x00000111,0x00000007,0x0004003b,0x00000008,0x00000114,0x00000007,0x0004003b,
|
||||
0x00000093,0x00000125,0x00000007,0x0004003b,0x00000008,0x00000129,0x00000007,0x0004003b,
|
||||
0x00000009,0x0000012b,0x00000007,0x0004003b,0x00000008,0x0000012d,0x00000007,0x0004003b,
|
||||
0x00000009,0x0000012e,0x00000007,0x0004003b,0x00000008,0x00000133,0x00000007,0x0004003b,
|
||||
0x00000009,0x00000135,0x00000007,0x0004003b,0x00000008,0x00000137,0x00000007,0x0004003b,
|
||||
0x00000009,0x00000138,0x00000007,0x0004003b,0x00000008,0x00000140,0x00000007,0x0004003b,
|
||||
0x00000009,0x00000142,0x00000007,0x0004003b,0x00000008,0x00000144,0x00000007,0x0004003b,
|
||||
0x00000009,0x00000145,0x00000007,0x0004003b,0x00000008,0x0000014b,0x00000007,0x0004003b,
|
||||
0x00000009,0x0000014d,0x00000007,0x0004003b,0x00000008,0x0000014f,0x00000007,0x0004003b,
|
||||
0x00000009,0x00000150,0x00000007,0x0004003b,0x00000008,0x00000153,0x00000007,0x0004003b,
|
||||
0x00000009,0x00000159,0x00000007,0x00050041,0x000000ef,0x000000f0,0x000000ee,0x00000024,
|
||||
0x0004003d,0x00000006,0x000000f1,0x000000f0,0x000500b8,0x0000003b,0x000000f2,0x000000f1,
|
||||
0x0000003a,0x000600a9,0x00000006,0x000000f3,0x000000f2,0x00000054,0x0000001c,0x0003003e,
|
||||
0x000000ec,0x000000f3,0x00050041,0x000000ef,0x000000f5,0x000000ee,0x00000024,0x0004003d,
|
||||
0x00000006,0x000000f6,0x000000f5,0x00050085,0x00000006,0x000000f8,0x000000f6,0x000000f7,
|
||||
0x0004003d,0x00000006,0x000000f9,0x000000ec,0x00050083,0x00000006,0x000000fa,0x000000f8,
|
||||
0x000000f9,0x00050041,0x000000ef,0x000000fb,0x000000ee,0x0000002b,0x0004003d,0x00000006,
|
||||
0x000000fc,0x000000fb,0x00050050,0x00000007,0x000000fd,0x000000fa,0x000000fc,0x0003003e,
|
||||
0x000000f4,0x000000fd,0x0004003d,0x00000006,0x000000ff,0x000000ec,0x000500b8,0x0000003b,
|
||||
0x00000100,0x000000ff,0x0000003a,0x000300f7,0x00000103,0x00000000,0x000400fa,0x00000100,
|
||||
0x00000102,0x00000106,0x000200f8,0x00000102,0x00060041,0x00000025,0x00000104,0x00000020,
|
||||
0x00000036,0x00000024,0x0004003d,0x00000006,0x00000105,0x00000104,0x0003003e,0x00000101,
|
||||
0x00000105,0x000200f9,0x00000103,0x000200f8,0x00000106,0x00060041,0x00000025,0x00000107,
|
||||
0x00000020,0x00000036,0x00000024,0x0004003d,0x00000006,0x00000108,0x00000107,0x0004007f,
|
||||
0x00000006,0x00000109,0x00000108,0x0003003e,0x00000101,0x00000109,0x000200f9,0x00000103,
|
||||
0x000200f8,0x00000103,0x0004003d,0x00000006,0x0000010a,0x00000101,0x00050081,0x00000006,
|
||||
0x0000010b,0x0000003a,0x0000010a,0x0003003e,0x000000fe,0x0000010b,0x0004003d,0x00000007,
|
||||
0x0000010d,0x000000f4,0x0004003d,0x00000006,0x0000010e,0x000000fe,0x00050050,0x00000007,
|
||||
0x0000010f,0x0000010e,0x0000003a,0x00050083,0x00000007,0x00000110,0x0000010d,0x0000010f,
|
||||
0x0003003e,0x0000010c,0x00000110,0x00060041,0x00000025,0x00000112,0x00000020,0x00000022,
|
||||
0x00000037,0x0004003d,0x00000006,0x00000113,0x00000112,0x0003003e,0x00000111,0x00000113,
|
||||
0x00050041,0x00000009,0x00000115,0x0000010c,0x00000024,0x0004003d,0x00000006,0x00000116,
|
||||
0x00000115,0x00060041,0x00000025,0x00000117,0x00000020,0x00000036,0x00000046,0x0004003d,
|
||||
0x00000006,0x00000118,0x00000117,0x00050085,0x00000006,0x00000119,0x00000116,0x00000118,
|
||||
0x00050041,0x00000009,0x0000011a,0x0000010c,0x0000002b,0x0004003d,0x00000006,0x0000011b,
|
||||
0x0000011a,0x00050050,0x00000007,0x0000011c,0x00000119,0x0000011b,0x0005008e,0x00000007,
|
||||
0x0000011d,0x0000011c,0x000000f7,0x0004003d,0x00000006,0x0000011e,0x00000111,0x0005008e,
|
||||
0x00000007,0x0000011f,0x0000011d,0x0000011e,0x0003003e,0x00000114,0x0000011f,0x00060041,
|
||||
0x00000025,0x00000120,0x00000020,0x00000022,0x00000046,0x0004003d,0x00000006,0x00000121,
|
||||
0x00000120,0x000500ba,0x0000003b,0x00000122,0x00000121,0x00000054,0x000300f7,0x00000124,
|
||||
0x00000000,0x000400fa,0x00000122,0x00000123,0x0000014a,0x000200f8,0x00000123,0x00060041,
|
||||
0x00000025,0x00000126,0x00000020,0x00000022,0x00000046,0x0004003d,0x00000006,0x00000127,
|
||||
0x00000126,0x00050083,0x00000006,0x00000128,0x0000001c,0x00000127,0x0004003d,0x00000007,
|
||||
0x0000012a,0x00000114,0x0003003e,0x00000129,0x0000012a,0x0003003e,0x0000012b,0x00000128,
|
||||
0x00060039,0x00000007,0x0000012c,0x0000000d,0x00000129,0x0000012b,0x0003003e,0x0000012d,
|
||||
0x0000012c,0x0004003d,0x00000006,0x0000012f,0x000000ec,0x0003003e,0x0000012e,0x0000012f,
|
||||
0x00060039,0x0000000f,0x00000130,0x00000013,0x0000012d,0x0000012e,0x00050051,0x00000006,
|
||||
0x00000131,0x00000130,0x00000000,0x00050041,0x00000009,0x00000132,0x00000125,0x00000024,
|
||||
0x0003003e,0x00000132,0x00000131,0x0004003d,0x00000007,0x00000134,0x00000114,0x0003003e,
|
||||
0x00000133,0x00000134,0x0003003e,0x00000135,0x0000001c,0x00060039,0x00000007,0x00000136,
|
||||
0x0000000d,0x00000133,0x00000135,0x0003003e,0x00000137,0x00000136,0x0004003d,0x00000006,
|
||||
0x00000139,0x000000ec,0x0003003e,0x00000138,0x00000139,0x00060039,0x0000000f,0x0000013a,
|
||||
0x00000013,0x00000137,0x00000138,0x00050051,0x00000006,0x0000013b,0x0000013a,0x00000001,
|
||||
0x00050041,0x00000009,0x0000013c,0x00000125,0x0000002b,0x0003003e,0x0000013c,0x0000013b,
|
||||
0x00060041,0x00000025,0x0000013d,0x00000020,0x00000022,0x00000046,0x0004003d,0x00000006,
|
||||
0x0000013e,0x0000013d,0x00050081,0x00000006,0x0000013f,0x0000001c,0x0000013e,0x0004003d,
|
||||
0x00000007,0x00000141,0x00000114,0x0003003e,0x00000140,0x00000141,0x0003003e,0x00000142,
|
||||
0x0000013f,0x00060039,0x00000007,0x00000143,0x0000000d,0x00000140,0x00000142,0x0003003e,
|
||||
0x00000144,0x00000143,0x0004003d,0x00000006,0x00000146,0x000000ec,0x0003003e,0x00000145,
|
||||
0x00000146,0x00060039,0x0000000f,0x00000147,0x00000013,0x00000144,0x00000145,0x00050051,
|
||||
0x00000006,0x00000148,0x00000147,0x00000002,0x00050041,0x00000009,0x00000149,0x00000125,
|
||||
0x00000037,0x0003003e,0x00000149,0x00000148,0x000200f9,0x00000124,0x000200f8,0x0000014a,
|
||||
0x0004003d,0x00000007,0x0000014c,0x00000114,0x0003003e,0x0000014b,0x0000014c,0x0003003e,
|
||||
0x0000014d,0x0000001c,0x00060039,0x00000007,0x0000014e,0x0000000d,0x0000014b,0x0000014d,
|
||||
0x0003003e,0x0000014f,0x0000014e,0x0004003d,0x00000006,0x00000151,0x000000ec,0x0003003e,
|
||||
0x00000150,0x00000151,0x00060039,0x0000000f,0x00000152,0x00000013,0x0000014f,0x00000150,
|
||||
0x0003003e,0x00000125,0x00000152,0x000200f9,0x00000124,0x000200f8,0x00000124,0x0004003d,
|
||||
0x00000007,0x00000154,0x000000f4,0x0004003d,0x00000007,0x00000155,0x000000f4,0x00050050,
|
||||
0x00000007,0x00000156,0x0000001c,0x0000001c,0x00050083,0x00000007,0x00000157,0x00000156,
|
||||
0x00000155,0x0007000c,0x00000007,0x00000158,0x00000001,0x00000025,0x00000154,0x00000157,
|
||||
0x0003003e,0x00000153,0x00000158,0x00050041,0x00000009,0x0000015b,0x00000153,0x00000024,
|
||||
0x0004003d,0x00000006,0x0000015c,0x0000015b,0x00060041,0x00000025,0x0000015d,0x00000020,
|
||||
0x00000036,0x00000046,0x0004003d,0x00000006,0x0000015e,0x0000015d,0x00050085,0x00000006,
|
||||
0x0000015f,0x0000015c,0x0000015e,0x00050041,0x00000009,0x00000160,0x00000153,0x0000002b,
|
||||
0x0004003d,0x00000006,0x00000161,0x00000160,0x0007000c,0x00000006,0x00000162,0x00000001,
|
||||
0x00000025,0x0000015f,0x00000161,0x0008000c,0x00000006,0x00000163,0x00000001,0x00000031,
|
||||
0x00000054,0x0000015a,0x00000162,0x0003003e,0x00000159,0x00000163,0x0004003d,0x0000000f,
|
||||
0x00000166,0x00000125,0x0004003d,0x00000006,0x00000167,0x00000159,0x0005008e,0x0000000f,
|
||||
0x00000168,0x00000166,0x00000167,0x00050051,0x00000006,0x00000169,0x00000168,0x00000000,
|
||||
0x00050051,0x00000006,0x0000016a,0x00000168,0x00000001,0x00050051,0x00000006,0x0000016b,
|
||||
0x00000168,0x00000002,0x00070050,0x0000001d,0x0000016c,0x00000169,0x0000016a,0x0000016b,
|
||||
0x0000001c,0x0003003e,0x00000165,0x0000016c,0x000100fd,0x00010038,0x00050036,0x00000007,
|
||||
0x0000000d,0x00000000,0x0000000a,0x00030037,0x00000008,0x0000000b,0x00030037,0x00000009,
|
||||
0x0000000c,0x000200f8,0x0000000e,0x0004003b,0x00000009,0x00000015,0x00000007,0x0004003d,
|
||||
0x00000007,0x00000016,0x0000000b,0x0004003d,0x00000007,0x00000017,0x0000000b,0x00050094,
|
||||
0x00000006,0x00000018,0x00000016,0x00000017,0x0003003e,0x00000015,0x00000018,0x0004003d,
|
||||
0x00000007,0x00000019,0x0000000b,0x0004003d,0x00000006,0x0000001a,0x0000000c,0x0005008e,
|
||||
0x00000007,0x0000001b,0x00000019,0x0000001a,0x00060041,0x00000025,0x00000026,0x00000020,
|
||||
0x00000022,0x00000024,0x0004003d,0x00000006,0x00000027,0x00000026,0x0004003d,0x00000006,
|
||||
0x00000028,0x00000015,0x00050085,0x00000006,0x00000029,0x00000027,0x00000028,0x00050081,
|
||||
0x00000006,0x0000002a,0x0000001c,0x00000029,0x00060041,0x00000025,0x0000002c,0x00000020,
|
||||
0x00000022,0x0000002b,0x0004003d,0x00000006,0x0000002d,0x0000002c,0x0004003d,0x00000006,
|
||||
0x0000002e,0x00000015,0x00050085,0x00000006,0x0000002f,0x0000002d,0x0000002e,0x0004003d,
|
||||
0x00000006,0x00000030,0x00000015,0x00050085,0x00000006,0x00000031,0x0000002f,0x00000030,
|
||||
0x00050081,0x00000006,0x00000032,0x0000002a,0x00000031,0x0005008e,0x00000007,0x00000033,
|
||||
0x0000001b,0x00000032,0x000200fe,0x00000033,0x00010038,0x00050036,0x0000000f,0x00000013,
|
||||
0x00000000,0x00000010,0x00030037,0x00000008,0x00000011,0x00030037,0x00000009,0x00000012,
|
||||
0x000200f8,0x00000014,0x0004003b,0x00000009,0x0000003f,0x00000007,0x0004003b,0x00000008,
|
||||
0x00000042,0x00000007,0x0004003b,0x00000076,0x00000077,0x00000007,0x0004003b,0x00000093,
|
||||
0x00000094,0x00000007,0x0004003b,0x00000093,0x0000009f,0x00000007,0x0004003b,0x00000009,
|
||||
0x000000b3,0x00000007,0x0004003b,0x00000093,0x000000bd,0x00000007,0x0004003b,0x00000009,
|
||||
0x000000c3,0x00000007,0x0004003b,0x00000009,0x000000c6,0x00000007,0x0004003b,0x00000008,
|
||||
0x000000cb,0x00000007,0x00060041,0x00000025,0x00000038,0x00000020,0x00000036,0x00000037,
|
||||
0x0004003d,0x00000006,0x00000039,0x00000038,0x000500ba,0x0000003b,0x0000003c,0x00000039,
|
||||
0x0000003a,0x000300f7,0x0000003e,0x00000000,0x000400fa,0x0000003c,0x0000003d,0x0000003e,
|
||||
0x000200f8,0x0000003d,0x00060041,0x00000025,0x00000040,0x00000020,0x00000022,0x00000037,
|
||||
0x0004003d,0x00000006,0x00000041,0x00000040,0x0003003e,0x0000003f,0x00000041,0x00050041,
|
||||
0x00000009,0x00000043,0x00000011,0x00000024,0x0004003d,0x00000006,0x00000044,0x00000043,
|
||||
0x0004003d,0x00000006,0x00000045,0x0000003f,0x00060041,0x00000025,0x00000047,0x00000020,
|
||||
0x00000036,0x00000046,0x0004003d,0x00000006,0x00000048,0x00000047,0x00050085,0x00000006,
|
||||
0x00000049,0x00000045,0x00000048,0x00050088,0x00000006,0x0000004a,0x00000044,0x00000049,
|
||||
0x00050041,0x00000009,0x0000004b,0x00000011,0x0000002b,0x0004003d,0x00000006,0x0000004c,
|
||||
0x0000004b,0x0004003d,0x00000006,0x0000004d,0x0000003f,0x00050088,0x00000006,0x0000004e,
|
||||
0x0000004c,0x0000004d,0x00050050,0x00000007,0x0000004f,0x0000004a,0x0000004e,0x0005008e,
|
||||
0x00000007,0x00000050,0x0000004f,0x0000003a,0x00050050,0x00000007,0x00000051,0x0000003a,
|
||||
0x0000003a,0x00050081,0x00000007,0x00000052,0x00000050,0x00000051,0x0003003e,0x00000042,
|
||||
0x00000052,0x0004003d,0x00000007,0x00000053,0x00000042,0x000500b8,0x00000056,0x00000057,
|
||||
0x00000053,0x00000055,0x0004009a,0x0000003b,0x00000058,0x00000057,0x000400a8,0x0000003b,
|
||||
0x00000059,0x00000058,0x000300f7,0x0000005b,0x00000000,0x000400fa,0x00000059,0x0000005a,
|
||||
0x0000005b,0x000200f8,0x0000005a,0x0004003d,0x00000007,0x0000005c,0x00000042,0x000500ba,
|
||||
0x00000056,0x0000005e,0x0000005c,0x0000005d,0x0004009a,0x0000003b,0x0000005f,0x0000005e,
|
||||
0x000200f9,0x0000005b,0x000200f8,0x0000005b,0x000700f5,0x0000003b,0x00000060,0x00000058,
|
||||
0x0000003d,0x0000005f,0x0000005a,0x000300f7,0x00000062,0x00000000,0x000400fa,0x00000060,
|
||||
0x00000061,0x00000062,0x000200f8,0x00000061,0x000200fe,0x00000063,0x000200f8,0x00000062,
|
||||
0x0004003d,0x00000066,0x00000069,0x00000068,0x00050041,0x00000009,0x0000006a,0x00000042,
|
||||
0x00000024,0x0004003d,0x00000006,0x0000006b,0x0000006a,0x0004003d,0x00000006,0x0000006c,
|
||||
0x00000012,0x00050081,0x00000006,0x0000006d,0x0000006b,0x0000006c,0x00050085,0x00000006,
|
||||
0x0000006e,0x0000006d,0x0000003a,0x00050041,0x00000009,0x0000006f,0x00000042,0x0000002b,
|
||||
0x0004003d,0x00000006,0x00000070,0x0000006f,0x00050050,0x00000007,0x00000071,0x0000006e,
|
||||
0x00000070,0x00050057,0x0000001d,0x00000072,0x00000069,0x00000071,0x0008004f,0x0000000f,
|
||||
0x00000073,0x00000072,0x00000072,0x00000000,0x00000001,0x00000002,0x000200fe,0x00000073,
|
||||
0x000200f8,0x0000003e,0x00050041,0x00000079,0x0000007a,0x00000020,0x00000078,0x0004003d,
|
||||
0x0000001d,0x0000007b,0x0000007a,0x0008004f,0x0000000f,0x0000007c,0x0000007b,0x0000007b,
|
||||
0x00000000,0x00000001,0x00000002,0x00050041,0x00000079,0x0000007e,0x00000020,0x0000007d,
|
||||
0x0004003d,0x0000001d,0x0000007f,0x0000007e,0x0008004f,0x0000000f,0x00000080,0x0000007f,
|
||||
0x0000007f,0x00000000,0x00000001,0x00000002,0x00050041,0x00000079,0x00000082,0x00000020,
|
||||
0x00000081,0x0004003d,0x0000001d,0x00000083,0x00000082,0x0008004f,0x0000000f,0x00000084,
|
||||
0x00000083,0x00000083,0x00000000,0x00000001,0x00000002,0x00050051,0x00000006,0x00000085,
|
||||
0x0000007c,0x00000000,0x00050051,0x00000006,0x00000086,0x0000007c,0x00000001,0x00050051,
|
||||
0x00000006,0x00000087,0x0000007c,0x00000002,0x00050051,0x00000006,0x00000088,0x00000080,
|
||||
0x00000000,0x00050051,0x00000006,0x00000089,0x00000080,0x00000001,0x00050051,0x00000006,
|
||||
0x0000008a,0x00000080,0x00000002,0x00050051,0x00000006,0x0000008b,0x00000084,0x00000000,
|
||||
0x00050051,0x00000006,0x0000008c,0x00000084,0x00000001,0x00050051,0x00000006,0x0000008d,
|
||||
0x00000084,0x00000002,0x00060050,0x0000000f,0x0000008e,0x00000085,0x00000086,0x00000087,
|
||||
0x00060050,0x0000000f,0x0000008f,0x00000088,0x00000089,0x0000008a,0x00060050,0x0000000f,
|
||||
0x00000090,0x0000008b,0x0000008c,0x0000008d,0x00060050,0x00000075,0x00000091,0x0000008e,
|
||||
0x0000008f,0x00000090,0x00040054,0x00000075,0x00000092,0x00000091,0x0003003e,0x00000077,
|
||||
0x00000092,0x0004003d,0x00000075,0x00000095,0x00000077,0x00050041,0x00000009,0x00000096,
|
||||
0x00000011,0x00000024,0x0004003d,0x00000006,0x00000097,0x00000096,0x00050041,0x00000009,
|
||||
0x00000098,0x00000011,0x0000002b,0x0004003d,0x00000006,0x00000099,0x00000098,0x0004007f,
|
||||
0x00000006,0x0000009a,0x00000099,0x00060050,0x0000000f,0x0000009c,0x00000097,0x0000009a,
|
||||
0x0000009b,0x0006000c,0x0000000f,0x0000009d,0x00000001,0x00000045,0x0000009c,0x00050091,
|
||||
0x0000000f,0x0000009e,0x00000095,0x0000009d,0x0003003e,0x00000094,0x0000009e,0x0004003d,
|
||||
0x00000075,0x000000a0,0x00000077,0x0004003d,0x00000006,0x000000a1,0x00000012,0x000500b8,
|
||||
0x0000003b,0x000000a2,0x000000a1,0x0000003a,0x000600a9,0x00000006,0x000000a3,0x000000a2,
|
||||
0x0000009b,0x0000001c,0x00060041,0x00000025,0x000000a4,0x00000020,0x00000078,0x00000046,
|
||||
0x0004003d,0x00000006,0x000000a5,0x000000a4,0x00050085,0x00000006,0x000000a6,0x000000a3,
|
||||
0x000000a5,0x00060050,0x0000000f,0x000000a7,0x000000a6,0x00000054,0x00000054,0x00050091,
|
||||
0x0000000f,0x000000a8,0x000000a0,0x000000a7,0x0003003e,0x0000009f,0x000000a8,0x00050041,
|
||||
0x00000009,0x000000a9,0x00000094,0x00000037,0x0004003d,0x00000006,0x000000aa,0x000000a9,
|
||||
0x000500ba,0x0000003b,0x000000ac,0x000000aa,0x000000ab,0x000300f7,0x000000ae,0x00000000,
|
||||
0x000400fa,0x000000ac,0x000000ad,0x000000ae,0x000200f8,0x000000ad,0x000200fe,0x000000b1,
|
||||
0x000200f8,0x000000ae,0x00060041,0x00000025,0x000000b4,0x00000020,0x0000007d,0x00000046,
|
||||
0x0004003d,0x00000006,0x000000b5,0x000000b4,0x0004007f,0x00000006,0x000000b6,0x000000b5,
|
||||
0x00050041,0x00000009,0x000000b7,0x0000009f,0x00000037,0x0004003d,0x00000006,0x000000b8,
|
||||
0x000000b7,0x00050083,0x00000006,0x000000b9,0x000000b6,0x000000b8,0x00050041,0x00000009,
|
||||
0x000000ba,0x00000094,0x00000037,0x0004003d,0x00000006,0x000000bb,0x000000ba,0x00050088,
|
||||
0x00000006,0x000000bc,0x000000b9,0x000000bb,0x0003003e,0x000000b3,0x000000bc,0x0004003d,
|
||||
0x0000000f,0x000000be,0x0000009f,0x0004003d,0x0000000f,0x000000bf,0x00000094,0x0004003d,
|
||||
0x00000006,0x000000c0,0x000000b3,0x0005008e,0x0000000f,0x000000c1,0x000000bf,0x000000c0,
|
||||
0x00050081,0x0000000f,0x000000c2,0x000000be,0x000000c1,0x0003003e,0x000000bd,0x000000c2,
|
||||
0x00060041,0x00000025,0x000000c4,0x00000020,0x00000081,0x00000046,0x0004003d,0x00000006,
|
||||
0x000000c5,0x000000c4,0x0003003e,0x000000c3,0x000000c5,0x0004003d,0x00000006,0x000000c7,
|
||||
0x000000c3,0x00060041,0x00000025,0x000000c8,0x00000020,0x00000036,0x0000002b,0x0004003d,
|
||||
0x00000006,0x000000c9,0x000000c8,0x00050088,0x00000006,0x000000ca,0x000000c7,0x000000c9,
|
||||
0x0003003e,0x000000c6,0x000000ca,0x00050041,0x00000009,0x000000cc,0x000000bd,0x00000024,
|
||||
0x0004003d,0x00000006,0x000000cd,0x000000cc,0x0004003d,0x00000006,0x000000ce,0x000000c3,
|
||||
0x00050088,0x00000006,0x000000cf,0x000000cd,0x000000ce,0x00050041,0x00000009,0x000000d0,
|
||||
0x000000bd,0x0000002b,0x0004003d,0x00000006,0x000000d1,0x000000d0,0x0004007f,0x00000006,
|
||||
0x000000d2,0x000000d1,0x0004003d,0x00000006,0x000000d3,0x000000c6,0x00050088,0x00000006,
|
||||
0x000000d4,0x000000d2,0x000000d3,0x00050050,0x00000007,0x000000d5,0x000000cf,0x000000d4,
|
||||
0x0005008e,0x00000007,0x000000d6,0x000000d5,0x0000003a,0x00050050,0x00000007,0x000000d7,
|
||||
0x0000003a,0x0000003a,0x00050081,0x00000007,0x000000d8,0x000000d6,0x000000d7,0x0003003e,
|
||||
0x000000cb,0x000000d8,0x0004003d,0x00000007,0x000000d9,0x000000cb,0x000500b8,0x00000056,
|
||||
0x000000da,0x000000d9,0x00000055,0x0004009a,0x0000003b,0x000000db,0x000000da,0x000400a8,
|
||||
0x0000003b,0x000000dc,0x000000db,0x000300f7,0x000000de,0x00000000,0x000400fa,0x000000dc,
|
||||
0x000000dd,0x000000de,0x000200f8,0x000000dd,0x0004003d,0x00000007,0x000000df,0x000000cb,
|
||||
0x000500ba,0x00000056,0x000000e0,0x000000df,0x0000005d,0x0004009a,0x0000003b,0x000000e1,
|
||||
0x000000e0,0x000200f9,0x000000de,0x000200f8,0x000000de,0x000700f5,0x0000003b,0x000000e2,
|
||||
0x000000db,0x000000ae,0x000000e1,0x000000dd,0x000300f7,0x000000e4,0x00000000,0x000400fa,
|
||||
0x000000e2,0x000000e3,0x000000e4,0x000200f8,0x000000e3,0x000200fe,0x000000b1,0x000200f8,
|
||||
0x000000e4,0x0004003d,0x00000066,0x000000e6,0x00000068,0x0004003d,0x00000007,0x000000e7,
|
||||
0x000000cb,0x00050057,0x0000001d,0x000000e8,0x000000e6,0x000000e7,0x0008004f,0x0000000f,
|
||||
0x000000e9,0x000000e8,0x000000e8,0x00000000,0x00000001,0x00000002,0x000200fe,0x000000e9,
|
||||
0x00010038
|
||||
};
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
#include <stdatomic.h>
|
||||
#include <fcntl.h>
|
||||
#include <time.h>
|
||||
#include <stdarg.h>
|
||||
@@ -218,8 +217,6 @@ 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;
|
||||
|
||||
@@ -274,9 +271,7 @@ volatile int g_ubwc = 1;
|
||||
|
||||
volatile int g_no_render_node;
|
||||
|
||||
static atomic_int g_output_refresh_mhz = 60000;
|
||||
static atomic_int g_output_refresh_pending;
|
||||
static int g_output_announced_refresh_mhz = 60000;
|
||||
volatile int g_output_refresh_mhz;
|
||||
|
||||
volatile int g_output_w, g_output_h;
|
||||
struct pending_release {
|
||||
@@ -662,7 +657,6 @@ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resou
|
||||
static struct surface *g_hud_surface;
|
||||
static int64_t g_hud_last_ns, g_hud_alive_ns;
|
||||
static int g_hud_fresh;
|
||||
static uint64_t g_game_frames;
|
||||
extern void droiddeck_on_game_surface(const char *window, const char *gpu);
|
||||
extern void droiddeck_on_game_frame(void);
|
||||
extern void droiddeck_on_game_program(int pid, const char *program);
|
||||
@@ -746,12 +740,10 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re
|
||||
}
|
||||
if (s == g_hud_surface) {
|
||||
g_hud_alive_ns = now_ns();
|
||||
if (!repeat) { g_hud_last_ns = g_hud_alive_ns; g_hud_fresh = 1; g_game_frames++; droiddeck_on_game_frame(); }
|
||||
if (!repeat) { g_hud_last_ns = g_hud_alive_ns; g_hud_fresh = 1; droiddeck_on_game_frame(); }
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t droiddeck_game_frames(void) { return g_game_frames; }
|
||||
|
||||
struct dmabuf_buffer *droiddeck_dmabuf_from_resource(struct wl_resource *buffer) { return get_dmabuf(buffer); }
|
||||
int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b) { return b && b->n_planes > 0 ? b->fd[0] : -1; }
|
||||
void droiddeck_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h) { *w = b->width; *h = b->height; }
|
||||
@@ -802,7 +794,7 @@ static void feedback_discard_all(struct wl_list *list) {
|
||||
}
|
||||
|
||||
static uint32_t nominal_refresh_ns(void) {
|
||||
int mhz = atomic_load(&g_output_refresh_mhz);
|
||||
int mhz = g_output_refresh_mhz;
|
||||
if (mhz <= 0) mhz = 60000;
|
||||
return (uint32_t)(1000000000000LL / (int64_t)mhz);
|
||||
}
|
||||
@@ -901,30 +893,8 @@ 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;
|
||||
@@ -951,7 +921,6 @@ static struct wl_resource *surface_take_idle(struct surface *s) {
|
||||
}
|
||||
|
||||
static void surface_drop_idle(struct surface *s) {
|
||||
surface_forget_idle_px(s);
|
||||
struct wl_resource *buffer = surface_take_idle(s);
|
||||
if (buffer) wl_buffer_send_release(buffer);
|
||||
}
|
||||
@@ -1080,7 +1049,6 @@ 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;
|
||||
@@ -1539,10 +1507,10 @@ static const struct wl_output_interface output_impl = { .release = output_releas
|
||||
static struct wl_list g_outputs;
|
||||
static void output_resource_destroy(struct wl_resource *r) { wl_list_remove(wl_resource_get_link(r)); }
|
||||
|
||||
static void output_send_mode(struct wl_resource *r, int mhz) {
|
||||
static void output_send_mode(struct wl_resource *r) {
|
||||
wl_output_send_mode(r, WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED,
|
||||
g_output_w > 0 ? g_output_w : 1920, g_output_h > 0 ? g_output_h : 1080,
|
||||
mhz);
|
||||
g_output_refresh_mhz > 0 ? g_output_refresh_mhz : 60000);
|
||||
}
|
||||
|
||||
static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t id) {
|
||||
@@ -1552,7 +1520,7 @@ static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t
|
||||
wl_list_insert(&g_outputs, wl_resource_get_link(r));
|
||||
wl_output_send_geometry(r, 0, 0, 340, 190, WL_OUTPUT_SUBPIXEL_UNKNOWN,
|
||||
"DroidDeck", "Wayland", WL_OUTPUT_TRANSFORM_NORMAL);
|
||||
output_send_mode(r, g_output_announced_refresh_mhz);
|
||||
output_send_mode(r);
|
||||
if (ver >= WL_OUTPUT_NAME_SINCE_VERSION) {
|
||||
wl_output_send_name(r, "DroidDeck-1");
|
||||
wl_output_send_description(r, "DroidDeck display");
|
||||
@@ -2068,9 +2036,6 @@ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint3
|
||||
wl_buffer_send_release(buffer);
|
||||
} else if (g_cursor_shown == s) {
|
||||
cursor_publish_shown();
|
||||
} else if (s->idle_px) {
|
||||
cursor_publish_pixels((const uint8_t *)s->idle_px, s->idle_w, s->idle_h, (size_t)s->idle_w * 4, hx, hy);
|
||||
g_cursor_shown = s;
|
||||
}
|
||||
}
|
||||
static void pointer_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
|
||||
@@ -2622,11 +2587,7 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int
|
||||
uint32_t t = now_ms();
|
||||
|
||||
pointer_focus(target->resource, fx, fy);
|
||||
// A button-only input must not replay a cached absolute position. A game
|
||||
// may have warped its cursor since the last motion (before taking lock,
|
||||
// or while releasing it), so that replay can become an unintended pan.
|
||||
// Absolute touch events still carry their position, including on clicks.
|
||||
if (!(k && k->is_lock) && (!relative || dx != 0 || dy != 0))
|
||||
if (!(k && k->is_lock))
|
||||
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) {
|
||||
@@ -2874,19 +2835,10 @@ static void deliver_key(const struct input_msg *m) {
|
||||
key_event((uint32_t)m->p1, m->p2);
|
||||
}
|
||||
|
||||
static void send_axis(struct seat_pointer *sp, uint32_t t, uint32_t axis, int value120) {
|
||||
if (!value120) return;
|
||||
if (wl_resource_get_version(sp->ptr) >= WL_POINTER_AXIS_VALUE120_SINCE_VERSION)
|
||||
wl_pointer_send_axis_value120(sp->ptr, axis, value120);
|
||||
else if (wl_resource_get_version(sp->ptr) >= WL_POINTER_AXIS_DISCRETE_SINCE_VERSION && value120 % 120 == 0)
|
||||
wl_pointer_send_axis_discrete(sp->ptr, axis, value120 / 120);
|
||||
wl_pointer_send_axis(sp->ptr, t, axis, wl_fixed_from_double(value120 / 12.0));
|
||||
}
|
||||
|
||||
static void axis_event(int vertical120, int horizontal120) {
|
||||
static void scroll_event(int steps) {
|
||||
struct surface *target = g_active_constraint ? g_active_constraint->surface
|
||||
: g_desktop ? g_desktop : (g_grab ? g_grab : toplevel_at(g_ptr_x, g_ptr_y));
|
||||
if (!target || (!vertical120 && !horizontal120)) return;
|
||||
if (!target || !steps) return;
|
||||
struct wl_client *client = wl_resource_get_client(target->resource);
|
||||
struct seat_pointer *sp;
|
||||
if (!pointer_for(client)) return;
|
||||
@@ -2895,18 +2847,17 @@ static void axis_event(int vertical120, int horizontal120) {
|
||||
uint32_t t = now_ms();
|
||||
pointer_focus(target->resource, wl_fixed_from_double(g_ptr_x - tx), wl_fixed_from_double(g_ptr_y - ty));
|
||||
for_each_pointer_of(client, sp) {
|
||||
send_axis(sp, t, WL_POINTER_AXIS_VERTICAL_SCROLL, vertical120);
|
||||
send_axis(sp, t, WL_POINTER_AXIS_HORIZONTAL_SCROLL, horizontal120);
|
||||
if (wl_resource_get_version(sp->ptr) >= WL_POINTER_AXIS_VALUE120_SINCE_VERSION)
|
||||
wl_pointer_send_axis_value120(sp->ptr, WL_POINTER_AXIS_VERTICAL_SCROLL, steps * 120);
|
||||
else if (wl_resource_get_version(sp->ptr) >= WL_POINTER_AXIS_DISCRETE_SINCE_VERSION)
|
||||
wl_pointer_send_axis_discrete(sp->ptr, WL_POINTER_AXIS_VERTICAL_SCROLL, steps);
|
||||
wl_pointer_send_axis(sp->ptr, t, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(steps * 10));
|
||||
if (wl_resource_get_version(sp->ptr) >= WL_POINTER_FRAME_SINCE_VERSION)
|
||||
wl_pointer_send_frame(sp->ptr);
|
||||
}
|
||||
wl_display_flush_clients(g_display);
|
||||
}
|
||||
|
||||
static void scroll_event(int steps) {
|
||||
axis_event(steps * 120, 0);
|
||||
}
|
||||
|
||||
static int update_modifiers(uint32_t evdev, int pressed) {
|
||||
static const struct { uint32_t evdev, mask; } mod_keys[] = {
|
||||
{ 42, 0x1 }, { 54, 0x1 },
|
||||
@@ -2962,19 +2913,13 @@ static void key_event(uint32_t evdev, int pressed) {
|
||||
wl_display_flush_clients(g_display);
|
||||
}
|
||||
|
||||
static void output_notify_mode(void) {
|
||||
int mhz = atomic_load(&g_output_refresh_mhz);
|
||||
struct wl_resource *r;
|
||||
wl_resource_for_each(r, &g_outputs) {
|
||||
output_send_mode(r, mhz);
|
||||
if (wl_resource_get_version(r) >= 2) wl_output_send_done(r);
|
||||
}
|
||||
g_output_announced_refresh_mhz = mhz;
|
||||
}
|
||||
|
||||
static void output_resized(void) {
|
||||
struct surface *s;
|
||||
output_notify_mode();
|
||||
struct wl_resource *r;
|
||||
wl_resource_for_each(r, &g_outputs) {
|
||||
output_send_mode(r);
|
||||
if (wl_resource_get_version(r) >= 2) wl_output_send_done(r);
|
||||
}
|
||||
wl_list_for_each(s, &g_surfaces, link)
|
||||
if (s->xdg_toplevel && s->toplevel_committed) send_toplevel_configure(s);
|
||||
droiddeck_log("display", "output resized to %dx%d", g_output_w, g_output_h);
|
||||
@@ -2983,15 +2928,13 @@ static void output_resized(void) {
|
||||
|
||||
static int on_input_readable(int fd, uint32_t mask, void *data) {
|
||||
struct input_msg m;
|
||||
int vsync = 0;
|
||||
int64_t vsync_ns = 0;
|
||||
while (read(fd, &m, sizeof(m)) == (ssize_t)sizeof(m)) {
|
||||
switch (m.type) {
|
||||
case 1: deliver_key(&m); break;
|
||||
case 2: pointer_event(m.p1, m.p2, 0, 0); break;
|
||||
case 3: pointer_button(m.p1, m.p2); break;
|
||||
case 4: scroll_event(m.p1); break;
|
||||
case 5: vsync = 1; vsync_ns = ((int64_t)m.p1 << 32) | (uint32_t)m.p2; break;
|
||||
case 5: on_vsync(((int64_t)m.p1 << 32) | (uint32_t)m.p2); break;
|
||||
case 6: pointer_delta(m.p1 / 256.0, m.p2 / 256.0); break;
|
||||
case 7: deliver_touch(&m); break;
|
||||
case 8: output_resized(); break;
|
||||
@@ -2999,17 +2942,9 @@ static int on_input_readable(int fd, uint32_t mask, void *data) {
|
||||
vk_present_set_scale_mode(m.p1, m.p2);
|
||||
schedule_render();
|
||||
break;
|
||||
case 10: break;
|
||||
case 11: axis_event(m.p1, m.p2); break;
|
||||
default: deliver_pointer(&m); break;
|
||||
}
|
||||
}
|
||||
if (atomic_exchange(&g_output_refresh_pending, 0) &&
|
||||
atomic_load(&g_output_refresh_mhz) != g_output_announced_refresh_mhz) {
|
||||
output_notify_mode();
|
||||
wl_display_flush_clients(g_display);
|
||||
}
|
||||
if (vsync) on_vsync(vsync_ns);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -3051,16 +2986,6 @@ void droiddeck_wayland_output_resized(void) {
|
||||
(void)n;
|
||||
}
|
||||
|
||||
void droiddeck_wayland_set_output_refresh_rate(int mhz) {
|
||||
if (mhz <= 0) mhz = 60000;
|
||||
if (atomic_exchange(&g_output_refresh_mhz, mhz) == mhz || g_input_pipe[1] < 0) return;
|
||||
atomic_store(&g_output_refresh_pending, 1);
|
||||
struct input_msg m = { 10, 0, 0, 0, 0 };
|
||||
ssize_t n;
|
||||
do { n = write(g_input_pipe[1], &m, sizeof(m)); } while (n < 0 && errno == EINTR);
|
||||
(void)n;
|
||||
}
|
||||
|
||||
void droiddeck_wayland_vsync(int64_t frame_time_ns) {
|
||||
if (g_input_pipe[1] < 0) return;
|
||||
struct input_msg m = { 5, (int)(frame_time_ns >> 32), (int)(uint32_t)frame_time_ns, 0, 0 };
|
||||
@@ -3298,7 +3223,6 @@ int droiddeck_wayland_run(void) {
|
||||
if ((g_stats_timer = wl_event_loop_add_timer(wl_display_get_event_loop(display), on_stats_timer, NULL)))
|
||||
wl_event_source_timer_update(g_stats_timer, 10000);
|
||||
|
||||
g_output_announced_refresh_mhz = atomic_load(&g_output_refresh_mhz);
|
||||
wl_display_run(display);
|
||||
|
||||
wl_display_destroy(display);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <android/log.h>
|
||||
|
||||
void droiddeck_log(const char *tag, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
uint64_t droiddeck_game_frames(void);
|
||||
#define FGLOG(...) droiddeck_log("framegen", __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "DroidDeckWayland", __VA_ARGS__)
|
||||
|
||||
@@ -86,7 +85,7 @@ static int64_t now_ns(void) {
|
||||
|
||||
|
||||
void vkp_framegen_set_engine(int kind) {
|
||||
if (kind != VKP_FG_ENGINE_LSFG && kind != VKP_FG_ENGINE_DDFG) kind = VKP_FG_ENGINE_LSFG;
|
||||
if (kind != VKP_FG_ENGINE_LSFG && kind != VKP_FG_ENGINE_WINFG) kind = VKP_FG_ENGINE_LSFG;
|
||||
atomic_store(&g_kind, kind);
|
||||
atomic_store(&g_failed_kind, -1);
|
||||
atomic_store(&g_cfg_dirty, 1);
|
||||
@@ -125,7 +124,7 @@ void vkp_framegen_set_tuning(float flow_scale, float refresh_hz) {
|
||||
atomic_store(&g_cfg_dirty, 1);
|
||||
}
|
||||
|
||||
void vkp_framegen_set_ddfg_tuning(int model, int perf_preset) {
|
||||
void vkp_framegen_set_winfg_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 +179,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), DD-FG Native %s",
|
||||
FGLOG("engines ready on the compositor's device: LSFG Native %s (%s), Win-FG Native %s",
|
||||
fge_caps_ok(VKP_FG_ENGINE_LSFG) ? "available" : "unavailable", fge_caps_reason(),
|
||||
fge_caps_ok(VKP_FG_ENGINE_DDFG) ? "available" : "unavailable");
|
||||
fge_caps_ok(VKP_FG_ENGINE_WINFG) ? "available" : "unavailable");
|
||||
}
|
||||
|
||||
void vkp_framegen_device_lost(void) { g_dev_lost = 1; }
|
||||
@@ -373,11 +372,7 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The pacer's source rate must be the game's, not this loop's: a loop slowed by its own
|
||||
* generations would otherwise read as a slower game and justify even more generations. */
|
||||
++g_source_frames;
|
||||
const uint64_t game_frames = droiddeck_game_frames();
|
||||
uint32_t n_gen = fge_plan(g_ring_n, game_frames ? game_frames : g_source_frames, g_presented_rate);
|
||||
uint32_t n_gen = fge_plan(g_ring_n, ++g_source_frames, g_presented_rate);
|
||||
if (n_gen > g_ring_n) n_gen = g_ring_n;
|
||||
g_planned = n_gen;
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
#endif
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
enum { VKP_FG_ENGINE_LSFG = 0, VKP_FG_ENGINE_DDFG = 1 };
|
||||
enum { VKP_FG_ENGINE_LSFG = 0, VKP_FG_ENGINE_WINFG = 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_ddfg_tuning(int model, int perf_preset);
|
||||
void vkp_framegen_set_winfg_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 "ddfg/ddfg_engine.h"
|
||||
#include "winfg/winfg_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<ddfg::Engine> g_ddfg;
|
||||
std::unique_ptr<winfg::Engine> g_winfg;
|
||||
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<ddfg::Engine>();
|
||||
auto e = std::make_unique<winfg::Engine>();
|
||||
if (!e->init(g_table, g_pd, g_dev, g_qfam, g_queue)) {
|
||||
ELOGE("ddfg-native: engine init failed");
|
||||
ELOGE("winfg-native: engine init failed");
|
||||
return -1;
|
||||
}
|
||||
g_ddfg = std::move(e);
|
||||
g_winfg = std::move(e);
|
||||
g_kind = 1;
|
||||
#if defined(DDFG_UPSTREAM) && defined(DDFG_CHAIN_HASH)
|
||||
std::snprintf(g_build, sizeof(g_build), "chain %s, src %s", DDFG_UPSTREAM, DDFG_CHAIN_HASH);
|
||||
#if defined(WINFG_UPSTREAM) && defined(WINFG_CHAIN_HASH)
|
||||
std::snprintf(g_build, sizeof(g_build), "chain %s, src %s", WINFG_UPSTREAM, WINFG_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_ddfg) && g_table.DeviceWaitIdle && g_dev) g_table.DeviceWaitIdle(g_dev);
|
||||
if ((g_lsfg || g_winfg) && g_table.DeviceWaitIdle && g_dev) g_table.DeviceWaitIdle(g_dev);
|
||||
g_lsfg.reset();
|
||||
g_ddfg.reset();
|
||||
g_winfg.reset();
|
||||
g_kind = -1;
|
||||
}
|
||||
|
||||
int fge_unavailable(void) {
|
||||
if (g_kind == 1) return g_ddfg && g_ddfg->unavailable() ? 1 : 0;
|
||||
if (g_kind == 1) return g_winfg && g_winfg->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_ddfg) g_ddfg->configure(multiplier, model, perf_preset, flow_scale);
|
||||
if (g_kind == 1 && g_winfg) g_winfg->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_ddfg && g_ddfg->prepare(w, h, fmt) ? 1 : 0;
|
||||
if (g_kind == 1) return g_winfg && g_winfg->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_ddfg ? g_ddfg->plan(capacity) : 0;
|
||||
if (g_kind == 1) return g_winfg ? g_winfg->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_ddfg) g_ddfg->process(cmd, source, w, h, gens); return; }
|
||||
if (g_kind == 1) { if (g_winfg) g_winfg->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_ddfg) g_ddfg->generateInto(cmd, g, count, img, view, w, h); return; }
|
||||
if (g_kind == 1) { if (g_winfg) g_winfg->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 ? "DD-FG Native" : "LSFG Native"; }
|
||||
const char *fge_engine_name(int kind) { return kind == 1 ? "Win-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 ? "DD-FG Native" : "LSFG Native"; }
|
||||
const char *fge_engine_name(int kind) { return kind == 1 ? "Win-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_nativeSetDdFgTuning(JNIEnv *env, jclass clazz,
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetWinFgTuning(JNIEnv *env, jclass clazz,
|
||||
jint model, jint perfPreset) {
|
||||
(void)env; (void)clazz;
|
||||
vkp_framegen_set_ddfg_tuning((int)model, (int)perfPreset);
|
||||
vkp_framegen_set_winfg_tuning((int)model, (int)perfPreset);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
enum {
|
||||
DROIDDECK_HEADPOSE_LIVE = 1u << 0,
|
||||
DROIDDECK_HEADPOSE_ENABLED = 1u << 1,
|
||||
DROIDDECK_HEADPOSE_MONO = 1u << 2,
|
||||
};
|
||||
|
||||
struct droiddeck_headpose {
|
||||
@@ -23,10 +22,7 @@ struct droiddeck_headpose {
|
||||
float tan_half_fov;
|
||||
float eye_aspect;
|
||||
uint32_t eye_w, eye_h;
|
||||
float display_hz;
|
||||
// Uses a formerly reserved word; older publishers leave this zero.
|
||||
uint32_t render_height;
|
||||
uint32_t reserved[10];
|
||||
uint32_t reserved[12];
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "spatialpose_shm.h"
|
||||
#include <jni.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static pthread_mutex_t spatial_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static struct droiddeck_spatialpose *shared;
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetSpatialFile(
|
||||
JNIEnv *env, jclass clazz, jstring path) {
|
||||
(void)clazz;
|
||||
pthread_mutex_lock(&spatial_lock);
|
||||
if (!shared && path) {
|
||||
const char *name = (*env)->GetStringUTFChars(env, path, NULL);
|
||||
if (name) {
|
||||
int fd = open(name, O_RDWR | O_CREAT | O_CLOEXEC, 0600);
|
||||
(*env)->ReleaseStringUTFChars(env, path, name);
|
||||
if (fd >= 0) {
|
||||
if (ftruncate(fd, sizeof(*shared)) == 0) {
|
||||
void *p = mmap(NULL, sizeof(*shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (p != MAP_FAILED) {
|
||||
shared = p;
|
||||
memset(shared, 0, sizeof(*shared));
|
||||
shared->magic = DROIDDECK_SPATIALPOSE_MAGIC;
|
||||
shared->version = DROIDDECK_SPATIALPOSE_VERSION;
|
||||
shared->size = sizeof(*shared);
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
jboolean ready = shared != NULL;
|
||||
pthread_mutex_unlock(&spatial_lock);
|
||||
return ready;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativePublishSpatial(
|
||||
JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
(void)clazz;
|
||||
if (!buffer || (*env)->GetDirectBufferCapacity(env, buffer) != sizeof(*shared)) return;
|
||||
const struct droiddeck_spatialpose *src = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
if (!src || src->magic != DROIDDECK_SPATIALPOSE_MAGIC || src->version != DROIDDECK_SPATIALPOSE_VERSION ||
|
||||
src->size != sizeof(*shared)) return;
|
||||
pthread_mutex_lock(&spatial_lock);
|
||||
if (shared) {
|
||||
unsigned seq = __atomic_load_n(&shared->seq, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&shared->seq, seq | 1u, __ATOMIC_RELAXED);
|
||||
__atomic_thread_fence(__ATOMIC_RELEASE);
|
||||
memcpy((char *)shared + 16, (const char *)src + 16, sizeof(*shared) - 16);
|
||||
__atomic_store_n(&shared->seq, (seq | 1u) + 1u, __ATOMIC_RELEASE);
|
||||
}
|
||||
pthread_mutex_unlock(&spatial_lock);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#ifndef DROIDDECK_SPATIALPOSE_SHM_H
|
||||
#define DROIDDECK_SPATIALPOSE_SHM_H
|
||||
#include <stdint.h>
|
||||
#define DROIDDECK_SPATIALPOSE_MAGIC 0x53504444u
|
||||
#define DROIDDECK_SPATIALPOSE_VERSION 4u
|
||||
#define DROIDDECK_SPATIALPOSE_FILE "droiddeck-spatialpose"
|
||||
enum { DD_SPATIAL_ACTIVE = 1u, DD_SPATIAL_HANDS = 2u, DD_SPATIAL_JOYCONS = 4u, DD_SPATIAL_STEADY_AIM = 8u, DD_SPATIAL_VALID = 1u };
|
||||
enum { DD_SPATIAL_TRIGGER_VALID = 2u, DD_SPATIAL_GRIP_VALID = 4u };
|
||||
struct dd_spatial_pose {
|
||||
int64_t time_ns;
|
||||
float position[3];
|
||||
float rotation[4];
|
||||
uint32_t flags;
|
||||
};
|
||||
struct dd_spatial_hand {
|
||||
struct dd_spatial_pose pose;
|
||||
float trigger;
|
||||
uint32_t grip;
|
||||
};
|
||||
struct droiddeck_spatialpose {
|
||||
uint32_t magic, version, size, seq;
|
||||
uint64_t connection, origin;
|
||||
uint32_t flags, input_epochs;
|
||||
struct dd_spatial_pose head;
|
||||
struct dd_spatial_hand hands[2];
|
||||
};
|
||||
#ifdef __cplusplus
|
||||
static_assert(sizeof(dd_spatial_pose) == 40, "spatial pose ABI");
|
||||
static_assert(sizeof(droiddeck_spatialpose) == 176, "spatial snapshot ABI");
|
||||
#else
|
||||
_Static_assert(sizeof(struct droiddeck_spatialpose) == 176, "spatial snapshot ABI");
|
||||
#endif
|
||||
#endif
|
||||
@@ -27,13 +27,13 @@ extern void droiddeck_wayland_send_key(int evdev, int state);
|
||||
extern void droiddeck_wayland_send_scene_input(int type, int a, int b);
|
||||
extern void droiddeck_wayland_vsync(int64_t frame_time_ns);
|
||||
extern void droiddeck_wayland_output_resized(void);
|
||||
extern void droiddeck_wayland_set_output_refresh_rate(int mhz);
|
||||
extern volatile int g_fps_limit;
|
||||
extern volatile int g_hide_shell;
|
||||
extern volatile int g_zero_copy;
|
||||
extern volatile unsigned g_zero_copy_last;
|
||||
extern volatile int g_ubwc;
|
||||
extern volatile int g_no_render_node;
|
||||
extern volatile int g_output_refresh_mhz;
|
||||
extern volatile int g_output_w, g_output_h;
|
||||
|
||||
#define TAG "DroidDeckWayland"
|
||||
@@ -328,7 +328,7 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLayerFrameRate(JN
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetOutputRefreshRate(JNIEnv *env, jclass clazz, jfloat hz) {
|
||||
droiddeck_wayland_set_output_refresh_rate(hz > 1.0f ? (int)(hz * 1000.0f + 0.5f) : 0);
|
||||
g_output_refresh_mhz = hz > 1.0f ? (int)(hz * 1000.0f + 0.5f) : 0;
|
||||
}
|
||||
|
||||
extern void vkp_reset_first_frame(void);
|
||||
@@ -342,7 +342,6 @@ JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetOutputSize(JNIEnv *env, jclass clazz, jint w, jint h) {
|
||||
g_output_w = w > 0 ? w : 0;
|
||||
g_output_h = h > 0 ? h : 0;
|
||||
vkp_cardboard_set_render_height(h);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
@@ -351,7 +350,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeResizeOutput(JNIEnv
|
||||
if (w <= 0 || h <= 0) return;
|
||||
g_output_w = w;
|
||||
g_output_h = h;
|
||||
vkp_cardboard_set_render_height(h);
|
||||
droiddeck_wayland_output_resized();
|
||||
}
|
||||
|
||||
@@ -404,11 +402,10 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetScreenEffects(JNI
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboard(JNIEnv *env, jclass clazz, jboolean enabled,
|
||||
jint source, jfloat ipd, jfloat dist, jfloat width, jfloat k1, jfloat k2, jfloat fov, jfloat inset, jfloat chroma,
|
||||
jboolean mono, jfloat hz) {
|
||||
jint source, jfloat ipd, jfloat dist, jfloat width, jfloat k1, jfloat k2, jfloat fov, jfloat inset, jfloat chroma) {
|
||||
struct cardboard_params p = {.enabled = enabled ? 1 : 0, .source = source, .ipd_m = ipd, .screen_dist_m = dist,
|
||||
.screen_width_m = width, .k1 = k1, .k2 = k2, .fov_deg = fov, .lens_inset = inset,
|
||||
.chroma = chroma, .mono = mono ? 1 : 0, .display_hz = hz};
|
||||
.chroma = chroma};
|
||||
vkp_cardboard_set(&p);
|
||||
}
|
||||
|
||||
@@ -649,14 +646,3 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeRaisePriority(JNIEnv
|
||||
int after = getpriority(PRIO_PROCESS, (id_t)tid);
|
||||
return after == -1 && errno != 0 ? 100 : after;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardSpatialPose(JNIEnv *env, jclass clazz,
|
||||
jfloatArray rot, jfloatArray pos, jboolean live) {
|
||||
(void)clazz;
|
||||
if (!rot || !pos || (*env)->GetArrayLength(env, rot) != 9 || (*env)->GetArrayLength(env, pos) != 3) return;
|
||||
float m[9], p[3];
|
||||
(*env)->GetFloatArrayRegion(env, rot, 0, 9, m);
|
||||
(*env)->GetFloatArrayRegion(env, pos, 0, 3, p);
|
||||
vkp_cardboard_set_spatial_pose(m, p, live ? 1 : 0);
|
||||
}
|
||||
@@ -16,48 +16,16 @@ class App : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// 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)
|
||||
BwrapSpawner.start(this)
|
||||
com.droiddeck.launcher.frontend.GameFileSync.start(this)
|
||||
com.droiddeck.launcher.stores.StoresState.init(this)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.gog.GogBackend)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.epic.EpicBackend)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.amazon.AmazonBackend)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.itch.ItchBackend)
|
||||
com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.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,8 +11,6 @@ import androidx.compose.runtime.getValue
|
||||
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.gpu.DriverPairs
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
@@ -23,12 +21,7 @@ import com.droiddeck.launcher.gpu.TurnipReleases
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.QuestExperimental
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import com.droiddeck.launcher.ui.DriverRow
|
||||
import com.droiddeck.launcher.ui.DriverSlot
|
||||
import com.droiddeck.launcher.ui.GPU_MODE_AUTO
|
||||
import com.droiddeck.launcher.ui.GPU_MODE_MIX
|
||||
import com.droiddeck.launcher.ui.GPU_MODE_PAIR
|
||||
import com.droiddeck.launcher.ui.GpuDriversState
|
||||
import com.droiddeck.launcher.ui.PairRow
|
||||
import com.droiddeck.launcher.wayland.CompositorHost
|
||||
@@ -49,18 +42,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
var pairPercent by mutableIntStateOf(-1)
|
||||
var autoStatus by mutableStateOf("")
|
||||
var activeBundle by mutableStateOf<String?>(null)
|
||||
private var lastAutoCheck = 0L
|
||||
private var pairModeOpen by mutableStateOf(false)
|
||||
var restartPending by mutableStateOf(false)
|
||||
private set
|
||||
private var runtimePending by mutableStateOf(false)
|
||||
private var sessionRunning by mutableStateOf(false)
|
||||
|
||||
val uiMode: String get() = when (mode) {
|
||||
SessionPrefs.GPU_DRIVERS_AUTO -> GPU_MODE_AUTO
|
||||
SessionPrefs.GPU_DRIVERS_MIX -> GPU_MODE_MIX
|
||||
else -> if (pairModeOpen || pairRows.any { it.active }) GPU_MODE_PAIR else GPU_MODE_MIX
|
||||
}
|
||||
private var autoCheckedThisProcess = false
|
||||
|
||||
fun state() = GpuDriversState(
|
||||
gpuName = gpu.displayName(activity), gpuFamily = gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity),
|
||||
@@ -70,56 +52,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
linuxRows = linuxRows, linuxSelected = linuxSelected, androidRows = androidRows, androidSelected = androidSelected,
|
||||
linuxDownloads = linuxDownloads, androidDownloads = androidDownloads, canRestoreBundled = canRestoreBundled,
|
||||
activeBundle = activeBundle,
|
||||
mode = uiMode, display = displaySlot(), runtime = runtimeSlot(),
|
||||
canRestart = !sessionRunning,
|
||||
checkedAt = TurnipReleases.cached(activity)?.checkedAt ?: 0L,
|
||||
)
|
||||
|
||||
private fun slotSource(chosenDefault: Boolean, defaultLabel: Int): String = activity.getString(when {
|
||||
uiMode == GPU_MODE_AUTO -> R.string.gpu_src_auto
|
||||
uiMode == GPU_MODE_PAIR && pairRows.any { it.active } -> R.string.gpu_src_pair
|
||||
activeBundle != null -> R.string.gpu_src_bundle
|
||||
chosenDefault -> defaultLabel
|
||||
else -> R.string.gpu_src_chosen
|
||||
})
|
||||
|
||||
private fun displaySlot(): DriverSlot {
|
||||
val row = androidRows.firstOrNull { it.id == androidSelected }
|
||||
val live = androidRows.firstOrNull { it.id == CompositorHost.displayChoice }?.name
|
||||
return DriverSlot(
|
||||
name = row?.name ?: activity.getString(R.string.gpu_auto_by_gpu),
|
||||
source = slotSource(androidSelected == TurnipDriver.AUTO, R.string.gpu_src_by_gpu),
|
||||
pending = restartPending,
|
||||
status = when {
|
||||
restartPending -> activity.getString(R.string.gpu_display_pending, live ?: activity.getString(R.string.gpu_previous_driver))
|
||||
CompositorHost.isStarted -> activity.getString(R.string.gpu_display_live)
|
||||
else -> activity.getString(R.string.gpu_display_next)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun runtimeSlot(): DriverSlot {
|
||||
val row = linuxRows.firstOrNull { it.id == linuxSelected }
|
||||
val live = SessionState.runtimeDriver?.let { id -> linuxRows.firstOrNull { it.id == id }?.name }
|
||||
return DriverSlot(
|
||||
name = row?.name ?: activity.getString(R.string.gpu_runtime_default),
|
||||
source = slotSource(linuxSelected.isEmpty(), R.string.gpu_src_default),
|
||||
pending = runtimePending,
|
||||
status = when {
|
||||
runtimePending -> activity.getString(R.string.gpu_runtime_pending, live ?: activity.getString(R.string.gpu_previous_driver))
|
||||
sessionRunning -> activity.getString(R.string.gpu_runtime_live)
|
||||
else -> activity.getString(R.string.gpu_runtime_hint)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun restartApp() {
|
||||
if (SessionState.running) return
|
||||
val launch = activity.packageManager.getLaunchIntentForPackage(activity.packageName) ?: return
|
||||
activity.startActivity(android.content.Intent.makeRestartActivityTask(launch.component)
|
||||
.putExtra(com.droiddeck.launcher.stores.download.StoreDownloadService.EXTRA_NAV, "components"))
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
var androidRows by mutableStateOf<List<DriverRow>>(emptyList())
|
||||
var androidSelected by mutableStateOf("")
|
||||
|
||||
@@ -157,30 +90,21 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
canRestoreBundled = td.hiddenBundled().isNotEmpty()
|
||||
androidSelected = SessionPrefs.androidDriver(activity)
|
||||
activeBundle = DriverBundle.active(activity)?.label
|
||||
restartPending = CompositorHost.isStarted && CompositorHost.displayChoice.let { it != null && it != androidSelected }
|
||||
sessionRunning = SessionState.running
|
||||
runtimePending = sessionRunning && SessionState.runtimeDriver.let { it != null && it != linuxSelected }
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
}
|
||||
|
||||
fun summary(): String {
|
||||
val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle
|
||||
val display = displaySlot().name
|
||||
val base = when {
|
||||
uiMode == GPU_MODE_AUTO -> activity.getString(R.string.drivers_summary, activity.getString(R.string.common_auto), active ?: display)
|
||||
uiMode == GPU_MODE_PAIR && active != null -> activity.getString(R.string.drivers_summary, activity.getString(R.string.gpu_mode_pair_short), active)
|
||||
else -> activity.getString(R.string.drivers_summary, activity.getString(R.string.gpu_mode_mix_short),
|
||||
runtimeSlot().name.let { if (it == display) it else activity.getString(R.string.gpu_summary_mix, display, it) })
|
||||
}
|
||||
return if (restartPending) activity.getString(R.string.gpu_summary_restart, base) else base
|
||||
val label = activity.getString(if (mode == SessionPrefs.GPU_DRIVERS_AUTO) R.string.common_auto else R.string.drivers_manual)
|
||||
return active?.let { activity.getString(R.string.drivers_summary, label, it) } ?: label
|
||||
}
|
||||
|
||||
fun refreshPairs() {
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
|
||||
val recommended = if (QuestExperimental.isSupported()) null else recommendedKey(pairs)
|
||||
val recommended = if (QuestExperimental.isSupported()) null else DriverPairs.recommendedKey(gpu, pairs)
|
||||
pairRows = pairs.map { p ->
|
||||
val bundle = p.bundle?.let { installedBundle(it) }
|
||||
val displayId = if (p.bundle != null) bundle?.androidId else p.display?.let { TurnipReleases.installedId(activity, it, td::isInstalled) }
|
||||
@@ -197,53 +121,18 @@ 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)
|
||||
}
|
||||
|
||||
fun setMode(auto: Boolean, mix: Boolean = false) {
|
||||
mode = when {
|
||||
auto -> SessionPrefs.GPU_DRIVERS_AUTO
|
||||
mix -> SessionPrefs.GPU_DRIVERS_MIX
|
||||
else -> SessionPrefs.GPU_DRIVERS_MANUAL
|
||||
}
|
||||
fun setMode(auto: Boolean) {
|
||||
mode = if (auto) SessionPrefs.GPU_DRIVERS_AUTO else SessionPrefs.GPU_DRIVERS_MANUAL
|
||||
SessionPrefs.setGpuDriverMode(activity, mode)
|
||||
autoStatus = ""
|
||||
if (auto) ensureAuto(force = false)
|
||||
}
|
||||
|
||||
fun checkOnOpen() {
|
||||
if (SessionPrefs.gpuDriverMode(activity) == SessionPrefs.GPU_DRIVERS_AUTO) ensureAuto(force = false)
|
||||
else if (!releaseChecking && releasesStale() && com.droiddeck.launcher.frontend.AutoComponents.isOnline(activity)) checkLatestTurnip()
|
||||
}
|
||||
|
||||
private fun releasesStale(): Boolean {
|
||||
val cached = TurnipReleases.cached(activity)
|
||||
return cached == null || System.currentTimeMillis() - cached.checkedAt > RELEASE_CHECK_INTERVAL_MS ||
|
||||
cached.assets.none { it.pair.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun ensureAuto(force: Boolean) {
|
||||
mode = SessionPrefs.gpuDriverMode(activity)
|
||||
if (mode != SessionPrefs.GPU_DRIVERS_AUTO || pairBusy != null || releaseChecking) return
|
||||
if (SessionState.running) {
|
||||
if (force) autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
return
|
||||
}
|
||||
AutoDriverTrial.rollbackDue(activity)?.let { trial ->
|
||||
AutoDriverTrial.rollBack(activity, trial)
|
||||
autoStatus = activity.getString(R.string.drivers_rolled_back, trial.name)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
if (!force && System.currentTimeMillis() - lastAutoCheck < RELEASE_CHECK_INTERVAL_MS) return
|
||||
lastAutoCheck = System.currentTimeMillis()
|
||||
if (!force && autoCheckedThisProcess) return
|
||||
autoCheckedThisProcess = true
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != QuestExperimental.defaultAndroidDriver()
|
||||
if (QuestExperimental.applyAutoDrivers(activity)) {
|
||||
autoStatus = activity.getString(
|
||||
@@ -259,15 +148,16 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
releaseChecking = true
|
||||
autoStatus = activity.getString(R.string.drivers_checking_latest)
|
||||
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() }
|
||||
val cached = TurnipReleases.cached(activity)
|
||||
val stale = cached == null || System.currentTimeMillis() - cached.checkedAt > 24 * 3_600_000L ||
|
||||
cached.assets.none { it.pair.isNotEmpty() }
|
||||
val problem = if (force || stale) runCatching { TurnipReleases.refresh(activity) }.exceptionOrNull()?.message else null
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
|
||||
val key = recommendedKey(pairs)
|
||||
val key = DriverPairs.recommendedKey(gpu, pairs)
|
||||
val pair = pairs.firstOrNull { it.key == key }
|
||||
val row = pairRows.firstOrNull { it.key == key }
|
||||
when {
|
||||
@@ -275,10 +165,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
?: activity.getString(R.string.upd_current_status)
|
||||
pair == null || !pair.complete ->
|
||||
autoStatus = problem?.let { activity.getString(R.string.upd_couldnt_check, it) } ?: activity.getString(R.string.drivers_recommended_unpublished)
|
||||
AutoDriverTrial.release(pair) in AutoDriverTrial.bad(activity) ->
|
||||
autoStatus = activity.getString(R.string.drivers_skipping_bad, pair.name, pair.version)
|
||||
SessionState.running -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
holdBack && row?.installed != true -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_network, pair.name, pair.version)
|
||||
else -> installPair(pair, auto = true)
|
||||
}
|
||||
}
|
||||
@@ -288,8 +174,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
fun selectPair(key: String) {
|
||||
if (pairBusy != null) return
|
||||
val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return
|
||||
pairModeOpen = true
|
||||
if (mode != SessionPrefs.GPU_DRIVERS_MANUAL) setMode(auto = false)
|
||||
installPair(pair, auto = false)
|
||||
}
|
||||
|
||||
@@ -299,6 +183,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
pairPercent = 0
|
||||
if (auto) autoStatus = activity.getString(R.string.drivers_downloading_pair, pair.name, pair.version)
|
||||
Thread({
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
var downloaded = false
|
||||
val result = runCatching {
|
||||
val zips = pair.assets
|
||||
@@ -312,27 +198,22 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
pairBusy = null
|
||||
pairPercent = -1
|
||||
result.onSuccess { (displayId, linuxId) ->
|
||||
if (auto && SessionState.running) {
|
||||
lastAutoCheck = 0L
|
||||
autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
return@onSuccess
|
||||
}
|
||||
val previousDisplay = SessionPrefs.androidDriver(activity)
|
||||
val previousLinux = SessionPrefs.linuxDriver(activity)
|
||||
val displayChanged = previousDisplay != displayId
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != displayId
|
||||
SessionPrefs.setAndroidDriver(activity, displayId)
|
||||
SessionPrefs.setLinuxDriver(activity, linuxId)
|
||||
val restart = displayChanged && CompositorHost.isStarted
|
||||
if (auto) {
|
||||
val trial = AutoDriverTrial.current(activity)
|
||||
val lastGood = trial?.let { setOf(it.previousDisplay, it.previousLinux) } ?: setOf(previousDisplay, previousLinux)
|
||||
val changed = previousDisplay != displayId || previousLinux != linuxId
|
||||
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId))
|
||||
if (changed) AutoDriverTrial.begin(
|
||||
activity, AutoDriverTrial.release(pair), "${pair.name} ${pair.version}", displayId, linuxId,
|
||||
trial?.previousDisplay ?: previousDisplay, trial?.previousLinux ?: previousLinux,
|
||||
)
|
||||
if (!SessionState.running) AutoDriverTrial.prune(activity, lastGood + setOf(displayId, linuxId))
|
||||
val previous = SessionPrefs.gpuAutoInstalled(activity)
|
||||
for (id in previous - setOf(displayId, linuxId)) {
|
||||
val bundle = DriverBundle.containing(activity, id, lm.isInstalled(id))
|
||||
when {
|
||||
bundle != null -> { DriverBundle.remove(activity, bundle); TurnipReleases.forget(activity, bundle.id) }
|
||||
lm.isInstalled(id) -> lm.removeDriver(id)
|
||||
else -> td.remove(id)
|
||||
}
|
||||
TurnipReleases.forget(activity, id)
|
||||
}
|
||||
SessionPrefs.setGpuAutoInstalled(activity, setOf(displayId, linuxId))
|
||||
autoStatus = activity.getString(
|
||||
if (downloaded) { if (restart) R.string.drivers_updated_to_restart else R.string.drivers_updated_to }
|
||||
else { if (restart) R.string.drivers_switched_to_restart else R.string.drivers_switched_to },
|
||||
@@ -423,15 +304,14 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
linux -> SessionPrefs.setLinuxDriver(activity, id)
|
||||
else -> SessionPrefs.setAndroidDriver(activity, id)
|
||||
}
|
||||
pairModeOpen = false
|
||||
setMode(auto = false, mix = true)
|
||||
setMode(false)
|
||||
refreshDrivers()
|
||||
}
|
||||
|
||||
private fun useBundle(bundle: DriverBundle.Bundle): String {
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != bundle.androidId
|
||||
DriverBundle.select(activity, bundle)
|
||||
if (mode == SessionPrefs.GPU_DRIVERS_AUTO) setMode(auto = false, mix = true)
|
||||
if (mode == SessionPrefs.GPU_DRIVERS_AUTO) setMode(false)
|
||||
return activity.getString(
|
||||
if (displayChanged && CompositorHost.isStarted) R.string.drivers_using_bundle_restart else R.string.drivers_using_bundle,
|
||||
bundle.label,
|
||||
@@ -540,7 +420,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
|
||||
private companion object {
|
||||
const val TAG = "MainActivity"
|
||||
const val RELEASE_CHECK_INTERVAL_MS = 3_600_000L
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ import com.droiddeck.launcher.session.SessionLogShare
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.session.SteamRepair
|
||||
import com.droiddeck.launcher.core.CpuCores
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import com.droiddeck.launcher.core.PhantomProcessLimit
|
||||
import com.droiddeck.launcher.core.PhantomProcessStatus
|
||||
import com.droiddeck.launcher.core.WirelessAdbFix
|
||||
@@ -109,13 +108,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
|
||||
private var switchDeadline = 0L
|
||||
private var showResetSettings by mutableStateOf(false)
|
||||
private var lowSpace by mutableStateOf<Long?>(null)
|
||||
private var showNonAdreno by mutableStateOf<LinuxRuntimeInstaller.Release?>(null)
|
||||
private var glThread by mutableStateOf(true)
|
||||
private var noGlError by mutableStateOf(true)
|
||||
@@ -124,7 +117,6 @@ class MainActivity : ComponentActivity() {
|
||||
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
|
||||
@@ -162,11 +154,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var stretch16x9 by mutableStateOf(false)
|
||||
private var orientation by mutableStateOf(android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_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)
|
||||
@@ -199,10 +187,6 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setWifiDiscoveryEnabled(this, WifiDiscovery.permissionGranted(this))
|
||||
refreshWifiDiscovery()
|
||||
}
|
||||
private val cameraPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
com.droiddeck.launcher.handtracking.HandTracking.refreshPermission(this)
|
||||
}
|
||||
private var placementCheck by mutableStateOf<Boolean?>(null)
|
||||
private val wifiPermissionSettings = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
SessionPrefs.setWifiDiscoveryEnabled(this, WifiDiscovery.permissionGranted(this))
|
||||
refreshWifiDiscovery()
|
||||
@@ -220,9 +204,6 @@ class MainActivity : ComponentActivity() {
|
||||
private val pickDeckyPluginZip = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedFile(r.data)?.let { decky.importPluginZip(it) }
|
||||
}
|
||||
private val pickBootMovie = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedFile(r.data)?.let { importBootMovie(it) }
|
||||
}
|
||||
private val pickAnyDriver = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedUri(r.data)?.let { drivers.importDriver(it, linux = null) }
|
||||
}
|
||||
@@ -267,38 +248,6 @@ class MainActivity : ComponentActivity() {
|
||||
}, "lossless-sync").start()
|
||||
}
|
||||
|
||||
private fun importBootMovie(file: File) {
|
||||
bootMovieImporting = true
|
||||
bootMovieProgress = -1
|
||||
Thread({
|
||||
val result = com.droiddeck.launcher.session.BootMovie.import(
|
||||
this, file,
|
||||
onStart = { ui.post { android.widget.Toast.makeText(this, getString(R.string.boot_movie_converting_started, file.name), android.widget.Toast.LENGTH_LONG).show() } },
|
||||
onProgress = { percent -> ui.post { bootMovieProgress = percent } },
|
||||
)
|
||||
val message = when (result) {
|
||||
is com.droiddeck.launcher.session.BootMovie.Import.Done -> getString(
|
||||
if (result.audioDropped) R.string.boot_movie_imported_silent else R.string.boot_movie_imported, result.name)
|
||||
com.droiddeck.launcher.session.BootMovie.Import.TooLong ->
|
||||
getString(R.string.boot_movie_too_long, com.droiddeck.launcher.session.BootMovie.MAX_CONVERT_SECONDS)
|
||||
is com.droiddeck.launcher.session.BootMovie.Import.Unsupported -> getString(R.string.boot_movie_unsupported)
|
||||
com.droiddeck.launcher.session.BootMovie.Import.Failed -> getString(R.string.boot_movie_import_failed)
|
||||
}
|
||||
ui.post {
|
||||
bootMovieImporting = false
|
||||
bootMovieProgress = -1
|
||||
loadBootMovie()
|
||||
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "boot-movie-import").start()
|
||||
}
|
||||
|
||||
private fun loadBootMovie() {
|
||||
bootMovie = com.droiddeck.launcher.session.BootMovie.choice(this)
|
||||
bootMovieSound = SessionPrefs.bootMovieSound(this)
|
||||
bootMovieCustomName = com.droiddeck.launcher.session.BootMovie.customName(this)
|
||||
}
|
||||
|
||||
private fun importLossless(dll: File) {
|
||||
Thread({
|
||||
val message = Lossless.message(this, Lossless.import(this, dll))
|
||||
@@ -374,13 +323,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var resolution by mutableStateOf(com.droiddeck.launcher.session.SessionDisplay.DEFAULT_RESOLUTION)
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var forceSsbs by mutableStateOf(false)
|
||||
private var steamClientArch by mutableStateOf("arm64")
|
||||
private var runSteamAtStartup by mutableStateOf(false)
|
||||
private var bootMovie by mutableStateOf(com.droiddeck.launcher.session.BootMovie.DROIDDECK_30)
|
||||
private var bootMovieSound by mutableStateOf(true)
|
||||
private var bootMovieCustomName by mutableStateOf<String?>(null)
|
||||
private var bootMovieImporting by mutableStateOf(false)
|
||||
private var bootMovieProgress by mutableStateOf(-1)
|
||||
private var theme by mutableStateOf("graphite")
|
||||
private var appScale by mutableStateOf(com.droiddeck.launcher.core.AppUiPrefs.DEFAULT_SCALE)
|
||||
private var hdrOn by mutableStateOf(false)
|
||||
@@ -403,13 +346,11 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
private var romsDir by mutableStateOf<String?>(null)
|
||||
private var steamGames by mutableStateOf<List<Library.SteamGame>>(emptyList())
|
||||
private var steamGamesX64 by mutableStateOf(false)
|
||||
@Volatile private var componentSweepDone = false
|
||||
@Volatile private var componentSweepSession: String? = null
|
||||
private var emulatorList by mutableStateOf<List<Library.Emulator>>(emptyList())
|
||||
private var runningLabel by mutableStateOf<String?>(null)
|
||||
private var logsEnabled by mutableStateOf(true)
|
||||
private var vacDiagnostics by mutableStateOf(false)
|
||||
private var agentCommands by mutableStateOf(false)
|
||||
private var showRoms by mutableStateOf(false)
|
||||
private var homeAppSelected by mutableStateOf(false)
|
||||
@@ -487,20 +428,6 @@ 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
|
||||
@@ -530,186 +457,24 @@ class MainActivity : ComponentActivity() {
|
||||
android.widget.Toast.makeText(this, R.string.main_wait_install_game, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
if (com.droiddeck.launcher.session.NoSteamLaunch.enabled(this, game)) return launchWithoutSteam(game, returnHome)
|
||||
if (SessionState.running) {
|
||||
if (SessionState.mode != SessionService.MODE_STEAM || SessionState.stopRequested) {
|
||||
android.widget.Toast.makeText(this, R.string.game_link_session_busy, android.widget.Toast.LENGTH_LONG).show()
|
||||
return true
|
||||
}
|
||||
val steamVr = game.gameIdString == com.droiddeck.launcher.gpu.SteamVr.APP_ID
|
||||
if (!steamVr && !SessionService.launchGame(this, game.gameIdString)) {
|
||||
if (!SessionService.launchGame(this, game.gameIdString)) {
|
||||
android.widget.Toast.makeText(this, R.string.game_link_failed, android.widget.Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.putExtra(SessionActivity.EXTRA_STEAMVR, steamVr)
|
||||
.putExtra(SessionActivity.EXTRA_RETURN_HOME, returnHome)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
return true
|
||||
}
|
||||
return startSession(Intent(this, SessionActivity::class.java)
|
||||
.putExtra(SessionActivity.EXTRA_RETURN_HOME, returnHome)
|
||||
.apply {
|
||||
if (game.gameIdString == com.droiddeck.launcher.gpu.SteamVr.APP_ID) {
|
||||
putExtra(SessionActivity.EXTRA_STEAMVR, true)
|
||||
} else {
|
||||
putExtra(SessionService.EXTRA_STEAM_URL,
|
||||
com.droiddeck.launcher.frontend.GameLaunchLink.steamUrl(game.gameIdString))
|
||||
}
|
||||
}, steamSession = true)
|
||||
}
|
||||
|
||||
private fun launchWithoutSteam(game: Library.SteamGame, returnHome: Boolean): Boolean {
|
||||
val app = applicationContext
|
||||
Thread({
|
||||
val added = if (game.library == Library.ADDED) runCatching { com.droiddeck.launcher.session.NoSteamLaunch.addedGame(app, game) }.getOrNull() else null
|
||||
val command = com.droiddeck.launcher.session.NoSteamLaunch.command(game, added)
|
||||
ui.post {
|
||||
if (isDestroyed) return@post
|
||||
if (command == null) {
|
||||
android.widget.Toast.makeText(this, R.string.game_link_failed, android.widget.Toast.LENGTH_LONG).show()
|
||||
return@post
|
||||
}
|
||||
com.droiddeck.launcher.session.NoSteamLaunch.remember(command)
|
||||
val next = Intent(this, SessionActivity::class.java)
|
||||
.putExtra(SessionActivity.EXTRA_RETURN_HOME, returnHome)
|
||||
.putExtra(SessionService.EXTRA_MODE, SessionService.MODE_RUN)
|
||||
.putExtra(SessionService.EXTRA_PROGRAM, command.program)
|
||||
.putExtra(SessionService.EXTRA_PROGRAM_ARGS, command.args.toTypedArray())
|
||||
when {
|
||||
!SessionState.running -> startSessionOrWait(next)
|
||||
SessionState.stopRequested -> android.widget.Toast.makeText(this, R.string.game_link_session_busy, android.widget.Toast.LENGTH_LONG).show()
|
||||
SessionState.mode == SessionService.MODE_RUN && SessionState.program == command.program && SessionState.programArgs == command.args ->
|
||||
startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.putExtra(SessionActivity.EXTRA_RETURN_HOME, returnHome)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
else -> pendingSwitch = next
|
||||
}
|
||||
}
|
||||
}, "no-steam-launch").start()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun opensEpicLauncher(next: Intent): Boolean =
|
||||
next.getStringExtra(SessionService.EXTRA_PROGRAM) == com.droiddeck.launcher.session.NoSteamLaunch.EPIC ||
|
||||
next.getStringExtra(SessionService.EXTRA_STEAM_URL) == com.droiddeck.launcher.frontend.GameLaunchLink.steamUrl(
|
||||
com.droiddeck.launcher.stores.epic.EpicClient.gameId(com.droiddeck.launcher.stores.epic.EpicClient.shortcutAppId))
|
||||
|
||||
private fun uninstallEpicLauncher() {
|
||||
if (com.droiddeck.launcher.stores.epic.EpicClient.uninstalling) return
|
||||
com.droiddeck.launcher.stores.epic.EpicClient.uninstalling = true
|
||||
pendingSwitch?.takeIf(::opensEpicLauncher)?.let { pendingSwitch = null }
|
||||
switchingTo?.takeIf(::opensEpicLauncher)?.let {
|
||||
switchPoll?.let { poll -> ui.removeCallbacks(poll) }
|
||||
switchPoll = null
|
||||
switchingTo = null
|
||||
}
|
||||
if (SessionState.running) SessionService.stop(this)
|
||||
val app = applicationContext
|
||||
val deadline = android.os.SystemClock.uptimeMillis() + SESSION_SWITCH_WAIT_MS
|
||||
Thread({
|
||||
while ((SessionState.running || SessionState.phase !in SESSION_ENDED) && android.os.SystemClock.uptimeMillis() < deadline) {
|
||||
Thread.sleep(SESSION_SWITCH_POLL_MS)
|
||||
}
|
||||
val ended = !SessionState.running && SessionState.phase in SESSION_ENDED
|
||||
val done = try {
|
||||
ended && com.droiddeck.launcher.stores.epic.EpicClient.uninstall(app)
|
||||
} finally {
|
||||
com.droiddeck.launcher.stores.epic.EpicClient.uninstalling = false
|
||||
}
|
||||
ui.post {
|
||||
if (!isDestroyed) refresh()
|
||||
android.widget.Toast.makeText(app, if (done) R.string.epic_client_uninstalled else R.string.epic_client_uninstall_failed,
|
||||
android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "epic-uninstall").start()
|
||||
}
|
||||
|
||||
private fun openEpicLauncher(): Boolean {
|
||||
if (com.droiddeck.launcher.stores.epic.EpicClient.uninstalling) {
|
||||
android.widget.Toast.makeText(this, R.string.epic_client_uninstalling, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
if (protons.protonBusyId != null || ProtonExtras.installInProgress || pkgStage != null) {
|
||||
android.widget.Toast.makeText(this, R.string.main_wait_install_game, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
if (!com.droiddeck.launcher.stores.epic.EpicClient.launcherRunning(this) && !com.droiddeck.launcher.stores.epic.EpicClient.fexReady(this)) {
|
||||
com.droiddeck.launcher.stores.epic.EpicClient.requestFex(this)
|
||||
android.widget.Toast.makeText(this, R.string.epic_client_needs_fex, android.widget.Toast.LENGTH_LONG).show()
|
||||
if (!SessionState.running) startSteamSession()
|
||||
else if (SessionState.mode == SessionService.MODE_STEAM) startActivity(Intent(this, SessionActivity::class.java)
|
||||
.setAction(SessionService.ACTION_RESUME).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
return true
|
||||
}
|
||||
if (com.droiddeck.launcher.session.NoSteamLaunch.enabled(this, com.droiddeck.launcher.stores.epic.EpicClient.shortcutAppId)) {
|
||||
com.droiddeck.launcher.stores.epic.EpicClient.launcherEntry(this, Library.EPIC_LAUNCHER_NAME)?.let { return launchWithoutSteam(it, false) }
|
||||
}
|
||||
val launcher = Intent(this, SessionActivity::class.java)
|
||||
.putExtra(SessionActivity.EXTRA_RETURN_HOME, false)
|
||||
.putExtra(SessionService.EXTRA_STEAM_URL, com.droiddeck.launcher.frontend.GameLaunchLink.steamUrl(
|
||||
com.droiddeck.launcher.stores.epic.EpicClient.gameId(com.droiddeck.launcher.stores.epic.EpicClient.shortcutAppId)))
|
||||
if (SessionState.running && SessionState.mode == SessionService.MODE_STEAM && !SessionState.stopRequested) {
|
||||
val app = applicationContext
|
||||
val shortcut = com.droiddeck.launcher.stores.epic.EpicClient.shortcutAppId
|
||||
val listed = com.droiddeck.launcher.frontend.AddedGames.inSteam(app, shortcut)
|
||||
if (listed && !SessionService.launchGame(this, com.droiddeck.launcher.stores.epic.EpicClient.gameId(shortcut))) {
|
||||
android.widget.Toast.makeText(this, R.string.game_link_failed, android.widget.Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.putExtra(SessionActivity.EXTRA_RETURN_HOME, false)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
if (!listed) Thread({
|
||||
com.droiddeck.launcher.frontend.AddedGamesPublisher.republish(app)
|
||||
val id = com.droiddeck.launcher.stores.epic.EpicClient.steamLauncherId(app) ?: shortcut
|
||||
ui.post { SessionService.launchGame(app, com.droiddeck.launcher.stores.epic.EpicClient.gameId(id)) }
|
||||
}, "epic-launcher-open").start()
|
||||
return true
|
||||
}
|
||||
if (SessionState.running) {
|
||||
if (SessionState.stopRequested) android.widget.Toast.makeText(this, R.string.game_link_session_busy, android.widget.Toast.LENGTH_LONG).show()
|
||||
else pendingSwitch = launcher
|
||||
return true
|
||||
}
|
||||
return startSessionOrWait(launcher)
|
||||
}
|
||||
|
||||
private fun startSessionOrWait(next: Intent): Boolean {
|
||||
val started = startSession(next, steamSession = next.hasExtra(SessionService.EXTRA_STEAM_URL))
|
||||
if (!started && (busy || LinuxRuntimeInstaller.isBusy())) android.widget.Toast.makeText(this, R.string.main_wait_install_game, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return started
|
||||
}
|
||||
|
||||
private fun startAfterSession(next: Intent) {
|
||||
SessionService.stop(this)
|
||||
awaitSessionEnd(next, android.os.SystemClock.uptimeMillis() + SESSION_SWITCH_WAIT_MS)
|
||||
}
|
||||
|
||||
private fun awaitSessionEnd(next: Intent, deadline: Long) {
|
||||
switchingTo = next
|
||||
switchDeadline = deadline
|
||||
switchPoll?.let { ui.removeCallbacks(it) }
|
||||
val poll = object : Runnable {
|
||||
override fun run() {
|
||||
if (isDestroyed) return
|
||||
if (SessionState.running || SessionState.phase !in SESSION_ENDED) {
|
||||
if (android.os.SystemClock.uptimeMillis() < deadline) ui.postDelayed(this, SESSION_SWITCH_POLL_MS)
|
||||
else {
|
||||
switchPoll = null
|
||||
switchingTo = null
|
||||
android.widget.Toast.makeText(this@MainActivity, R.string.game_link_session_busy, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
return
|
||||
}
|
||||
switchPoll = null
|
||||
switchingTo = null
|
||||
if (com.droiddeck.launcher.stores.epic.EpicClient.uninstalling && opensEpicLauncher(next)) return
|
||||
startSessionOrWait(next)
|
||||
}
|
||||
}
|
||||
switchPoll = poll
|
||||
ui.postDelayed(poll, SESSION_SWITCH_POLL_MS)
|
||||
.putExtra(SessionService.EXTRA_STEAM_URL,
|
||||
com.droiddeck.launcher.frontend.GameLaunchLink.steamUrl(game.gameIdString)), steamSession = true)
|
||||
}
|
||||
|
||||
private fun handleGameLink(games: List<Library.SteamGame>) {
|
||||
@@ -744,19 +509,12 @@ class MainActivity : ComponentActivity() {
|
||||
theme = SessionPrefs.theme(this)
|
||||
frameGenLabel = getString(R.string.frame_gen_off)
|
||||
appScale = com.droiddeck.launcher.core.AppUiPrefs.scale(this)
|
||||
val (cached, cachedX64) = com.droiddeck.launcher.frontend.LibraryCache.snapshot(this)
|
||||
steamGames = if (shortcutPicker) emptyList() else cached
|
||||
steamGamesX64 = cachedX64
|
||||
steamGames = if (shortcutPicker) emptyList() else com.droiddeck.launcher.frontend.LibraryCache.load(this)
|
||||
backActionsInverted = SessionPrefs.backActionsInverted(this)
|
||||
orientation = SessionPrefs.orientation(this)
|
||||
requestedOrientation = orientation
|
||||
launcherFullscreen = SessionPrefs.launcherFullscreen(this)
|
||||
downloadSource = DownloadSource.mode(this)
|
||||
downloadMirror = DownloadSource.mirror(this)
|
||||
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)
|
||||
@@ -784,9 +542,8 @@ class MainActivity : ComponentActivity() {
|
||||
busy = busy, stage = stage, percent = percent,
|
||||
desktopInstalled = desktopInstalled, desktopPresent = desktopPresent,
|
||||
offlineAccount = offlineAccount, offline = offline,
|
||||
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled,
|
||||
vacDiagnostics = vacDiagnostics, agentCommands = agentCommands,
|
||||
steamGames = steamGames, steamGamesX64 = steamGamesX64, emulators = emulatorList, running = runningLabel,
|
||||
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled, agentCommands = agentCommands,
|
||||
steamGames = steamGames, emulators = emulatorList, running = runningLabel,
|
||||
frameGen = FrameGen.mode(this),
|
||||
lossless = lossless,
|
||||
pageKey = if (showPerformance) "performance" else if (showComponents) "components" else if (showMapping) "controller-mapping" else null,
|
||||
@@ -813,11 +570,7 @@ class MainActivity : ComponentActivity() {
|
||||
showPhantomGate = showPhantomGate,
|
||||
orientation = orientation,
|
||||
launcherFullscreen = launcherFullscreen,
|
||||
downloadSource = downloadSource,
|
||||
downloadMirror = downloadMirror,
|
||||
downloadPrefix = downloadPrefix,
|
||||
animationsEnabled = animationsEnabled,
|
||||
launcherIcon = launcherIcon,
|
||||
gameStoresEnabled = gameStoresEnabled,
|
||||
gameStoresSpeedTier = gameStoresSpeedTier,
|
||||
storesOpenTab = storesOpenTab,
|
||||
@@ -830,13 +583,7 @@ class MainActivity : ComponentActivity() {
|
||||
storeDownloadsActive = com.droiddeck.launcher.stores.StoresState.activeDownloads,
|
||||
),
|
||||
FrontEndActions(
|
||||
onPlay = { playSteam() },
|
||||
onPlaySteamVr = { playSteamVr() },
|
||||
onSteamUrl = { url ->
|
||||
if (!SessionState.running) startSession(
|
||||
Intent(this, SessionActivity::class.java).putExtra(SessionService.EXTRA_STEAM_URL, url), steamSession = true,
|
||||
)
|
||||
},
|
||||
onPlay = { startSteamSession() },
|
||||
onPlayDesktopUi = {
|
||||
startSession(Intent(this, SessionActivity::class.java)
|
||||
.putExtra(SessionService.EXTRA_MODE, SessionService.MODE_STEAM)
|
||||
@@ -849,8 +596,6 @@ class MainActivity : ComponentActivity() {
|
||||
com.droiddeck.launcher.stores.StoreLaunch.prepare(this, g) { launchGame(g) }
|
||||
}
|
||||
},
|
||||
onEpicLauncher = { openEpicLauncher() },
|
||||
onUninstallEpicLauncher = { uninstallEpicLauncher() },
|
||||
onGameShortcut = { g -> com.droiddeck.launcher.frontend.GameShortcuts.pin(this, g) },
|
||||
onExportGameFile = { g -> pickGameExport(g) },
|
||||
onSyncGameFiles = { pickGameExport(null) },
|
||||
@@ -859,9 +604,7 @@ class MainActivity : ComponentActivity() {
|
||||
val known = steamGames.mapNotNull { it.gameFiles?.path }.toSet()
|
||||
updateAddedGames(folders, then = { if (folders.any { it !in known }) fetchAddedGameArt() })
|
||||
},
|
||||
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({
|
||||
@@ -923,7 +666,6 @@ 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) } },
|
||||
@@ -938,10 +680,6 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setLogsEnabled(this, !SessionPrefs.logsEnabled(this))
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
},
|
||||
onVacDiagnostics = {
|
||||
SessionPrefs.setVacDiagnostics(this, !SessionPrefs.vacDiagnostics(this))
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
},
|
||||
onAgentCommands = {
|
||||
com.droiddeck.launcher.agent.AgentAccess.setCommandsEnabled(this, !com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this))
|
||||
agentCommands = com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this)
|
||||
@@ -973,7 +711,6 @@ 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)
|
||||
@@ -1006,18 +743,6 @@ class MainActivity : ComponentActivity() {
|
||||
animationsEnabled = on
|
||||
com.droiddeck.launcher.ui.Motion.refresh(this)
|
||||
},
|
||||
onDownloadSource = { mode ->
|
||||
DownloadSource.setMode(this, mode)
|
||||
downloadSource = mode
|
||||
},
|
||||
onDownloadMirror = { host ->
|
||||
DownloadSource.setMirror(this, host)
|
||||
downloadMirror = host
|
||||
},
|
||||
onDownloadPrefix = { prefix ->
|
||||
DownloadSource.setCustom(this, prefix)
|
||||
downloadPrefix = DownloadSource.custom(this)
|
||||
},
|
||||
onHomeApp = { manageHomeApp() },
|
||||
onHomeScreen = { on ->
|
||||
HomeApp.setHomeScreenEnabled(this, on)
|
||||
@@ -1091,7 +816,6 @@ class MainActivity : ComponentActivity() {
|
||||
onQamButton = { on -> ControllerPrefs.setQamButton(this, on); refreshController() },
|
||||
onKeyboardButton = { on -> ControllerPrefs.setKeyboardButton(this, on); refreshController() },
|
||||
onSelectSteam = { on -> ControllerPrefs.setSelectSteam(this, on); refreshController() },
|
||||
onJoyConVr = { on -> ControllerPrefs.setJoyConVr(this, on); refreshController() },
|
||||
onEditLayout = { startActivity(Intent(this, ControllerEditorActivity::class.java)) },
|
||||
onResetLayout = { ControllerPrefs.resetAllLayouts(this); refreshController() },
|
||||
onMapping = { showPerformance = false; showComponents = false; showMapping = true },
|
||||
@@ -1119,22 +843,6 @@ class MainActivity : ComponentActivity() {
|
||||
onDismiss = { showNonAdreno = null },
|
||||
)
|
||||
}
|
||||
pendingSwitch?.let { next ->
|
||||
ConfirmDialog(
|
||||
title = androidx.compose.ui.res.stringResource(R.string.drawer_stop_title),
|
||||
text = runningLabel ?: "",
|
||||
confirm = androidx.compose.ui.res.stringResource(R.string.drawer_stop),
|
||||
onConfirm = { pendingSwitch = null; startAfterSession(next) },
|
||||
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),
|
||||
@@ -1142,20 +850,6 @@ class MainActivity : ComponentActivity() {
|
||||
onConfirm = { showRemove = false; removeRuntime() },
|
||||
onDismiss = { showRemove = false },
|
||||
)
|
||||
lowSpace?.let { free ->
|
||||
com.droiddeck.launcher.ui.LowSpaceDialog(
|
||||
free,
|
||||
onFreeUp = { lowSpace = null; com.droiddeck.launcher.storage.StorageState.requestOpen() },
|
||||
onPlay = { lowSpace = null; com.droiddeck.launcher.storage.StorageState.lowSpaceDismissed = true; startSteamSession() },
|
||||
onDismiss = { lowSpace = null },
|
||||
)
|
||||
}
|
||||
com.droiddeck.launcher.ui.HandCameraHost(
|
||||
check = placementCheck,
|
||||
onAskPermission = { cameraPermission.launch(Manifest.permission.CAMERA) },
|
||||
onPlay = { placementCheck = null; startSteamSession(steamVr = true) },
|
||||
onClose = { placementCheck = null },
|
||||
)
|
||||
if (showResetSettings) com.droiddeck.launcher.ui.ResetSettingsDialog(
|
||||
onConfirm = { gameProfiles -> resetSettings(gameProfiles) },
|
||||
onDismiss = { showResetSettings = false },
|
||||
@@ -1173,10 +867,6 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) ui.post { startSteamAtStartupIfEnabled() }
|
||||
else {
|
||||
pendingSwitch = savedIntent(savedInstanceState, KEY_PENDING_SWITCH)
|
||||
savedIntent(savedInstanceState, KEY_SWITCHING_TO)?.let { awaitSessionEnd(it, savedInstanceState.getLong(KEY_SWITCH_DEADLINE)) }
|
||||
}
|
||||
|
||||
val wanted = ArrayList<String>()
|
||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
@@ -1212,23 +902,8 @@ class MainActivity : ComponentActivity() {
|
||||
if (SessionPrefs.runSteamAtStartup(this) && !isFinishing && !isDestroyed) startSteamSession()
|
||||
}
|
||||
|
||||
private fun playSteam() {
|
||||
val storage = com.droiddeck.launcher.storage.StorageState
|
||||
if (!SessionState.running && !storage.lowSpaceDismissed && storage.lowOnSpace(this)) {
|
||||
lowSpace = storage.freeBytes(this)
|
||||
storage.refresh(this)
|
||||
} else startSteamSession()
|
||||
}
|
||||
|
||||
private fun playSteamVr() {
|
||||
val tracking = com.droiddeck.launcher.glasses.Glasses.settings.value
|
||||
if (!com.droiddeck.launcher.glasses.Glasses.state.value.usesSpatialControllers(tracking) &&
|
||||
com.droiddeck.launcher.handtracking.HandTracking.wanted(this)) placementCheck = true
|
||||
else startSteamSession(steamVr = true)
|
||||
}
|
||||
|
||||
private fun startSteamSession(steamVr: Boolean = false) {
|
||||
startSession(Intent(this, SessionActivity::class.java).putExtra(SessionActivity.EXTRA_STEAMVR, steamVr), steamSession = true)
|
||||
private fun startSteamSession() {
|
||||
startSession(Intent(this, SessionActivity::class.java), steamSession = true)
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
@@ -1290,8 +965,7 @@ class MainActivity : ComponentActivity() {
|
||||
refreshPhantomStatus()
|
||||
syncLossless()
|
||||
updates.onResume()
|
||||
drivers.checkOnOpen()
|
||||
resumeInterruptedPackages()
|
||||
drivers.ensureAuto(force = false)
|
||||
protons.checkUpdates()
|
||||
if (!SessionState.running) Thread({
|
||||
val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList())
|
||||
@@ -1326,19 +1000,6 @@ class MainActivity : ComponentActivity() {
|
||||
refreshSecondScreenDisplays()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
pendingSwitch?.let { outState.putParcelable(KEY_PENDING_SWITCH, it) }
|
||||
switchingTo?.let {
|
||||
outState.putParcelable(KEY_SWITCHING_TO, it)
|
||||
outState.putLong(KEY_SWITCH_DEADLINE, switchDeadline)
|
||||
}
|
||||
}
|
||||
|
||||
private fun savedIntent(state: Bundle, key: String): Intent? =
|
||||
if (Build.VERSION.SDK_INT >= 33) state.getParcelable(key, Intent::class.java)
|
||||
else @Suppress("DEPRECATION") state.getParcelable(key)
|
||||
|
||||
override fun onDestroy() {
|
||||
updates.unregister()
|
||||
super.onDestroy()
|
||||
@@ -1366,9 +1027,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun refreshSecondScreenDisplays() {
|
||||
if (!::displayManager.isInitialized) return
|
||||
// The launcher can itself be shown on a non-default display (DeX, an external monitor); that one is not a "second screen".
|
||||
val ownDisplayId = if (android.os.Build.VERSION.SDK_INT >= 30) display?.displayId ?: android.view.Display.DEFAULT_DISPLAY else @Suppress("DEPRECATION") windowManager.defaultDisplay.displayId
|
||||
secondScreenDisplays = SecondScreenDisplays.available(displayManager, ownDisplayId)
|
||||
secondScreenDisplays = SecondScreenDisplays.available(displayManager)
|
||||
}
|
||||
|
||||
private fun launchAndroidApp(app: HomeApp.LaunchableApp, displayId: Int?) {
|
||||
@@ -1436,9 +1095,8 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun openComponents(focusContent: Boolean = true, tab: String? = null, gameKey: String? = null) {
|
||||
private fun openComponents(focusContent: Boolean = true, tab: String? = null) {
|
||||
focusComponentsContent = focusContent
|
||||
componentsGameKey = gameKey
|
||||
if (tab != null) components.compComp = tab
|
||||
showPerformance = false
|
||||
showMapping = false
|
||||
@@ -1487,7 +1145,6 @@ 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(),
|
||||
@@ -1504,8 +1161,6 @@ class MainActivity : ComponentActivity() {
|
||||
onImportAndroid = { pickAndroidDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_android))) },
|
||||
onImportZip = { pickAnyDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_any))) },
|
||||
onRestoreBundled = { TurnipDriver(this).restoreBundled(); drivers.refreshDrivers() },
|
||||
onMode = { m -> drivers.chooseMode(m) },
|
||||
onRestart = { drivers.restartApp() },
|
||||
),
|
||||
protons = com.droiddeck.launcher.ui.ProtonsState(
|
||||
rows = protons.protonRows, busyId = protons.protonBusyId, stage = protons.protonStage, percent = protons.protonPercent,
|
||||
@@ -1547,27 +1202,6 @@ 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
|
||||
@@ -1630,16 +1264,10 @@ class MainActivity : ComponentActivity() {
|
||||
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
|
||||
forceSsbs = forceSsbs,
|
||||
syncBackend = if (mode == SessionService.MODE_STEAM) SessionPrefs.syncBackendOf(fastSync, fsyncFirst, syncFallback) else null,
|
||||
steamClientArch = if (mode == SessionService.MODE_STEAM) steamClientArch else null,
|
||||
steamRepairQueued = steamRepairQueued,
|
||||
mangoapp = mangoapp,
|
||||
steamController = if (mode == SessionService.MODE_STEAM) steamController else null,
|
||||
runSteamAtStartup = mode == SessionService.MODE_STEAM && runSteamAtStartup,
|
||||
bootMovie = if (mode == SessionService.MODE_STEAM) bootMovie else null,
|
||||
bootMovieSound = bootMovieSound,
|
||||
bootMovieCustomName = bootMovieCustomName,
|
||||
bootMovieImporting = bootMovieImporting,
|
||||
bootMovieProgress = bootMovieProgress,
|
||||
wifiDiscovery = if (mode == SessionService.MODE_STEAM) wifiDiscovery else null,
|
||||
wifiDiscoveryPermission = wifiDiscoveryPermission,
|
||||
wifiDiscoveryLocation = wifiDiscoveryLocation,
|
||||
@@ -1714,19 +1342,12 @@ class MainActivity : ComponentActivity() {
|
||||
syncFallback = SessionPrefs.syncFallback(this)
|
||||
},
|
||||
onSteamRepair = { steamRepairQueued = SteamRepair.queue(this) },
|
||||
onSteamClientArch = { id -> SessionPrefs.setSteamClientArch(this, id); steamClientArch = id; refresh() },
|
||||
onMangoapp = { on -> SessionPrefs.setMangoapp(this, on); mangoapp = on },
|
||||
onSteamController = { id -> SessionPrefs.setSteamController(this, id); steamController = id },
|
||||
onRunSteamAtStartup = { on ->
|
||||
SessionPrefs.setRunSteamAtStartup(this, on)
|
||||
runSteamAtStartup = on
|
||||
},
|
||||
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)))
|
||||
},
|
||||
onBootMovieChanged = { loadBootMovie() },
|
||||
onDeckyInstall = { release -> decky.installDecky(release) },
|
||||
onDeckyCheck = { decky.refreshDecky() },
|
||||
onDeckyEnabled = { enabled -> DeckyManager.setSupervisorEnabled(this, enabled); decky.deckySupervisor = enabled },
|
||||
@@ -1797,44 +1418,6 @@ class MainActivity : ComponentActivity() {
|
||||
}, "added-games-update").start()
|
||||
}
|
||||
|
||||
private val steamArtRefreshes = HashMap<Int, Int>()
|
||||
|
||||
private fun refreshSteamGameArt(appId: Int) {
|
||||
val version = (steamArtRefreshes[appId] ?: 0) + 1
|
||||
steamArtRefreshes[appId] = version
|
||||
Thread({
|
||||
val fresh = com.droiddeck.launcher.frontend.Library.steamGames(this).firstOrNull { it.appId == appId } ?: return@Thread
|
||||
ui.post {
|
||||
if (steamArtRefreshes[appId] != version) return@post
|
||||
steamGames = steamGames.map { if (it.appId == appId && it.library != Library.ADDED) fresh else it }
|
||||
}
|
||||
}, "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()
|
||||
@@ -1878,12 +1461,10 @@ class MainActivity : ComponentActivity() {
|
||||
fastSync = SessionPrefs.fastSync(this)
|
||||
fsyncFirst = SessionPrefs.fsyncFirst(this)
|
||||
syncFallback = SessionPrefs.syncFallback(this)
|
||||
steamClientArch = SessionPrefs.steamClientArch(this)
|
||||
steamRepairQueued = SteamRepair.queued(this)
|
||||
mangoapp = SessionPrefs.mangoapp(this)
|
||||
steamController = SessionPrefs.steamController(this)
|
||||
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
|
||||
loadBootMovie()
|
||||
hdrOn = SessionPrefs.hdr(this, mode)
|
||||
fpsLimit = SessionPrefs.fpsLimit(this, mode)
|
||||
upscaler = SessionPrefs.upscaler(this)
|
||||
@@ -1955,14 +1536,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
// Until the scan finishes, the other client's games would launch in this one.
|
||||
val listX64 = SessionPrefs.activeSteamClientArch(this) == "x86_64"
|
||||
if (listX64 != steamGamesX64) {
|
||||
steamGamesX64 = listX64
|
||||
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()
|
||||
addedGamesArt = SessionPrefs.addedGamesArt(this)
|
||||
desktopInstalled = DesktopCatalog.desktopInstalled(this)
|
||||
desktopPresent = DesktopCatalog.desktopPresent(this)
|
||||
@@ -1974,7 +1548,6 @@ class MainActivity : ComponentActivity() {
|
||||
frameGenLabel = FrameGen.label(this)
|
||||
romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
agentCommands = com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this)
|
||||
runningLabel = if (SessionState.running) when (SessionState.mode) {
|
||||
SessionService.MODE_DESKTOP -> getString(R.string.rail_desktop)
|
||||
@@ -1983,33 +1556,26 @@ class MainActivity : ComponentActivity() {
|
||||
else -> "Steam"
|
||||
} else null
|
||||
val scanGeneration = ++libraryScanGeneration
|
||||
val artRefreshesAtStart = HashMap(steamArtRefreshes)
|
||||
if (shortcutPicker) shortcutLibraryScanning = true
|
||||
if (SessionPrefs.steamClientArch(this) == "x86_64" && !OfflineMode.enabled(this)) {
|
||||
Thread({ com.droiddeck.launcher.session.FexCatalog.refresh(this) }, "fex-catalog").start()
|
||||
}
|
||||
Thread({
|
||||
val emus = Library.emulators(this) { id -> DesktopCatalog.installed(this, id) != null }
|
||||
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 && it.gameFiles?.absolutePath.let { path -> path == null || path !in claimed } }).distinctBy { it.gameId }
|
||||
steamGames = (quick + steamGames.filter { it.library == Library.ADDED }).distinctBy { it.gameId }
|
||||
fresh.forEach { com.droiddeck.launcher.frontend.AutoComponents.queueGame(this, it, asked = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val scannedArch = SessionPrefs.activeSteamClientArch(this)
|
||||
val scannedX64 = scannedArch == "x86_64"
|
||||
val games = if (ready) Library.launchableGames(this, arch = scannedArch) else emptyList()
|
||||
val games = if (ready) Library.launchableGames(this) else emptyList()
|
||||
val all = games.distinctBy { it.gameId }
|
||||
if (ready && scanGeneration == libraryScanGeneration) com.droiddeck.launcher.frontend.LibraryCache.save(this, all, scannedX64)
|
||||
if (ready) com.droiddeck.launcher.frontend.LibraryCache.save(this, all)
|
||||
val endedSession = SessionState.sessionId.takeIf { !SessionState.running && it != null && it != componentSweepSession }
|
||||
if (ready && (!componentSweepDone || endedSession != null)) {
|
||||
componentSweepDone = true
|
||||
@@ -2018,12 +1584,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
ui.post {
|
||||
if (scanGeneration == libraryScanGeneration) {
|
||||
val current = steamGames.filter { it.library != Library.ADDED }.associateBy { it.appId }
|
||||
steamGamesX64 = scannedX64
|
||||
steamGames = all.map { g ->
|
||||
val newer = current[g.appId]
|
||||
if (newer != null && g.library != Library.ADDED && steamArtRefreshes[g.appId] != artRefreshesAtStart[g.appId]) g.withArt(newer.art, newer.hero, newer.icon) else g
|
||||
}
|
||||
steamGames = all
|
||||
shortcutLibraryScanning = false
|
||||
handleGameLink(all)
|
||||
}
|
||||
@@ -2038,14 +1599,10 @@ class MainActivity : ComponentActivity() {
|
||||
private fun onRuntimeButton() {
|
||||
if (runtimeChangesBlocked()) return
|
||||
val release = available
|
||||
if (removalPending) {
|
||||
if (removalPending || installed != null && release?.version == installed) {
|
||||
showRemove = true
|
||||
return
|
||||
}
|
||||
if (installed != null && release?.version == installed) {
|
||||
showRuntimeActions = release
|
||||
return
|
||||
}
|
||||
if (release == null) {
|
||||
Thread({ checkCatalog() }, "catalog").start()
|
||||
return
|
||||
@@ -2056,10 +1613,6 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun startSession(intent: Intent, steamSession: Boolean = false): Boolean {
|
||||
if (busy || LinuxRuntimeInstaller.isBusy()) return false
|
||||
if (com.droiddeck.launcher.storage.StorageState.cleaning) {
|
||||
android.widget.Toast.makeText(this, R.string.storage_cleanup_busy, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
refreshPhantomStatus()
|
||||
if (steamSession && PhantomProcessLimit.blocksSteam(phantomProcessStatus)) {
|
||||
showPhantomGate = true
|
||||
@@ -2126,7 +1679,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)
|
||||
if (!switchLauncherIcon(LauncherIcon.DARK)) recreate()
|
||||
recreate()
|
||||
}
|
||||
}, "reset-settings").start()
|
||||
}
|
||||
@@ -2173,12 +1726,6 @@ class MainActivity : ComponentActivity() {
|
||||
companion object {
|
||||
private const val TAG = "MainActivity"
|
||||
private const val MEDIA_SETTLE_MS = 1500L
|
||||
private const val SESSION_SWITCH_WAIT_MS = 30_000L
|
||||
private const val SESSION_SWITCH_POLL_MS = 250L
|
||||
private const val KEY_PENDING_SWITCH = "pending_switch"
|
||||
private const val KEY_SWITCHING_TO = "switching_to"
|
||||
private const val KEY_SWITCH_DEADLINE = "switch_deadline"
|
||||
private val SESSION_ENDED = setOf(SessionPhase.IDLE, SessionPhase.FAILED)
|
||||
private val ZIP_EXT = listOf("zip")
|
||||
private val WCP_EXT = listOf("wcp")
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class QuestXrActivity : NativeActivity() {
|
||||
if (active) {
|
||||
check(CompositorHost.isStarted) { "DroidDeck compositor is not running" }
|
||||
SessionService.resume(this)
|
||||
WaylandCompositor.nativeSetCardboard(false, 1, 0.064f, 3f, 4f, 0f, 0f, 90f, 0f, 0f, false, 0f)
|
||||
WaylandCompositor.nativeSetCardboard(false, 1, 0.064f, 3f, 4f, 0f, 0f, 90f, 0f, 0f)
|
||||
WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0)
|
||||
WaylandCompositor.nativeSetZeroCopy(false)
|
||||
CompositorHost.attachXr()
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -92,111 +92,11 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
AgentEnv.override(context, request.optString("name")).put("ok", true).put("command", "override")
|
||||
}
|
||||
"prefs" -> prefs(context, request)
|
||||
"glasses" -> glasses(context, request)
|
||||
"access" -> JSONObject().put("ok", true).put("commands", AgentAccess.commandsAllowed(context))
|
||||
.put("debugBuild", BuildConfig.DEBUG).put("inbox", AgentEnv.inbox(context).absolutePath)
|
||||
else -> error("UNKNOWN_COMMAND", "Unknown agent command '$method'")
|
||||
}
|
||||
|
||||
private fun glasses(context: Context, request: JSONObject): JSONObject {
|
||||
val g = com.droiddeck.launcher.glasses.Glasses
|
||||
when (request.optString("op", "status")) {
|
||||
"status" -> {}
|
||||
"allow" -> g.requestControl()
|
||||
"set" -> {
|
||||
val key = request.optString("key")
|
||||
val value = request.optString("value")
|
||||
fun on() = value == "1" || value.equals("true", true) || value.equals("on", true)
|
||||
val number = value.toIntOrNull()
|
||||
val numeric = setOf("refreshHz", "sbsHz", "brightness", "volume", "picture3d", "screenWidthDm", "screenDistanceDm", "vrFovDeg", "sbsSupport", "eyeHeightCm")
|
||||
if (key in numeric && number == null) return error("INVALID_VALUE", "'$key' takes a number")
|
||||
when (key) {
|
||||
"refreshHz" -> g.setRefreshHz(number!!)
|
||||
"sbsHz" -> g.setSbsHz(number!!)
|
||||
"resolution2d" -> g.setResolution(com.droiddeck.launcher.glasses.GlassesDisplayProfile.TWO_D, value)
|
||||
"resolution3d" -> g.setResolution(com.droiddeck.launcher.glasses.GlassesDisplayProfile.THREE_D, value)
|
||||
"resolutionVr" -> g.setResolution(com.droiddeck.launcher.glasses.GlassesDisplayProfile.VR, value)
|
||||
"brightness" -> g.setBrightness(number!!)
|
||||
"volume" -> g.setVolume(number!!)
|
||||
"sunblock" -> g.setSunblock(on())
|
||||
"3d" -> g.set3d(on())
|
||||
"3dof" -> g.set3dof(on())
|
||||
"gameOnGlasses" -> g.setGameOnGlasses(on())
|
||||
"headTracking" -> g.setHeadTracking(on())
|
||||
"auto3d" -> g.setAuto3d(on())
|
||||
"picture3d" -> g.setPicture3d(number!!)
|
||||
"screenWidthDm" -> g.setScreenWidthDm(number!!)
|
||||
"screenDistanceDm" -> g.setScreenDistanceDm(number!!)
|
||||
"vrFovDeg" -> g.setVrFovDeg(number!!)
|
||||
"sbsSupport" -> g.setSbsSupport(number!!)
|
||||
"positionalTracking" -> g.setPositionalTracking(on())
|
||||
"handControllers" -> g.setHandControllers(on())
|
||||
"joyConTracking" -> g.setJoyConTracking(on())
|
||||
"eyeHeightCm" -> g.setEyeHeight(number!!)
|
||||
else -> return error("INVALID_KEY", "Unknown glasses setting '$key'")
|
||||
}
|
||||
}
|
||||
"simulate" -> {
|
||||
fun angle(key: String) = if (request.has(key)) request.optDouble(key).takeIf { it.isFinite() }?.coerceIn(-90.0, 90.0) else null
|
||||
if (!g.simulate(request.optInt("seconds", 20).coerceIn(1, 600), angle("yaw"), angle("pitch"))) return error("BUSY", "Already simulating")
|
||||
}
|
||||
"stop-simulate" -> g.stopSimulation()
|
||||
"calibrate" -> g.startCalibration()
|
||||
"cancel-calibration" -> g.cancelCalibration()
|
||||
"reset-calibration" -> g.resetCalibration()
|
||||
"spatial-diagnostics" -> com.droiddeck.launcher.glasses.SpatialTracking.diagnostics(request.optBoolean("enabled",true))
|
||||
"spatial-recenter" -> com.droiddeck.launcher.glasses.SpatialTracking.recenter(request.optBoolean("screen",false))
|
||||
"spatial-floor" -> g.setSpatialFloor()
|
||||
"spatial-restart" -> g.restartSpatialTracking()
|
||||
"spatial-hand-options" -> {
|
||||
AgentAccess.requireCommands(context)
|
||||
if(request.has("lite")) com.droiddeck.launcher.glasses.SpatialTracking.setHandModel(request.getBoolean("lite"))
|
||||
if(request.has("parallel")) com.droiddeck.launcher.glasses.SpatialTracking.setHandCpuParallel(request.getBoolean("parallel"))
|
||||
if(request.has("halfRate")) com.droiddeck.launcher.glasses.SpatialTracking.setHandHalfRate(request.getBoolean("halfRate"))
|
||||
if(request.has("adaptive")) com.droiddeck.launcher.glasses.SpatialTracking.setHandAdaptiveInput(request.getBoolean("adaptive"))
|
||||
if(request.has("scheduled")) com.droiddeck.launcher.glasses.SpatialTracking.setHandScheduledTracking(request.getBoolean("scheduled"))
|
||||
}
|
||||
"spatial-hand-benchmark" -> {
|
||||
AgentAccess.requireCommands(context)
|
||||
com.droiddeck.launcher.glasses.SpatialTracking.benchmarkSavedCapture(request.getString("capture"))
|
||||
}
|
||||
"spatial-hand-capture" -> {
|
||||
AgentAccess.requireCommands(context)
|
||||
com.droiddeck.launcher.glasses.SpatialTracking.captureDiagnostic()
|
||||
}
|
||||
"spatial-rgb-capture", "spatial-rgb-status" -> {
|
||||
AgentAccess.requireCommands(context)
|
||||
return (if (request.getString("op") == "spatial-rgb-capture")
|
||||
com.droiddeck.launcher.glasses.LumaRgbDiagnostic.capture(context!!,request.optString("transport","sdk"),
|
||||
request.optInt("width",640),request.optInt("height",480),request.optInt("fps",30),
|
||||
request.optInt("durationMs",5000))
|
||||
else com.droiddeck.launcher.glasses.LumaRgbDiagnostic.status(context!!)).put("ok", true)
|
||||
}
|
||||
"query" -> { g.query(); Thread.sleep(600) }
|
||||
"mode" -> {
|
||||
val mode = hex(request.optString("mode"), 0xFF) ?: return error("INVALID_VALUE", "mode must be one hex byte, e.g. 0x31")
|
||||
g.sendMode(mode)
|
||||
Thread.sleep(600)
|
||||
}
|
||||
"imu" -> return g.sampleImu(request.optLong("ms", 2000).coerceIn(200, 20_000)).put("ok", true).put("command", "glasses")
|
||||
"raw" -> {
|
||||
AgentAccess.requireCommands(context)
|
||||
val msgId = hex(request.optString("msgId"), 0xFFFF) ?: return error("INVALID_VALUE", "msgId must be a hex number up to 0xFFFF")
|
||||
val digits = request.optString("payload").removePrefix("0x")
|
||||
if (digits.length % 2 != 0 || digits.any { Character.digit(it, 16) < 0 }) return error("INVALID_VALUE", "payload must be whole hex bytes")
|
||||
val payload = digits.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
if (!g.sendRaw(msgId, payload)) return error("NOT_CONNECTED", "The glasses' control link is not open")
|
||||
Thread.sleep(300)
|
||||
}
|
||||
else -> return error("INVALID_OP", "Unknown glasses op")
|
||||
}
|
||||
if (request.optString("op") in setOf("set", "allow")) Thread.sleep(400)
|
||||
return g.status().put("ok", true).put("command", "glasses").put("frames", g.recentFrames())
|
||||
}
|
||||
|
||||
private fun hex(text: String, max: Int): Int? =
|
||||
text.removePrefix("0x").removePrefix("0X").takeIf { it.isNotEmpty() }?.toIntOrNull(16)?.takeIf { it in 0..max }
|
||||
|
||||
private fun state(context: Context): JSONObject {
|
||||
val runtimeVersion = LinuxRuntimeInstaller.installedVersion(context)
|
||||
val dir = SessionState.logDirectory ?: SessionState.logFile?.parentFile ?: SessionPaths.sessionFolders(context).lastOrNull()
|
||||
@@ -209,7 +109,6 @@ class AgentBridgeProvider : ContentProvider() {
|
||||
.put("steamUi", SessionState.steamUi ?: JSONObject.NULL)
|
||||
.put("steamUrl", SessionState.steamUrl ?: JSONObject.NULL)
|
||||
.put("suspended", SessionState.suspended)
|
||||
.put("steamVr", SessionState.steamVr)
|
||||
.put("pip", SessionState.pipActive)
|
||||
.put("firstFrame", SessionState.firstFrameSeen)
|
||||
.put("output", JSONArray().put(SessionState.outputSize.first).put(SessionState.outputSize.second))
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
@@ -20,8 +19,6 @@ 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()
|
||||
@@ -35,7 +32,6 @@ object AgentGuest {
|
||||
observer?.stopWatching()
|
||||
observer = null
|
||||
lastFocusKey = null
|
||||
main.post { steamKeyboard = false }
|
||||
val dir = dir(context)
|
||||
dir.deleteRecursively()
|
||||
dir.mkdirs()
|
||||
@@ -47,7 +43,6 @@ 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() }
|
||||
@@ -58,19 +53,9 @@ 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"),
|
||||
@@ -151,12 +136,8 @@ object AgentGuest {
|
||||
return null
|
||||
}
|
||||
|
||||
fun steamEval(context: Context, expression: String, timeoutMs: Long): JSONObject =
|
||||
call(context, JSONObject().put("kind", "cdp").put("expression", expression).put("timeout", timeoutMs / 1000.0),
|
||||
timeoutMs + 2_000, trusted = true)
|
||||
|
||||
fun call(context: Context, request: JSONObject, timeoutMs: Long, trusted: Boolean = false): JSONObject {
|
||||
if (!trusted && request.optString("kind") in setOf("exec", "cdp")) AgentAccess.requireCommands(context)
|
||||
fun call(context: Context, request: JSONObject, timeoutMs: Long): JSONObject {
|
||||
if (request.optString("kind") in setOf("exec", "cdp")) AgentAccess.requireCommands(context)
|
||||
if (!SessionState.running) throw AgentException("NO_ACTIVE_SESSION", "There is no running session")
|
||||
hello(context) ?: throw AgentException("AGENT_UNAVAILABLE", "The guest agent is not running in this session (yet)")
|
||||
val dir = dir(context)
|
||||
|
||||
@@ -97,7 +97,6 @@ class AgentStartActivity : Activity() {
|
||||
request.optString("steamUrl").takeIf { it.startsWith("steam://") }?.let {
|
||||
putExtra(SessionService.EXTRA_STEAM_URL, it)
|
||||
}
|
||||
if (request.optBoolean("steamVr")) putExtra(SessionActivity.EXTRA_STEAMVR, true)
|
||||
}
|
||||
}
|
||||
startActivity(sessionIntent)
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
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>) {
|
||||
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), sha256)
|
||||
fun file(context: Context, relative: String) = File(dir(context), relative)
|
||||
|
||||
fun installed(context: Context): Boolean = available && File(dir(context), COMPLETE).isFile
|
||||
|
||||
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, "$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"))
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,10 @@ package com.droiddeck.launcher.core
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.PerformanceHintManager
|
||||
import android.os.PowerManager
|
||||
import android.os.StatFs
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.Display
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
@@ -37,57 +32,6 @@ object DeviceReport {
|
||||
}
|
||||
}
|
||||
|
||||
/** Name of an ApplicationExitInfo reason code, so a report says "LOW_MEMORY" instead of "3". */
|
||||
internal fun exitReasonName(reason: Int): String = when (reason) {
|
||||
0 -> "UNKNOWN"
|
||||
1 -> "EXIT_SELF"
|
||||
2 -> "SIGNALED"
|
||||
3 -> "LOW_MEMORY"
|
||||
4 -> "CRASH"
|
||||
5 -> "CRASH_NATIVE"
|
||||
6 -> "ANR"
|
||||
7 -> "INITIALIZATION_FAILURE"
|
||||
8 -> "PERMISSION_CHANGE"
|
||||
9 -> "EXCESSIVE_RESOURCE_USAGE"
|
||||
10 -> "USER_REQUESTED"
|
||||
11 -> "USER_STOPPED"
|
||||
12 -> "DEPENDENCY_DIED"
|
||||
13 -> "OTHER"
|
||||
14 -> "FREEZER"
|
||||
15 -> "PACKAGE_STATE_CHANGE"
|
||||
16 -> "PACKAGE_UPDATED"
|
||||
else -> "REASON_$reason"
|
||||
}
|
||||
|
||||
internal fun thermalStatusName(status: Int): String = when (status) {
|
||||
0 -> "NONE"
|
||||
1 -> "LIGHT"
|
||||
2 -> "MODERATE"
|
||||
3 -> "SEVERE"
|
||||
4 -> "CRITICAL"
|
||||
5 -> "EMERGENCY"
|
||||
6 -> "SHUTDOWN"
|
||||
else -> "STATUS_$status"
|
||||
}
|
||||
|
||||
internal fun gameModeName(mode: Int): String = when (mode) {
|
||||
0 -> "UNSUPPORTED"
|
||||
1 -> "STANDARD"
|
||||
2 -> "PERFORMANCE"
|
||||
3 -> "BATTERY"
|
||||
4 -> "CUSTOM"
|
||||
else -> "MODE_$mode"
|
||||
}
|
||||
|
||||
internal fun refreshModesSummary(rates: List<Float>): String =
|
||||
rates.map { Math.round(it) }.distinct().sortedDescending().joinToString(", ") { "$it Hz" }
|
||||
|
||||
internal fun exitSummary(processName: String?, reason: Int, status: Int, description: String?): String =
|
||||
buildString {
|
||||
append(processName ?: "?").append(" ").append(exitReasonName(reason)).append(" status=").append(status)
|
||||
if (!description.isNullOrBlank()) append(" ").append(description.trim())
|
||||
}
|
||||
|
||||
fun build(context: Context, mode: String): String {
|
||||
val b = StringBuilder()
|
||||
fun h(title: String) {
|
||||
@@ -124,20 +68,6 @@ object DeviceReport {
|
||||
k("Fingerprint", Build.FINGERPRINT)
|
||||
k("Kernel", System.getProperty("os.version"))
|
||||
k("ABIs", Build.SUPPORTED_ABIS.joinToString(", "))
|
||||
k("Memory page size", runCatching { android.system.Os.sysconf(android.system.OsConstants._SC_PAGESIZE) }.getOrNull()?.let { "$it bytes" })
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
h("Recent process exits")
|
||||
val exits = runCatching {
|
||||
context.getSystemService(android.app.ActivityManager::class.java).getHistoricalProcessExitReasons(context.packageName, 0, 5)
|
||||
}.getOrNull().orEmpty()
|
||||
if (exits.isEmpty()) k("Exits", "none recorded") else {
|
||||
val stamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||
for (e in exits) {
|
||||
k(stamp.format(Date(e.timestamp)), exitSummary(e.processName, e.reason, e.status, e.description))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h("CPU and memory")
|
||||
k("Cores", CpuCores.all.size)
|
||||
@@ -152,19 +82,6 @@ object DeviceReport {
|
||||
k("RAM total", FileUtils.sizeToString(mi.totalMem))
|
||||
k("RAM available", FileUtils.sizeToString(mi.availMem))
|
||||
}
|
||||
runCatching {
|
||||
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) k("Thermal status", thermalStatusName(pm.currentThermalStatus))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
k("Thermal headroom (10 s)", pm.getThermalHeadroom(10).let { if (it.isNaN()) "not reported" else String.format(Locale.US, "%.2f", it) })
|
||||
}
|
||||
k("Power saver", pm.isPowerSaveMode)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val rate = runCatching { context.getSystemService(PerformanceHintManager::class.java)?.preferredUpdateRateNanos }.getOrNull()
|
||||
k("ADPF hint sessions", if (rate == null || rate <= 0) "not supported" else "supported (preferred update ${rate / 1000} us)")
|
||||
k("Game Mode", runCatching { context.getSystemService(android.app.GameManager::class.java)?.gameMode }.getOrNull()?.let { gameModeName(it) })
|
||||
}
|
||||
runCatching {
|
||||
val fs = StatFs(context.filesDir.path)
|
||||
k("App storage free", FileUtils.sizeToString(fs.availableBlocksLong * fs.blockSizeLong))
|
||||
@@ -182,13 +99,6 @@ object DeviceReport {
|
||||
h("Display")
|
||||
k("Session output", SessionState.outputSize?.let { "${it.first}x${it.second}" })
|
||||
k("Session refresh", String.format(Locale.US, "%.2f Hz", SessionState.refreshHz))
|
||||
runCatching {
|
||||
val display = context.getSystemService(DisplayManager::class.java).getDisplay(Display.DEFAULT_DISPLAY)
|
||||
k("Panel refresh now", String.format(Locale.US, "%.2f Hz", display.refreshRate))
|
||||
k("Panel refresh modes", refreshModesSummary(display.supportedModes.map { it.refreshRate }))
|
||||
}
|
||||
k("peak_refresh_rate", runCatching { Settings.System.getString(context.contentResolver, "peak_refresh_rate") }.getOrNull() ?: "not set")
|
||||
k("refresh_rate_mode", runCatching { Settings.Secure.getString(context.contentResolver, "refresh_rate_mode") }.getOrNull() ?: "not set")
|
||||
val panelSize = com.droiddeck.launcher.session.SessionDisplay.panelSize(context)
|
||||
for ((label, mode) in listOf("Resolution (Steam)" to SessionService.MODE_STEAM, "Resolution (Desktop)" to SessionService.MODE_DESKTOP)) {
|
||||
val choice = SessionPrefs.resolutionChoice(context, mode, panelSize)
|
||||
@@ -200,11 +110,9 @@ object DeviceReport {
|
||||
h("Drivers")
|
||||
val gpu = com.droiddeck.launcher.gpu.GpuInfo.detect()
|
||||
k("GPU", "${gpu.name} · ${gpu.family.label} · ${gpu.supportText}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "")
|
||||
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)), com.droiddeck.launcher.gpu.TurnipReleases.autoPicks(context),
|
||||
com.droiddeck.launcher.gpu.AutoDriverTrial.bad(context))?.let { " (recommended pair: $it)" } ?: ""))
|
||||
com.droiddeck.launcher.gpu.TurnipReleases.cached(context)))?.let { " (recommended pair: $it)" } ?: ""))
|
||||
val turnip = TurnipDriver(context)
|
||||
val androidChoice = SessionPrefs.androidDriver(context)
|
||||
k("Display driver (chosen)", if (androidChoice.isEmpty()) "Auto -> ${turnip.autoId()}" else androidChoice)
|
||||
@@ -225,9 +133,6 @@ 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")
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
object DownloadSource {
|
||||
const val MODE_AUTO = "auto"
|
||||
const val MODE_DIRECT = "direct"
|
||||
const val MODE_MIRROR = "mirror"
|
||||
const val MODE_CUSTOM = "custom"
|
||||
|
||||
private const val PREFS = "download-source"
|
||||
private const val KEY_MODE = "mode"
|
||||
private const val KEY_MIRROR = "mirror"
|
||||
private const val KEY_CUSTOM = "custom"
|
||||
private const val KEY_FASTEST = "fastest"
|
||||
|
||||
private const val PROBE = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck/main/README.md"
|
||||
private const val PROBE_TIMEOUT_MS = 4_000
|
||||
private const val PROBE_COOLDOWN_MS = 30_000L
|
||||
private const val UA = "DroidDeck-Android"
|
||||
|
||||
private val MODES = listOf(MODE_AUTO, MODE_DIRECT, MODE_MIRROR, MODE_CUSTOM)
|
||||
|
||||
private val GITHUB_HOSTS = listOf(
|
||||
"github.com",
|
||||
"raw.githubusercontent.com",
|
||||
"api.github.com",
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
val MIRRORS = listOf(
|
||||
Mirror("gh-proxy.com", true),
|
||||
Mirror("gh-proxy.org", true),
|
||||
Mirror("ghproxy.net", false),
|
||||
Mirror("ghfast.top", false),
|
||||
)
|
||||
|
||||
@Volatile
|
||||
private var app: Context? = null
|
||||
|
||||
@Volatile
|
||||
private var lastGood: String? = null
|
||||
|
||||
private val probing = AtomicBoolean(false)
|
||||
private val probedAt = AtomicLong(0L)
|
||||
|
||||
fun init(context: Context) {
|
||||
app = context.applicationContext
|
||||
lastGood = prefs()?.getString(KEY_FASTEST, null)
|
||||
}
|
||||
|
||||
fun probe() {
|
||||
if (app == null || prefs()?.getString(KEY_MODE, MODE_AUTO) == MODE_DIRECT) return
|
||||
if (!probing.compareAndSet(false, true)) return
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - probedAt.get() < PROBE_COOLDOWN_MS) {
|
||||
probing.set(false)
|
||||
return
|
||||
}
|
||||
probedAt.set(now)
|
||||
Thread {
|
||||
try {
|
||||
val times = ConcurrentHashMap<String, Long>()
|
||||
val sources = listOf<String?>(null) + MIRRORS.map { it.host }
|
||||
val latch = CountDownLatch(sources.size)
|
||||
for (mirror in sources) {
|
||||
Thread {
|
||||
try {
|
||||
val ms = latency(mirror?.let { "https://$it/$PROBE" } ?: PROBE)
|
||||
if (ms > 0) times[mirror ?: ""] = ms
|
||||
} finally {
|
||||
latch.countDown()
|
||||
}
|
||||
}.apply { isDaemon = true }.start()
|
||||
}
|
||||
latch.await(PROBE_TIMEOUT_MS * 3L, TimeUnit.MILLISECONDS)
|
||||
val best = times.minByOrNull { it.value }?.key ?: return@Thread
|
||||
lastGood = best
|
||||
prefs()?.edit()?.putString(KEY_FASTEST, best)?.apply()
|
||||
} finally {
|
||||
probing.set(false)
|
||||
}
|
||||
}.apply { isDaemon = true }.start()
|
||||
}
|
||||
|
||||
private fun latency(url: String): Long {
|
||||
val started = System.nanoTime()
|
||||
var connection: HttpURLConnection? = null
|
||||
return try {
|
||||
val c = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = PROBE_TIMEOUT_MS
|
||||
readTimeout = PROBE_TIMEOUT_MS
|
||||
instanceFollowRedirects = true
|
||||
setRequestProperty("Range", "bytes=0-1023")
|
||||
setRequestProperty("User-Agent", UA)
|
||||
}
|
||||
connection = c
|
||||
if (c.responseCode / 100 != 2) 0L else {
|
||||
c.inputStream.use { it.read() }
|
||||
(System.nanoTime() - started) / 1_000_000
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
0L
|
||||
} finally {
|
||||
runCatching { connection?.disconnect() }
|
||||
}
|
||||
}
|
||||
|
||||
fun isGithub(url: String): Boolean = GITHUB_HOSTS.any { url.startsWith("https://$it/") }
|
||||
|
||||
/** Catalogs carry the digests everything else is checked against, so a mirror must never supply one. */
|
||||
fun isCatalog(url: String): Boolean = CATALOGS.any { url.startsWith(it) }
|
||||
|
||||
fun candidates(target: String): List<String> {
|
||||
// GitHub API metadata carries executable URLs and digests; never proxy it.
|
||||
if (isCatalog(target) || target.startsWith("https://api.github.com/")) return listOf(target)
|
||||
val prefs = prefs() ?: return listOf(target)
|
||||
if (!isGithub(target)) return listOf(target)
|
||||
val api = target.startsWith("https://api.github.com/")
|
||||
val proxies = MIRRORS.filter { !api || it.api }.map { it.proxy(target) }
|
||||
val ordered = when (prefs.getString(KEY_MODE, MODE_AUTO) ?: MODE_AUTO) {
|
||||
MODE_DIRECT -> listOf(target)
|
||||
MODE_MIRROR -> listOfNotNull(picked(prefs, api)?.proxy(target)) + listOf(target) + quickest(proxies)
|
||||
MODE_CUSTOM -> listOfNotNull(customProxy(prefs, target)) + listOf(target) + quickest(proxies)
|
||||
else -> if (api) listOf(target) else listOf(target) + quickest(proxies)
|
||||
}
|
||||
return ordered.distinct()
|
||||
}
|
||||
|
||||
private fun quickest(sources: List<String>): List<String> =
|
||||
sources.sortedBy { if (host(it) == lastGood) 0 else 1 }
|
||||
|
||||
fun succeeded(source: String) {
|
||||
lastGood = host(source)
|
||||
}
|
||||
|
||||
fun failed() {
|
||||
lastGood = null
|
||||
prefs()?.edit()?.remove(KEY_FASTEST)?.apply()
|
||||
}
|
||||
|
||||
fun mode(context: Context): String = prefs(context)?.getString(KEY_MODE, MODE_AUTO) ?: MODE_AUTO
|
||||
|
||||
fun setMode(context: Context, mode: String) {
|
||||
prefs(context)?.edit()?.putString(KEY_MODE, mode.takeIf { it in MODES } ?: MODE_AUTO)?.apply()
|
||||
}
|
||||
|
||||
fun mirror(context: Context): String {
|
||||
val host = prefs(context)?.getString(KEY_MIRROR, "") ?: ""
|
||||
return MIRRORS.firstOrNull { it.host == host }?.host ?: MIRRORS.first().host
|
||||
}
|
||||
|
||||
fun setMirror(context: Context, host: String) {
|
||||
prefs(context)?.edit()?.putString(KEY_MIRROR, host.takeIf { h -> MIRRORS.any { it.host == h } } ?: "")?.apply()
|
||||
}
|
||||
|
||||
fun custom(context: Context): String = prefs(context)?.getString(KEY_CUSTOM, "") ?: ""
|
||||
|
||||
fun setCustom(context: Context, prefix: String) {
|
||||
prefs(context)?.edit()?.putString(KEY_CUSTOM, normalizePrefix(prefix))?.apply()
|
||||
}
|
||||
|
||||
fun reset(context: Context) {
|
||||
lastGood = null
|
||||
prefs(context)?.edit()?.clear()?.apply()
|
||||
}
|
||||
|
||||
private fun prefs(): SharedPreferences? = app?.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
|
||||
private fun prefs(context: Context): SharedPreferences? =
|
||||
context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
|
||||
private fun Mirror.proxy(target: String): String = "https://$host/$target"
|
||||
|
||||
private fun host(url: String): String =
|
||||
MIRRORS.firstOrNull { url.startsWith("https://${it.host}/") }?.host ?: ""
|
||||
|
||||
private fun picked(prefs: SharedPreferences, api: Boolean): Mirror? {
|
||||
val chosen = MIRRORS.firstOrNull { it.host == (prefs.getString(KEY_MIRROR, "") ?: "") } ?: MIRRORS.firstOrNull()
|
||||
return if (api && chosen != null && !chosen.api) MIRRORS.firstOrNull { it.api } else chosen
|
||||
}
|
||||
|
||||
private fun custom(prefs: SharedPreferences): String = prefs.getString(KEY_CUSTOM, "") ?: ""
|
||||
|
||||
private fun customProxy(prefs: SharedPreferences, target: String): String? =
|
||||
normalizePrefix(custom(prefs)).takeIf { it.isNotEmpty() }?.let { "$it/$target" }
|
||||
|
||||
private fun normalizePrefix(value: String): String {
|
||||
val trimmed = value.trim().trimEnd('/')
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("http://")) return ""
|
||||
return if (trimmed.startsWith("https://")) trimmed else "https://$trimmed"
|
||||
}
|
||||
}
|
||||
@@ -6,201 +6,69 @@ import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
public final class Downloader {
|
||||
private static final String TAG = "Downloader";
|
||||
private static final int CONNECT_TIMEOUT_MS = 10_000;
|
||||
private static final int STALL_TIMEOUT_MS = 15_000;
|
||||
private static final int HTTP_RANGE_NOT_SATISFIABLE = 416;
|
||||
private static final int TIMEOUT_MS = 30_000;
|
||||
|
||||
private Downloader() {}
|
||||
|
||||
public static String downloadString(String url) {
|
||||
List<String> sources = DownloadSource.INSTANCE.candidates(url);
|
||||
for (int i = 0; i < sources.size(); i++) {
|
||||
if (i > 0) Log.i(TAG, "retrying " + url + " via " + sources.get(i));
|
||||
Fetch fetch = fetch(sources.get(i));
|
||||
if (fetch.body != null) {
|
||||
DownloadSource.INSTANCE.succeeded(sources.get(i));
|
||||
return fetch.body;
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
connection = open(url);
|
||||
if (connection.getResponseCode() / 100 != 2) {
|
||||
Log.w(TAG, url + " -> HTTP " + connection.getResponseCode());
|
||||
return null;
|
||||
}
|
||||
if (fetch.fatal && sources.get(i).equals(url)) return null;
|
||||
try (InputStream in = connection.getInputStream();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
FileUtils.copy(in, out);
|
||||
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "GET " + url, e);
|
||||
return null;
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
}
|
||||
DownloadSource.INSTANCE.failed();
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Fetches without the mirror list, for a URL whose body is a digest the payload is checked against. */
|
||||
public static String downloadDirect(String url) {
|
||||
return fetch(url).body;
|
||||
}
|
||||
|
||||
public static boolean downloadFile(String url, File destination, boolean resume,
|
||||
java.util.function.Consumer<Float> progress) {
|
||||
return downloadFile(url, destination, resume, null, progress);
|
||||
}
|
||||
|
||||
/** Verify each candidate before accepting it, so a bad mirror can fall back. */
|
||||
public static boolean downloadFile(String url, File destination, boolean resume, String expectedSha256,
|
||||
java.util.function.Consumer<Float> progress) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Package-private for the local multi-source HTTP test.
|
||||
static boolean downloadCandidates(String original, List<String> sources, File destination, boolean resume,
|
||||
String expectedSha256, java.util.function.Consumer<Float> progress) {
|
||||
Identity identity = loadIdentity(destination, resume, expectedSha256);
|
||||
for (int i = 0; i < sources.size(); i++) {
|
||||
String source = sources.get(i);
|
||||
if (i > 0) Log.i(TAG, "retrying " + original + " via " + source);
|
||||
Outcome outcome = attempt(source, destination, resume, identity, expectedSha256, progress);
|
||||
if (outcome == Outcome.DONE) {
|
||||
if (expectedSha256 != null && !expectedSha256.equalsIgnoreCase(Hashes.sha256(destination))) {
|
||||
Log.w(TAG, "checksum mismatch from " + source + "; trying another source");
|
||||
if (!discardPartial(destination, identity)) return false;
|
||||
continue;
|
||||
}
|
||||
resumeFile(destination).delete();
|
||||
DownloadSource.INSTANCE.succeeded(source);
|
||||
if (progress != null) progress.accept(1f);
|
||||
return true;
|
||||
}
|
||||
if (outcome == Outcome.FATAL && source.equals(original)) return false;
|
||||
}
|
||||
DownloadSource.INSTANCE.failed();
|
||||
return false;
|
||||
}
|
||||
|
||||
private enum Outcome { DONE, RETRY, FATAL }
|
||||
|
||||
private static final class Identity {
|
||||
private String source;
|
||||
private String etag;
|
||||
}
|
||||
|
||||
private static File resumeFile(File dest) {
|
||||
return new File(dest.getPath() + ".resume.properties");
|
||||
}
|
||||
|
||||
private static boolean strongETag(String value) {
|
||||
return value != null && !value.isEmpty() && !value.startsWith("W/");
|
||||
}
|
||||
|
||||
/** Only reuse saved bytes when we persisted a strong validator and their source. */
|
||||
private static Identity loadIdentity(File dest, boolean resume, String expectedSha256) {
|
||||
Identity identity = new Identity();
|
||||
File marker = resumeFile(dest);
|
||||
if (!resume || !dest.isFile() || dest.length() == 0) {
|
||||
marker.delete();
|
||||
return identity;
|
||||
}
|
||||
try (FileInputStream in = new FileInputStream(marker)) {
|
||||
Properties p = new Properties();
|
||||
p.load(in);
|
||||
String source = p.getProperty("source");
|
||||
String etag = p.getProperty("etag");
|
||||
String expected = p.getProperty("sha256", "");
|
||||
if (source != null && source.startsWith("http") && strongETag(etag)
|
||||
&& expected.equals(expectedSha256 == null ? "" : expectedSha256.toLowerCase(java.util.Locale.ROOT))) {
|
||||
identity.source = source;
|
||||
identity.etag = etag;
|
||||
return identity;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Legacy partials or interrupted metadata writes have no trustworthy identity.
|
||||
}
|
||||
if (!discardPartial(dest, identity)) Log.w(TAG, "could not discard unidentified partial " + dest);
|
||||
return identity;
|
||||
}
|
||||
|
||||
private static boolean discardPartial(File dest, Identity identity) {
|
||||
identity.source = null;
|
||||
identity.etag = null;
|
||||
File marker = resumeFile(dest);
|
||||
return (!dest.exists() || dest.delete()) && (!marker.exists() || marker.delete());
|
||||
}
|
||||
|
||||
private static void saveIdentity(File dest, Identity identity, String expectedSha256) {
|
||||
File marker = resumeFile(dest);
|
||||
if (!strongETag(identity.etag)) {
|
||||
marker.delete();
|
||||
return;
|
||||
}
|
||||
Properties p = new Properties();
|
||||
p.setProperty("source", identity.source);
|
||||
p.setProperty("etag", identity.etag);
|
||||
p.setProperty("sha256", expectedSha256 == null ? "" : expectedSha256.toLowerCase(java.util.Locale.ROOT));
|
||||
try (FileOutputStream out = new FileOutputStream(marker)) {
|
||||
p.store(out, "DroidDeck partial download validator");
|
||||
} catch (IOException e) {
|
||||
marker.delete(); // No valid metadata means no future resume.
|
||||
Log.w(TAG, "cannot persist download validator", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Outcome attempt(String url, File destination, boolean resume, Identity identity,
|
||||
String expectedSha256, java.util.function.Consumer<Float> progress) {
|
||||
HttpURLConnection connection = null;
|
||||
long have = resume && destination.isFile() ? destination.length() : 0;
|
||||
try {
|
||||
if (have > 0 && (!url.equals(identity.source) || !strongETag(identity.etag))) {
|
||||
// A new source or unvalidated bytes must restart from byte zero.
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
have = 0;
|
||||
}
|
||||
connection = open(url, STALL_TIMEOUT_MS);
|
||||
if (have > 0) {
|
||||
connection.setRequestProperty("Range", "bytes=" + have + "-");
|
||||
connection.setRequestProperty("If-Range", identity.etag);
|
||||
}
|
||||
connection = open(url);
|
||||
if (have > 0) connection.setRequestProperty("Range", "bytes=" + have + "-");
|
||||
int code = connection.getResponseCode();
|
||||
if (have > 0 && code == HTTP_RANGE_NOT_SATISFIABLE) {
|
||||
connection.disconnect();
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
return attempt(url, destination, false, identity, expectedSha256, progress);
|
||||
}
|
||||
if (code / 100 != 2) {
|
||||
Log.w(TAG, url + " -> HTTP " + code);
|
||||
return code == HttpURLConnection.HTTP_NOT_FOUND || code == HttpURLConnection.HTTP_GONE
|
||||
? Outcome.FATAL : Outcome.RETRY;
|
||||
}
|
||||
boolean appending = code == HttpURLConnection.HTTP_PARTIAL;
|
||||
if (appending && !rangeStartsAt(connection.getHeaderField("Content-Range"), have)) {
|
||||
// A 206 that does not begin where the kept bytes end would corrupt the file if appended.
|
||||
connection.disconnect();
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
return attempt(url, destination, false, identity, expectedSha256, progress);
|
||||
destination.delete();
|
||||
return downloadFile(url, destination, false, progress);
|
||||
}
|
||||
String etag = connection.getHeaderField("ETag");
|
||||
if (have > 0 && appending && (!strongETag(etag) || !etag.equals(identity.etag))) {
|
||||
connection.disconnect();
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
return attempt(url, destination, false, identity, expectedSha256, progress);
|
||||
}
|
||||
if (!appending && have > 0) {
|
||||
// If-Range may legitimately return full 200 when representation changed.
|
||||
if (!discardPartial(destination, identity)) return Outcome.RETRY;
|
||||
have = 0;
|
||||
if (code / 100 != 2) {
|
||||
if (have > 0) {
|
||||
connection.disconnect();
|
||||
destination.delete();
|
||||
return downloadFile(url, destination, false, progress);
|
||||
}
|
||||
Log.w(TAG, url + " -> HTTP " + code);
|
||||
return false;
|
||||
}
|
||||
if (!appending) have = 0;
|
||||
long length = connection.getContentLengthLong();
|
||||
long total = appending ? rangeTotal(connection.getHeaderField("Content-Range")) : -1;
|
||||
if (total < 0) total = length < 0 ? -1 : length + have;
|
||||
long total = length < 0 ? -1 : length + have;
|
||||
File parent = destination.getParentFile();
|
||||
if (parent != null && !parent.isDirectory()) parent.mkdirs();
|
||||
identity.source = url;
|
||||
identity.etag = strongETag(etag) ? etag : null;
|
||||
saveIdentity(destination, identity, expectedSha256);
|
||||
if (parent != null && !parent.isDirectory())
|
||||
parent.mkdirs();
|
||||
long written = have;
|
||||
try (InputStream in = new BufferedInputStream(connection.getInputStream(), 1 << 16);
|
||||
OutputStream out = new FileOutputStream(destination, appending)) {
|
||||
@@ -215,11 +83,11 @@ public final class Downloader {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (total > 0 && written < total) return Outcome.RETRY;
|
||||
return Outcome.DONE;
|
||||
if (progress != null) progress.accept(1f);
|
||||
return total <= 0 || written >= total;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "download " + url, e);
|
||||
return Outcome.RETRY;
|
||||
return false;
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
}
|
||||
@@ -232,49 +100,10 @@ public final class Downloader {
|
||||
return m.matches() && Long.parseLong(m.group(1)) == start;
|
||||
}
|
||||
|
||||
/** The complete length in a `Content-Range` header, or -1 when the header leaves it unknown. */
|
||||
static long rangeTotal(String contentRange) {
|
||||
if (contentRange == null) return -1;
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile("^\\s*bytes\\s+\\d+-\\d+/(\\d+)\\s*$").matcher(contentRange);
|
||||
return m.matches() ? Long.parseLong(m.group(1)) : -1;
|
||||
}
|
||||
|
||||
private static final class Fetch {
|
||||
final String body;
|
||||
final boolean fatal;
|
||||
|
||||
Fetch(String body, boolean fatal) {
|
||||
this.body = body;
|
||||
this.fatal = fatal;
|
||||
}
|
||||
}
|
||||
|
||||
private static Fetch fetch(String url) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
connection = open(url, STALL_TIMEOUT_MS);
|
||||
int code = connection.getResponseCode();
|
||||
if (code / 100 != 2) {
|
||||
Log.w(TAG, url + " -> HTTP " + code);
|
||||
return new Fetch(null, code == HttpURLConnection.HTTP_NOT_FOUND || code == HttpURLConnection.HTTP_GONE);
|
||||
}
|
||||
try (InputStream in = connection.getInputStream();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
FileUtils.copy(in, out);
|
||||
return new Fetch(new String(out.toByteArray(), StandardCharsets.UTF_8), false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "GET " + url, e);
|
||||
return new Fetch(null, false);
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpURLConnection open(String url, int readTimeoutMs) throws java.io.IOException {
|
||||
private static HttpURLConnection open(String url) throws java.io.IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(readTimeoutMs);
|
||||
connection.setConnectTimeout(TIMEOUT_MS);
|
||||
connection.setReadTimeout(TIMEOUT_MS);
|
||||
connection.setInstanceFollowRedirects(true);
|
||||
connection.setRequestProperty("User-Agent", "DroidDeck-Android");
|
||||
return connection;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
|
||||
object GameEnvironment {
|
||||
data class Config(
|
||||
val shared: Map<String, String?> = emptyMap(),
|
||||
@@ -29,7 +27,6 @@ object GameEnvironment {
|
||||
|
||||
fun defaults(preset: String, forceSsbs: Boolean = false): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
"MESA_SHADER_CACHE_DISABLE" to "false",
|
||||
"DROIDDECK_ANTICHEAT_OFFLINE" to "1",
|
||||
"VKD3D_FEATURE_LEVEL" to "12_2",
|
||||
"VKD3D_SHADER_MODEL" to "6_9",
|
||||
"DOTNET_GCRegionRange" to "800000000",
|
||||
@@ -86,34 +83,4 @@ object GameEnvironment {
|
||||
}
|
||||
|
||||
const val CUSTOM_FEX_PRESET = "__custom__"
|
||||
|
||||
val syncBackends = listOf(SessionPrefs.SYNC_NTSYNC, SessionPrefs.SYNC_FSYNC, SessionPrefs.SYNC_ESYNC, SessionPrefs.SYNC_WINESERVER)
|
||||
|
||||
val syncKeys = listOf("BL_SYNC", "BL_FSYNC_FIRST", "BL_SYNC_FALLBACK", "PROTON_NO_ESYNC", "PROTON_NO_FSYNC", "PROTON_NO_NTSYNC", "WINEESYNC")
|
||||
|
||||
fun syncEnvironment(backend: String): Map<String, String> {
|
||||
require(backend in syncBackends) { "unknown sync $backend" }
|
||||
return buildMap {
|
||||
put("BL_SYNC", if (backend == SessionPrefs.SYNC_NTSYNC) "1" else "0")
|
||||
put("BL_FSYNC_FIRST", if (backend == SessionPrefs.SYNC_FSYNC) "1" else "0")
|
||||
put("BL_SYNC_FALLBACK", if (backend == SessionPrefs.SYNC_WINESERVER) "0" else "1")
|
||||
if (backend == SessionPrefs.SYNC_FSYNC) put("PROTON_NO_ESYNC", "1")
|
||||
}
|
||||
}
|
||||
|
||||
fun gameSync(config: Config, scope: String): String? {
|
||||
require(scope.isNotEmpty() && validScope(scope))
|
||||
val entries = config.games[scope].orEmpty().filterKeys { it in syncKeys }
|
||||
if (entries.isEmpty()) return null
|
||||
return syncBackends.firstOrNull { entries == syncEnvironment(it) } ?: CUSTOM_SYNC
|
||||
}
|
||||
|
||||
fun withGameSync(config: Config, scope: String, backend: String?): Config {
|
||||
require(scope.isNotEmpty() && validScope(scope))
|
||||
val entries = config.entries(scope).filterKeys { it !in syncKeys }.toMutableMap()
|
||||
if (backend != null) entries += syncEnvironment(backend)
|
||||
return config.withEntries(scope, entries)
|
||||
}
|
||||
|
||||
const val CUSTOM_SYNC = "__custom__"
|
||||
}
|
||||
@@ -32,7 +32,6 @@ 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")),
|
||||
|
||||
@@ -51,29 +51,25 @@ object LogRedactor {
|
||||
@Volatile
|
||||
private var accounts: List<Learned> = emptyList()
|
||||
|
||||
fun learnAccounts(vararg loginUsers: java.io.File) {
|
||||
val names = LinkedHashSet<String>()
|
||||
val kv = Regex("\"(AccountName|PersonaName)\"\\s+\"([^\"]*)\"")
|
||||
var unread = false
|
||||
for (file in loginUsers) {
|
||||
if (!file.isFile) continue
|
||||
val text = try { file.readText() } catch (e: Exception) { unread = true; continue }
|
||||
kv.findAll(text).map { it.groupValues[2].trim() }.filter { it.length >= 3 }.forEach { names += it }
|
||||
fun learnAccounts(loginUsers: java.io.File) {
|
||||
val found = ArrayList<Learned>()
|
||||
try {
|
||||
if (loginUsers.isFile) {
|
||||
val kv = Regex("\"(AccountName|PersonaName)\"\\s+\"([^\"]*)\"")
|
||||
kv.findAll(loginUsers.readText()).map { it.groupValues[2].trim() }.filter { it.length >= 3 }.distinct().forEach { name ->
|
||||
val needs = name.lowercase().takeIf { name.all { it.code < 0x80 } }
|
||||
found += Learned(Regex("(?i)(?<![A-Za-z0-9_])" + Regex.escape(name) + "(?![A-Za-z0-9_])"), needs)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return
|
||||
}
|
||||
val learned = names.map { name ->
|
||||
val needs = name.lowercase().takeIf { name.all { it.code < 0x80 } }
|
||||
Learned(Regex("(?i)(?<![A-Za-z0-9_])" + Regex.escape(name) + "(?![A-Za-z0-9_])"), needs)
|
||||
}
|
||||
// A file that could not be read keeps the names learned from it before.
|
||||
accounts = if (unread) (accounts + learned).distinctBy { it.regex.pattern } else learned
|
||||
accounts = found
|
||||
}
|
||||
|
||||
fun learnFromRuntime(root: java.io.File) {
|
||||
learnOwnAddresses(java.io.File(root, "etc/droiddeck-net"))
|
||||
learnAccounts(
|
||||
java.io.File(root, "root/.local/share/Steam/config/loginusers.vdf"),
|
||||
java.io.File(root, "root/.droiddeck-x64/.local/share/Steam/config/loginusers.vdf"),
|
||||
)
|
||||
learnAccounts(java.io.File(root, "root/.local/share/Steam/config/loginusers.vdf"))
|
||||
}
|
||||
|
||||
@Volatile
|
||||
@@ -146,21 +142,16 @@ object LogRedactor {
|
||||
return out
|
||||
}
|
||||
|
||||
/** The at-rest rewrite reads a file into memory twice; larger logs are only scrubbed on the way out. */
|
||||
const val IN_MEMORY_SCRUB_MAX = 64L * 1024 * 1024
|
||||
|
||||
fun isBinary(file: java.io.File): Boolean = try {
|
||||
file.inputStream().use { input ->
|
||||
fun isText(file: java.io.File): Boolean = try {
|
||||
file.isFile && file.length() < 64L * 1024 * 1024 && file.inputStream().use { input ->
|
||||
val buf = ByteArray(4096)
|
||||
val n = input.read(buf)
|
||||
n > 0 && (0 until n).any { buf[it].toInt() == 0 }
|
||||
n <= 0 || (0 until n).none { buf[it].toInt() == 0 }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
true
|
||||
false
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -181,62 +172,8 @@ object LogRedactor {
|
||||
}
|
||||
}
|
||||
|
||||
private const val SHARE_LINE_MAX = 64 * 1024
|
||||
|
||||
fun scrubForShare(src: java.io.File, out: java.io.Writer) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
src.forEachLine { line -> out.write(redactForShare(line)); out.write("\n") }
|
||||
}
|
||||
|
||||
private fun quoted(value: String): String {
|
||||
|
||||
@@ -26,12 +26,10 @@ object PhantomProcessLimit {
|
||||
usesDeviceConfig(sdk) && enabled -> listOf(
|
||||
"device_config delete activity_manager max_phantom_processes",
|
||||
"device_config set_sync_disabled_for_tests none",
|
||||
"settings put global $SETTING true",
|
||||
)
|
||||
usesDeviceConfig(sdk) -> listOf(
|
||||
"device_config set_sync_disabled_for_tests persistent",
|
||||
"device_config put activity_manager max_phantom_processes $MAX_PHANTOM",
|
||||
"settings put global $SETTING false",
|
||||
)
|
||||
else -> listOf("settings put global $SETTING ${if (enabled) "true" else "false"}")
|
||||
}
|
||||
@@ -59,12 +57,7 @@ object PhantomProcessLimit {
|
||||
if (sdk < Build.VERSION_CODES.S) return PhantomProcessStatus.NOT_APPLICABLE
|
||||
if (usesDeviceConfig(sdk)) {
|
||||
val off = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(PREF_ANDROID_12_OFF, false)
|
||||
val global = try {
|
||||
Settings.Global.getString(context.contentResolver, SETTING)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
return android12Status(off, global, systemProperty(OVERRIDE_PROPERTY))
|
||||
return if (off) PhantomProcessStatus.DISABLED else PhantomProcessStatus.UNREADABLE
|
||||
}
|
||||
val global = try {
|
||||
Settings.Global.getString(context.contentResolver, SETTING)
|
||||
@@ -74,10 +67,6 @@ object PhantomProcessLimit {
|
||||
return status(global, systemProperty(OVERRIDE_PROPERTY))
|
||||
}
|
||||
|
||||
fun android12Status(rememberedOff: Boolean, global: String?, override: String?): PhantomProcessStatus =
|
||||
if (rememberedOff || status(global, override) == PhantomProcessStatus.DISABLED) PhantomProcessStatus.DISABLED
|
||||
else PhantomProcessStatus.UNREADABLE
|
||||
|
||||
fun status(global: String?, override: String?): PhantomProcessStatus {
|
||||
val value = global?.trim()?.lowercase()?.takeIf { it.isNotEmpty() }
|
||||
?: override?.trim()?.lowercase()?.takeIf { it.isNotEmpty() }
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.os.Process
|
||||
import android.util.Log
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
object SessionLogCapture {
|
||||
private const val TAG = "SessionLogCapture"
|
||||
@@ -12,7 +11,6 @@ object SessionLogCapture {
|
||||
private var pid = -1
|
||||
private var writer: BufferedWriter? = null
|
||||
private var target: File? = null
|
||||
private var syncer: Thread? = null
|
||||
|
||||
@Synchronized
|
||||
fun start(target: File) {
|
||||
@@ -39,7 +37,6 @@ object SessionLogCapture {
|
||||
out.write("logcat could not be started; this ROM may not hand an app its own entries.\n")
|
||||
out.flush()
|
||||
}
|
||||
target.parentFile?.let { syncer = startSyncing(it) }
|
||||
Log.i(TAG, "app log -> $target (logcat pid $pid)")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "could not start the app log", e)
|
||||
@@ -52,19 +49,8 @@ object SessionLogCapture {
|
||||
if (t.parentFile?.absolutePath == dir.absolutePath) stop()
|
||||
}
|
||||
|
||||
/** flush() only reaches the page cache, and a hard reset on f2fs lost the last minutes of every
|
||||
* log. Every 2 s the session folder's files (ours and the native parts') are synced to storage. */
|
||||
private fun startSyncing(dir: File): Thread = Thread({
|
||||
while (true) {
|
||||
try { Thread.sleep(2000) } catch (e: InterruptedException) { break }
|
||||
dir.listFiles()?.forEach { runCatching { FileOutputStream(it, true).use { s -> s.fd.sync() } } }
|
||||
}
|
||||
}, "session-log-sync").apply { isDaemon = true; start() }
|
||||
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
syncer?.interrupt()
|
||||
syncer = null
|
||||
target = null
|
||||
if (pid != -1) {
|
||||
try {
|
||||
|
||||
@@ -85,7 +85,7 @@ class AppFilesProvider : DocumentsProvider() {
|
||||
|
||||
override fun deleteDocument(documentId: String) {
|
||||
val file = fileFor(documentId)
|
||||
if (file.canonicalPath == baseCanonical) throw FileNotFoundException("the app folder itself is not deleted")
|
||||
if (file.absolutePath == base.absolutePath) 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 || f.normalize().path != documentId || !inside(f)) throw FileNotFoundException("$documentId is outside the app's files")
|
||||
if (!f.isAbsolute || !inside(f)) throw FileNotFoundException("$documentId is outside the app's files")
|
||||
if (!f.exists()) throw FileNotFoundException("$documentId not found")
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -90,7 +90,6 @@ 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
|
||||
@@ -152,11 +151,8 @@ 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)) }
|
||||
@@ -178,20 +174,16 @@ 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()
|
||||
listingJob?.cancel()
|
||||
listingJob = scope.launch {
|
||||
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)
|
||||
}
|
||||
@@ -260,7 +252,7 @@ fun FileManagerScreen(
|
||||
|
||||
fun performPaste() {
|
||||
val sources = clipboardFiles
|
||||
if (sources.isEmpty() || isOperationRunning || operationJob?.isActive == true || cancelledJob?.isCompleted == false) return
|
||||
if (sources.isEmpty()) return
|
||||
val dstDir = currentDir
|
||||
val cut = isCutOperation
|
||||
|
||||
@@ -269,7 +261,6 @@ 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
|
||||
@@ -305,7 +296,6 @@ 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
|
||||
@@ -314,8 +304,8 @@ fun FileManagerScreen(
|
||||
}
|
||||
val target = dst
|
||||
val ok = withContext(Dispatchers.IO) {
|
||||
if (cut) FileOps.moveWithProgress(src, target, onProgress) { job?.isActive == false }
|
||||
else FileOps.copyWithProgress(src, target, onProgress) { job?.isActive == false }
|
||||
if (cut) FileOps.moveWithProgress(src, target, onProgress)
|
||||
else FileOps.copyWithProgress(src, target, onProgress)
|
||||
}
|
||||
if (ok) done++ else failed++
|
||||
}
|
||||
@@ -801,21 +791,19 @@ 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 = {
|
||||
val shown = visibleEntries.map { it.absolutePath }
|
||||
selectedPaths = if (allVisibleSelected) selectedPaths - shown.toSet() else selectedPaths + shown
|
||||
selectedPaths = if (selectedPaths.size == entries.size) emptySet()
|
||||
else entries.map { it.absolutePath }.toSet()
|
||||
},
|
||||
contentPadding = selBarPadding,
|
||||
) { Text(if (allVisibleSelected) stringResource(R.string.fm_select_none) else stringResource(R.string.fm_select_all), fontSize = 12.sp) }
|
||||
) { Text(if (selectedPaths.size == entries.size) stringResource(R.string.fm_select_none) else stringResource(R.string.fm_select_all), fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
enabled = anyVisibleSelected,
|
||||
enabled = selectedPaths.isNotEmpty(),
|
||||
onClick = {
|
||||
clipboardFiles = visibleEntries.filter { it.absolutePath in selectedPaths }
|
||||
clipboardFiles = entries.filter { it.absolutePath in selectedPaths }
|
||||
isCutOperation = false
|
||||
selectionMode = false
|
||||
selectedPaths = emptySet()
|
||||
@@ -824,9 +812,9 @@ fun FileManagerScreen(
|
||||
) { Text(stringResource(R.string.fm_copy), fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
enabled = anyVisibleSelected,
|
||||
enabled = selectedPaths.isNotEmpty(),
|
||||
onClick = {
|
||||
clipboardFiles = visibleEntries.filter { it.absolutePath in selectedPaths }
|
||||
clipboardFiles = entries.filter { it.absolutePath in selectedPaths }
|
||||
isCutOperation = true
|
||||
selectionMode = false
|
||||
selectedPaths = emptySet()
|
||||
@@ -835,8 +823,8 @@ fun FileManagerScreen(
|
||||
) { Text(stringResource(R.string.fm_cut), fontSize = 12.sp) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
OutlinedButton(
|
||||
enabled = anyVisibleSelected,
|
||||
onClick = { pendingBulkDelete = visibleEntries.filter { it.absolutePath in selectedPaths } },
|
||||
enabled = selectedPaths.isNotEmpty(),
|
||||
onClick = { pendingBulkDelete = entries.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) }
|
||||
@@ -854,7 +842,7 @@ fun FileManagerScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.1f))
|
||||
.clickable(enabled = !isOperationRunning) { performPaste() }
|
||||
.clickable { 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))
|
||||
@@ -890,7 +878,6 @@ fun FileManagerScreen(
|
||||
)
|
||||
if (operationJob != null) {
|
||||
TextButton(onClick = {
|
||||
cancelledJob = operationJob
|
||||
operationJob?.cancel()
|
||||
operationJob = null
|
||||
isOperationRunning = false
|
||||
@@ -977,7 +964,8 @@ fun FileManagerScreen(
|
||||
.fillMaxSize()
|
||||
.nestedScroll(pullState.nestedScrollConnection),
|
||||
) {
|
||||
val shownEntries = visibleEntries
|
||||
val shownEntries = if (searchQuery.isBlank()) entries
|
||||
else entries.filter { it.name.contains(searchQuery, ignoreCase = true) }
|
||||
|
||||
if (showGrid && entries.isNotEmpty()) {
|
||||
LazyVerticalGrid(
|
||||
|
||||
@@ -45,25 +45,22 @@ object FileOps {
|
||||
return f.length()
|
||||
}
|
||||
|
||||
fun moveWithProgress(src: File, dst: File, progress: ProgressCallback?, isCancelled: () -> Boolean = { false }): Boolean {
|
||||
fun moveWithProgress(src: File, dst: File, progress: ProgressCallback?): 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, isCancelled)) return false
|
||||
if (isCancelled()) return false
|
||||
if (!copyWithProgress(src, dst, progress)) return false
|
||||
return delete(src)
|
||||
}
|
||||
|
||||
fun copyWithProgress(src: File, dst: File, progress: ProgressCallback?, isCancelled: () -> Boolean = { false }): Boolean {
|
||||
fun copyWithProgress(src: File, dst: File, progress: ProgressCallback?): Boolean {
|
||||
val total = maxOf(totalSize(src), 1L)
|
||||
return copyWithProgress(src, dst, longArrayOf(0), total, progress, isCancelled)
|
||||
return copyWithProgress(src, dst, longArrayOf(0), total, progress)
|
||||
}
|
||||
|
||||
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")
|
||||
private fun copyWithProgress(src: File, dst: File, done: LongArray, total: Long, progress: ProgressCallback?): Boolean {
|
||||
if (isSymlink(src)) return true
|
||||
if (sameFile(src, dst)) return true
|
||||
if (src.isDirectory && isWithin(dst, src)) {
|
||||
@@ -74,7 +71,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, isCancelled)) {
|
||||
if (!copyWithProgress(File(src, name), File(dst, name), done, total, progress)) {
|
||||
Log.e(TAG, "Failed to copy: ${File(src, name)}")
|
||||
allOk = false
|
||||
}
|
||||
@@ -84,41 +81,13 @@ 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 ->
|
||||
checkNotNull(output).use { channel ->
|
||||
FileOutputStream(dst).channel.use { output ->
|
||||
val size = input.size()
|
||||
var position = 0L
|
||||
while (position < size) {
|
||||
val transferred = input.transferTo(position, minOf(chunk, size - position), channel)
|
||||
val transferred = input.transferTo(position, minOf(chunk, size - position), output)
|
||||
if (transferred <= 0) break
|
||||
position += transferred
|
||||
done[0] += transferred
|
||||
@@ -127,28 +96,11 @@ 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)
|
||||
if (created) dst.delete()
|
||||
dst.delete()
|
||||
return false
|
||||
} finally {
|
||||
output?.close()
|
||||
staging?.let { directory ->
|
||||
File(directory, "file").delete()
|
||||
directory.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ object AddedGameArt {
|
||||
}
|
||||
|
||||
enum class Slot(val key: String, val cacheName: String, val assetType: Int?) {
|
||||
COVER("p", "p.jpg", 0), WIDE("header", "header.jpg", 3), BACKGROUND("hero", "hero.jpg", 1), LOGO("logo", "logo.png", 2), ICON("icon", "icon.png", null),
|
||||
COVER("p", "p.jpg", 0), BACKGROUND("hero", "hero.jpg", 1), LOGO("logo", "logo.png", 2), ICON("icon", "icon.png", null),
|
||||
}
|
||||
|
||||
const val FOLDER = "folder"
|
||||
@@ -40,10 +40,10 @@ object AddedGameArt {
|
||||
|
||||
class Found<T>(val items: List<T>, val status: SteamGridDb.Status)
|
||||
|
||||
private val CHOSEN = Regex("""chosen-(p|header|hero|logo|icon)(?:-\d+)?\.(\w+)""")
|
||||
private val CHOSEN = Regex("""chosen-(\w+)\.(\w+)""")
|
||||
|
||||
internal fun chosen(cache: File, slot: Slot): File? =
|
||||
cache.listFiles { f -> CHOSEN.matchEntire(f.name)?.groupValues?.get(1) == slot.key }?.maxWithOrNull(compareBy({ it.lastModified() }, { it.name }))
|
||||
private fun chosen(cache: File, slot: Slot): File? =
|
||||
cache.listFiles { f -> CHOSEN.matchEntire(f.name)?.groupValues?.get(1) == slot.key }?.firstOrNull()
|
||||
|
||||
private val IMAGE = listOf("png", "jpg", "jpeg")
|
||||
private val PORTRAIT = listOf("cover", "poster", "grid", "boxart", "capsule", "folder", "library_600x900", "portrait")
|
||||
@@ -79,7 +79,7 @@ object AddedGameArt {
|
||||
fun cached(name: String) = File(cache, name).takeIf { it.isFile }
|
||||
return Art(
|
||||
portrait = chosen(cache, Slot.COVER) ?: own.portrait ?: cached("p.jpg") ?: exeArt(context, game, cover = true),
|
||||
header = chosen(cache, Slot.WIDE) ?: own.header ?: cached("header.jpg") ?: exeArt(context, game, cover = false),
|
||||
header = own.header ?: cached("header.jpg") ?: exeArt(context, game, cover = false),
|
||||
hero = chosen(cache, Slot.BACKGROUND) ?: own.hero ?: cached("hero.jpg"),
|
||||
logo = chosen(cache, Slot.LOGO) ?: own.logo ?: cached("logo.png"),
|
||||
icon = chosen(cache, Slot.ICON) ?: own.icon ?: cached("icon-sgdb.png") ?: exeIcon(context, game),
|
||||
@@ -96,7 +96,6 @@ object AddedGameArt {
|
||||
chosen(cache, slot)?.let { return@map SlotState(slot, it, chosenSource(context, game, slot).first.ifEmpty { FILE }, true) }
|
||||
val auto: Pair<File, String>? = when (slot) {
|
||||
Slot.COVER -> own.portrait?.let { it to FOLDER } ?: cached("p.jpg") ?: exeArt(context, game, cover = true)?.let { it to EXE }
|
||||
Slot.WIDE -> own.header?.let { it to FOLDER } ?: cached("header.jpg") ?: exeArt(context, game, cover = false)?.let { it to EXE }
|
||||
Slot.BACKGROUND -> own.hero?.let { it to FOLDER } ?: cached("hero.jpg")
|
||||
Slot.LOGO -> own.logo?.let { it to FOLDER } ?: cached("logo.png")
|
||||
Slot.ICON -> own.icon?.let { it to FOLDER } ?: File(cache, "icon-sgdb.png").takeIf { it.isFile }?.let { it to SGDB }
|
||||
@@ -118,37 +117,36 @@ object AddedGameArt {
|
||||
folderImages(game.folder).forEach { out.add(Option(FOLDER, it.path, it.path)) }
|
||||
val cache = cacheDir(context, game)
|
||||
val steamId = SteamMatch.resolve(context, game.folder.path, game.exe, game.name, legacy = File(cache, "steam-appid")).appId?.toString()
|
||||
if (steamId != null) steamOption(steamId, slot)?.let { out.add(it) }
|
||||
val (found, status) = sgdbChoices(context, File(cache, "sgdb-id"), steamId?.toIntOrNull(), game.name, slot)
|
||||
out.addAll(found)
|
||||
return Choices(out, status)
|
||||
}
|
||||
|
||||
internal fun steamOption(steamId: String, slot: Slot): Option? {
|
||||
val remote = when (slot) { Slot.COVER -> "library_600x900.jpg"; Slot.WIDE -> "header.jpg"; Slot.BACKGROUND -> "library_hero.jpg"; Slot.LOGO -> "logo.png"; Slot.ICON -> null } ?: return null
|
||||
val url = "$CDN$steamId/$remote".takeIf { exists(it) } ?: steamAssetUrl(steamId, remote) ?: return null
|
||||
return Option(STEAM, url, url)
|
||||
}
|
||||
|
||||
internal fun sgdbChoices(context: Context, marker: File, steamId: Int?, name: String, slot: Slot): Pair<List<Option>, SteamGridDb.Status> {
|
||||
val key = SteamGridDb.key(context)
|
||||
if (key.isEmpty()) return emptyList<Option>() to SteamGridDb.Status.NO_KEY
|
||||
marker.parentFile?.mkdirs()
|
||||
return try {
|
||||
val gridId = remembered(marker, SteamGridDb.keyChangedAt(context), retryMiss = true) {
|
||||
SteamGridDb.gameId(key, steamId, name)?.toString()
|
||||
}?.toIntOrNull() ?: return emptyList<Option>() to SteamGridDb.Status.NO_GAME
|
||||
SteamGridDb.images(key, gridId, slot, if (slot == Slot.ICON) 3 else 5).map { (thumb, full) -> Option(SGDB, thumb, full) } to SteamGridDb.Status.OK
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "$name: SteamGridDB failed (${e.javaClass.simpleName})")
|
||||
emptyList<Option>() to ((e as? SteamGridDb.Failed)?.status ?: SteamGridDb.Status.SERVER)
|
||||
val remote = when (slot) { Slot.COVER -> "library_600x900.jpg"; Slot.BACKGROUND -> "library_hero.jpg"; Slot.LOGO -> "logo.png"; Slot.ICON -> null }
|
||||
if (steamId != null && remote != null) {
|
||||
val url = "$CDN$steamId/$remote".takeIf { exists(it) } ?: steamAssetUrl(steamId, remote)
|
||||
if (url != null) out.add(Option(STEAM, url, url))
|
||||
}
|
||||
var status = SteamGridDb.Status.OK
|
||||
val key = SteamGridDb.key(context)
|
||||
if (key.isEmpty()) {
|
||||
status = SteamGridDb.Status.NO_KEY
|
||||
} else {
|
||||
cache.mkdirs()
|
||||
try {
|
||||
val gridId = remembered(File(cache, "sgdb-id"), SteamGridDb.keyChangedAt(context), retryMiss = true) {
|
||||
SteamGridDb.gameId(key, steamId?.toIntOrNull(), game.name)?.toString()
|
||||
}?.toIntOrNull()
|
||||
if (gridId == null) status = SteamGridDb.Status.NO_GAME
|
||||
else SteamGridDb.images(key, gridId, slot, if (slot == Slot.ICON) 3 else 5)
|
||||
.forEach { (thumb, full) -> out.add(Option(SGDB, thumb, full)) }
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "${game.name}: SteamGridDB failed (${e.javaClass.simpleName})")
|
||||
status = (e as? SteamGridDb.Failed)?.status ?: SteamGridDb.Status.SERVER
|
||||
}
|
||||
}
|
||||
return Choices(out, status)
|
||||
}
|
||||
|
||||
fun searchGames(context: Context, term: String): Found<SteamGridDb.Game> = sgdb(context) { SteamGridDb.search(it, term) }
|
||||
|
||||
fun gameImages(context: Context, id: Int, slot: Slot): Found<Option> = sgdb(context) { key ->
|
||||
SteamGridDb.all(key, id, slot).map { (thumb, full) -> Option(SGDB, thumb, full) }
|
||||
fun gameImages(context: Context, id: Int, slot: Slot, page: Int): Found<Option> = sgdb(context) { key ->
|
||||
SteamGridDb.page(key, id, slot, page).map { (thumb, full) -> Option(SGDB, thumb, full) }
|
||||
}
|
||||
|
||||
private fun <T> sgdb(context: Context, call: (String) -> List<T>): Found<T> {
|
||||
@@ -163,36 +161,17 @@ object AddedGameArt {
|
||||
}
|
||||
|
||||
fun choose(context: Context, game: AddedGames.Game, slot: Slot, option: Option): Boolean {
|
||||
if (!store(cacheDir(context, game), slot, option)) return false
|
||||
SessionPrefs.setAddedGameArtSource(context, game.folder.path, slot.key, option.group + "|" + option.full)
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun store(cache: File, slot: Slot, option: Option): Boolean {
|
||||
val fresh = storeNew(cache, slot, option) ?: return false
|
||||
dropOlder(cache, slot, fresh)
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun dropOlder(cache: File, slot: Slot, keep: File) {
|
||||
cache.listFiles { f -> f != keep && CHOSEN.matchEntire(f.name)?.groupValues?.get(1) == slot.key }?.forEach { it.delete() }
|
||||
}
|
||||
|
||||
private val lastStamp = java.util.concurrent.atomic.AtomicLong(0)
|
||||
|
||||
private fun nextStamp(): Long = lastStamp.updateAndGet { maxOf(it + 1, System.currentTimeMillis()) }
|
||||
|
||||
internal fun storeNew(cache: File, slot: Slot, option: Option): File? {
|
||||
val bytes = if (option.full.startsWith("https://")) get(option.full) else runCatching { File(option.full).readBytes() }.getOrNull()
|
||||
bytes ?: return null
|
||||
bytes ?: return false
|
||||
val ext = option.full.substringAfterLast('.', "png").substringBefore('?').lowercase().takeIf { it in CHOOSABLE + "ico" } ?: "png"
|
||||
cache.mkdirs()
|
||||
val suffix = if (ext == "jpeg") "jpg" else ext
|
||||
var dst = File(cache, "chosen-${slot.key}-" + "%013d".format(nextStamp()) + "." + suffix)
|
||||
while (dst.exists()) dst = File(cache, "chosen-${slot.key}-" + "%013d".format(nextStamp()) + "." + suffix)
|
||||
val cache = cacheDir(context, game).apply { mkdirs() }
|
||||
chosen(cache, slot)?.delete()
|
||||
val dst = File(cache, "chosen-${slot.key}.${if (ext == "jpeg") "jpg" else ext}")
|
||||
val tmp = File(cache, dst.name + ".tmp")
|
||||
tmp.writeBytes(bytes)
|
||||
return if (tmp.renameTo(dst)) dst else null
|
||||
if (!tmp.renameTo(dst)) return false
|
||||
SessionPrefs.setAddedGameArtSource(context, game.folder.path, slot.key, option.group + "|" + option.full)
|
||||
return true
|
||||
}
|
||||
|
||||
fun chosenSource(context: Context, game: AddedGames.Game, slot: Slot): Pair<String, String> {
|
||||
|
||||
@@ -36,34 +36,19 @@ object AddedGameEdits {
|
||||
|
||||
fun setArt(context: Context, folder: String, slot: AddedGameArt.Slot, option: AddedGameArt.Option): Boolean {
|
||||
val game = game(context, folder) ?: return false
|
||||
return synchronized(SteamGrid.lock) {
|
||||
if (!AddedGameArt.choose(context, game, slot, option)) return@synchronized false
|
||||
applyArt(context, game, slot)
|
||||
true
|
||||
}
|
||||
if (!AddedGameArt.choose(context, game, slot, option)) return false
|
||||
applyArt(context, game, slot)
|
||||
return true
|
||||
}
|
||||
|
||||
fun resetArt(context: Context, folder: String, slot: AddedGameArt.Slot) {
|
||||
val game = game(context, folder) ?: return
|
||||
synchronized(SteamGrid.lock) {
|
||||
AddedGameArt.reset(context, game, slot)
|
||||
applyArt(context, game, slot)
|
||||
}
|
||||
}
|
||||
|
||||
fun logoPosition(context: Context, folder: String): LogoPosition.Value? =
|
||||
game(context, folder)?.let { SteamGrid.position(SteamGrid.target(it, context)) }
|
||||
|
||||
fun setLogoPosition(context: Context, folder: String, value: LogoPosition.Value?): Boolean {
|
||||
val game = game(context, folder) ?: return false
|
||||
if (!SteamGrid.writePosition(context, SteamGrid.target(game, context), value)) return false
|
||||
publish(context) { SteamLiveShortcuts.setLogoPosition(context, game.appId, value) }
|
||||
return true
|
||||
AddedGameArt.reset(context, game, slot)
|
||||
applyArt(context, game, slot)
|
||||
}
|
||||
|
||||
private fun applyArt(context: Context, game: AddedGames.Game, slot: AddedGameArt.Slot) {
|
||||
val file = AddedGameArt.describe(context, game).first { it.slot == slot }.file
|
||||
if (slot != AddedGameArt.Slot.ICON) SteamGrid.write(context, SteamGrid.target(game, context), slot, AddedGameArt.chosen(AddedGameArt.cacheDir(context, game), slot))
|
||||
publish(context) {
|
||||
if (slot == AddedGameArt.Slot.ICON) SteamLiveShortcuts.setArt(context, game.appId, null, null, iconPath = file?.absolutePath.orEmpty())
|
||||
else SteamLiveShortcuts.setArt(context, game.appId, slot.assetType, file)
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import org.json.JSONObject
|
||||
import com.droiddeck.launcher.session.GameStorage
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.stores.Store
|
||||
import com.droiddeck.launcher.stores.StoreGameSidecar
|
||||
import com.droiddeck.launcher.stores.StoreInstallRoot
|
||||
import java.io.File
|
||||
@@ -41,20 +40,10 @@ object AddedGames {
|
||||
return SessionPrefs.gameFolderBinds(context).map { (host, guest) -> Root(File(host), guest) }
|
||||
}
|
||||
|
||||
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 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 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"
|
||||
|
||||
@@ -85,7 +74,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) { !skipExe(it.name) }
|
||||
val guessed = GameExePicker.rank(folder) { !SKIP.matches(it.name) }
|
||||
val declared = declaredExes(folder)
|
||||
val preferred = declared.map { it.path }.toSet()
|
||||
val result = if (declared.isEmpty()) guessed else GameExePicker.Ranking(
|
||||
@@ -138,7 +127,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) && !skipExe(it.name) && it.isFile }
|
||||
.filter { it.name.endsWith(".exe", ignoreCase = true) && !SKIP.matches(it.name) && it.isFile }
|
||||
.filter { runCatching { it.canonicalPath.startsWith(root) }.getOrDefault(false) }
|
||||
.distinctBy { it.path }
|
||||
.toList()
|
||||
@@ -149,10 +138,6 @@ object AddedGames {
|
||||
"appxmanifest.xml" to Regex("<Application\\b[^>]*?\\bExecutable\\s*=\\s*\"([^\"]+)\"", RegexOption.IGNORE_CASE),
|
||||
)
|
||||
|
||||
/** The card's Steam libraries: the arm64 client's steamapps and the x86-64 client's own folder beside it. */
|
||||
internal fun isSteamLibraryFolder(name: String): Boolean =
|
||||
name.equals("steamapps", ignoreCase = true) || name == com.droiddeck.launcher.session.GameStorage.X64_CARD_DIR
|
||||
|
||||
fun scan(context: Context): List<Game> {
|
||||
val out = ArrayList<Game>()
|
||||
GamesFolderMigration.run(context)
|
||||
@@ -165,7 +150,7 @@ object AddedGames {
|
||||
if (!dir.isDirectory) { Log.w(TAG, "$dir is not a folder; skipped"); continue }
|
||||
val steamInstalls = steamInstallDirs(dir)
|
||||
for (folder in dir.listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() } ?: emptyList()) {
|
||||
if (folder.name.lowercase() in steamInstalls || isSteamLibraryFolder(folder.name)) continue
|
||||
if (folder.name.lowercase() in steamInstalls || folder.name.equals("steamapps", ignoreCase = true)) continue
|
||||
if (folder.path in removed) continue
|
||||
if (folder.name == "Games" && StoreInstallRoot.roots(context).any { it.absolutePath == folder.absolutePath }) continue
|
||||
scanGame(context, folder, out)
|
||||
@@ -214,10 +199,8 @@ object AddedGames {
|
||||
GamesFolderMigration.run(context)
|
||||
val path = canonical(exe)
|
||||
fun holds(f: File) = inside(path, canonical(f))
|
||||
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 }
|
||||
AddedExes.list(context).map { File(it.folder) }.firstOrNull(::holds)?.let { return it }
|
||||
SessionPrefs.removedAddedGames(context).map { File(it) }.firstOrNull(::holds)?.let { return it }
|
||||
StoreInstallRoot.gameFolders(context).firstOrNull(::holds)?.let { return it }
|
||||
val dirs = listOfNotNull(
|
||||
GameStorage.effective(context)?.let { File(it.path, "steamapps/common") },
|
||||
@@ -227,29 +210,11 @@ object AddedGames {
|
||||
val base = canonical(dir)
|
||||
if (!inside(path, base)) continue
|
||||
val first = path.removePrefix("$base/").substringBefore('/')
|
||||
File(dir, first).takeIf { it.isDirectory && !isSteamLibraryFolder(first) }?.let { return it }
|
||||
File(dir, first).takeIf { it.isDirectory && first != "steamapps" }?.let { return it }
|
||||
}
|
||||
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
|
||||
@@ -258,10 +223,7 @@ 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 (guestPath(app, exe) == null) {
|
||||
(owner ?: exe.parentFile)?.takeIf { guestPath(app, it) == null }?.let { bindFolder(app, it) }
|
||||
if (guestPath(app, exe) == null) return AddResult.Unreachable
|
||||
}
|
||||
if (owner == null && 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)
|
||||
@@ -276,8 +238,6 @@ 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
|
||||
@@ -301,20 +261,6 @@ 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)
|
||||
@@ -324,7 +270,7 @@ object AddedGames {
|
||||
)
|
||||
for (dir in dirs.distinctBy { it.absolutePath }) {
|
||||
for (folder in dir.listFiles { f -> f.isDirectory }.orEmpty()) {
|
||||
if (folder.path in removed || isSteamLibraryFolder(folder.name)) continue
|
||||
if (folder.path in removed || folder.name.equals("steamapps", ignoreCase = true)) continue
|
||||
if (folder.name == "Games" && StoreInstallRoot.roots(context).any { it.absolutePath == folder.absolutePath }) continue
|
||||
out.add(canonical(folder))
|
||||
}
|
||||
@@ -350,22 +296,19 @@ object AddedGames {
|
||||
private fun scanGame(context: Context, folder: File, out: MutableList<Game>, single: AddedExes.Entry? = null) {
|
||||
run {
|
||||
val sidecar = StoreGameSidecar.read(folder)
|
||||
sidecar?.takeIf { it.id.isNotEmpty() }?.let { SessionPrefs.followStoreGame(context, "${it.store.id}:${it.id}", folder.path) { old -> !File(old).exists() } }
|
||||
val ranking = rank(folder)
|
||||
val candidates = ranking.exes
|
||||
val picked = SessionPrefs.addedGameExe(context, folder.path)
|
||||
val chosen = picked.takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.isFile }
|
||||
?: single?.takeIf { it.picked }?.let { File(it.exe) }?.takeIf { it.isFile }
|
||||
val found = chosen ?: sidecar?.exeFile(folder)?.takeIf { it.isFile } ?: candidates.firstOrNull() ?: return
|
||||
val epicLauncher = sidecar?.takeIf { it.store == Store.EPIC && (chosen == null || canonical(chosen) == canonical(it.exeFile(folder))) }
|
||||
?.launcherFile(folder)?.takeIf { it.isFile }
|
||||
val launch = epicLauncher ?: if (chosen == null) sidecar?.launcherFile(folder)?.takeIf { it.isFile } ?: found else found
|
||||
val launch = if (chosen == null) sidecar?.launcherFile(folder)?.takeIf { it.isFile } ?: found else found
|
||||
val foundGuest = guestPath(context, launch)
|
||||
if (foundGuest == null) { Log.w(TAG, "${folder.name}: the session cannot see ${launch.path}"); return }
|
||||
val autoName = sidecar?.title ?: identifiedName(folder, found) ?: folder.name
|
||||
val crc = CRC32().apply { update(("\"${foundGuest.replaceFirst(Regex("^$LIBRARY/"), "$LEGACY_LIBRARY/")}\"" + autoName).toByteArray()) }.value
|
||||
val appId = SessionPrefs.addedGameAppId(context, folder.path, crc or 0x80000000L)
|
||||
val config = accountConfig(SessionPrefs.activeSteamRoot(context))
|
||||
val config = accountConfig(File(LinuxRuntime.rootDir(context), "root/.local/share/Steam"))
|
||||
val record = config?.let { shortcutRecord(it, appId) }
|
||||
val rev = record?.optInt("rev", 0) ?: 0
|
||||
if (sidecar == null && record != null) adoptSteamEdits(context, folder, record)
|
||||
@@ -387,7 +330,7 @@ object AddedGames {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun accountConfig(root: File): File? {
|
||||
private fun accountConfig(root: File): File? {
|
||||
val users = runCatching { File(root, "config/loginusers.vdf").readText() }.getOrDefault("")
|
||||
val recent = Regex(""""(\d{5,})"\s*\{([^}]*)\}""").findAll(users)
|
||||
.lastOrNull { Regex(""""MostRecent"\s*"1"""").containsMatchIn(it.groupValues[2]) }
|
||||
@@ -400,9 +343,6 @@ object AddedGames {
|
||||
JSONObject(File(config, ".droiddeck-routes.json").readText()).optInt(appId.toString()).takeIf { it > 0 }
|
||||
}.getOrNull()
|
||||
|
||||
fun inSteam(context: Context, appId: Long): Boolean =
|
||||
accountConfig(SessionPrefs.activeSteamRoot(context))?.let { shortcutRecord(it, appId) } != null
|
||||
|
||||
private fun shortcutRecord(config: File, appId: Long): JSONObject? = runCatching {
|
||||
JSONObject(File(config, ".droiddeck-shortcuts.json").readText()).optJSONObject("games")?.optJSONObject(appId.toString())
|
||||
}.getOrNull()
|
||||
@@ -442,8 +382,7 @@ object AddedGames {
|
||||
}
|
||||
}
|
||||
|
||||
fun writeListing(context: Context, games: List<Game>,
|
||||
extra: List<JSONObject> = listOfNotNull(com.droiddeck.launcher.stores.epic.EpicClient.steamListing(context))): File {
|
||||
fun writeListing(context: Context, games: List<Game>): File {
|
||||
val file = File(context.filesDir, "session/added-games.json").apply { parentFile?.mkdirs() }
|
||||
val json = StringBuilder("[")
|
||||
games.forEachIndexed { i, g ->
|
||||
@@ -460,10 +399,6 @@ object AddedGames {
|
||||
if (pieces.isNotEmpty()) json.append(",\"art\":{").append(pieces.joinToString(",") { (k, v) -> quote(k) + ":" + quote(v) }).append('}')
|
||||
json.append('}')
|
||||
}
|
||||
extra.forEachIndexed { i, entry ->
|
||||
if (games.isNotEmpty() || i > 0) json.append(',')
|
||||
json.append(entry.toString())
|
||||
}
|
||||
file.writeText(json.append(']').toString())
|
||||
return file
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ object AddedGamesPublisher {
|
||||
fun republish(context: Context) {
|
||||
val games = AddedGames.scan(context)
|
||||
AddedGames.writeListing(context, games)
|
||||
val arch = com.droiddeck.launcher.session.SessionPrefs.activeSteamClientArch(context)
|
||||
LibraryCache.save(context, Library.launchableGames(context, added = games, arch = arch), arch == "x86_64")
|
||||
LibraryCache.save(context, Library.launchableGames(context, added = games))
|
||||
}
|
||||
}
|
||||
@@ -332,7 +332,7 @@ object AutoComponents {
|
||||
caps.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
}
|
||||
|
||||
fun constrained(context: Context): Boolean {
|
||||
private fun constrained(context: Context): Boolean {
|
||||
val power = context.getSystemService(PowerManager::class.java)
|
||||
if (power?.isPowerSaveMode == true) return true
|
||||
if (Build.VERSION.SDK_INT >= 29 && (power?.currentThermalStatus ?: 0) >= PowerManager.THERMAL_STATUS_MODERATE) return true
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.droiddeck.launcher.frontend
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
@@ -200,62 +199,24 @@ object CoverArt {
|
||||
setRequestProperty("User-Agent", "DroidDeck")
|
||||
}
|
||||
|
||||
private fun <T> through(url: String, attempt: (String, Boolean) -> T?): T? {
|
||||
for (source in DownloadSource.candidates(url)) {
|
||||
val result = attempt(source, source == url)
|
||||
if (result != null) {
|
||||
DownloadSource.succeeded(source)
|
||||
return result
|
||||
}
|
||||
}
|
||||
DownloadSource.failed()
|
||||
return null
|
||||
}
|
||||
private fun exists(url: String): Boolean = try {
|
||||
val c = open(url, "HEAD"); val ok = c.responseCode == 200; c.disconnect(); ok
|
||||
} catch (e: Exception) { false }
|
||||
|
||||
private fun exists(url: String): Boolean = through(url) { source, original -> head(source, original) } ?: false
|
||||
|
||||
private fun head(url: String, original: Boolean): Boolean? = try {
|
||||
val c = open(url, "HEAD")
|
||||
val code = c.responseCode
|
||||
c.disconnect()
|
||||
when {
|
||||
code == 200 -> true
|
||||
code == 404 || code == 410 -> if (original) false else null
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) { null }
|
||||
|
||||
private fun get(url: String): String? = through(url) { source, original -> text(source, original) }
|
||||
|
||||
private fun text(url: String, original: Boolean): String? = try {
|
||||
private fun get(url: String): String? = try {
|
||||
val c = open(url)
|
||||
val code = c.responseCode
|
||||
when {
|
||||
code == 200 -> c.inputStream.bufferedReader().use { it.readText() }.takeIf { it.isNotEmpty() }
|
||||
code == 404 || code == 410 -> { c.disconnect(); if (original) "" else null }
|
||||
else -> { c.disconnect(); null }
|
||||
}
|
||||
if (c.responseCode != 200) { c.disconnect(); null } else c.inputStream.bufferedReader().use { it.readText() }
|
||||
} catch (e: Exception) { Log.w(TAG, "$url: $e"); null }
|
||||
|
||||
private fun download(url: String, target: File): Boolean = through(url) { source, original -> fetch(source, target, original) } ?: false
|
||||
|
||||
private fun fetch(url: String, target: File, original: Boolean): Boolean? = try {
|
||||
private fun download(url: String, target: File): Boolean = try {
|
||||
val c = open(url)
|
||||
when (val code = c.responseCode) {
|
||||
200 -> {
|
||||
target.parentFile?.mkdirs()
|
||||
val tmp = File(target.path + ".part")
|
||||
c.inputStream.use { input -> tmp.outputStream().use { input.copyTo(it) } }
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(tmp.path, bounds)
|
||||
when {
|
||||
bounds.outWidth > 0 && tmp.renameTo(target) -> true
|
||||
original -> { tmp.delete(); false }
|
||||
else -> { tmp.delete(); null }
|
||||
}
|
||||
}
|
||||
404, 410 -> { c.disconnect(); if (original) false else null }
|
||||
else -> { c.disconnect(); null }
|
||||
if (c.responseCode != 200) { c.disconnect(); false } else {
|
||||
target.parentFile?.mkdirs()
|
||||
val tmp = File(target.path + ".part")
|
||||
c.inputStream.use { input -> tmp.outputStream().use { input.copyTo(it) } }
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(tmp.path, bounds)
|
||||
if (bounds.outWidth > 0 && tmp.renameTo(target)) true else { tmp.delete(); false }
|
||||
}
|
||||
} catch (e: Exception) { Log.w(TAG, "$url: $e"); null }
|
||||
} catch (e: Exception) { Log.w(TAG, "$url: $e"); false }
|
||||
}
|
||||
@@ -38,7 +38,7 @@ object GameFileSync {
|
||||
|
||||
@Synchronized private fun sync(context: Context, folder: File) {
|
||||
check(LinuxRuntime.isInstalled(context)) { context.getString(R.string.game_sync_err_runtime) }
|
||||
check(File(SessionPrefs.activeSteamRoot(context), "steamapps").isDirectory) { context.getString(R.string.game_sync_err_steam_library) }
|
||||
check(File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps").isDirectory) { context.getString(R.string.game_sync_err_steam_library) }
|
||||
val storage = GameStorage.effective(context)
|
||||
val storageSetting = SessionPrefs.gameStorage(context)
|
||||
val previousStorage = prefs(context).getString("storagePath", null)
|
||||
|
||||
@@ -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.*|bootstrappackagedgame)$"""
|
||||
"""(?i)^(gse|steam|steam client|goldberg.*|launcher|game|play|start|setup|.* launcher.*|.*launcher redirector|.* redirector|rockstar games launcher.*|epic games launcher|smartsteamemu|coldclient.*|steamemu.*)$"""
|
||||
)
|
||||
|
||||
fun isJunkPeName(name: String): Boolean {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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,7 +10,6 @@ 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
|
||||
@@ -24,7 +23,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(LauncherIcon.activity(context))
|
||||
.setActivity(android.content.ComponentName(context, MainActivity::class.java))
|
||||
.setShortLabel(name.take(40)).setLongLabel(name).setIcon(icon)
|
||||
.setIntent(launchIntent(context, game.gameIdString)).build()
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ object GamesFolderMigration {
|
||||
var n = 0
|
||||
val steamInstalls = AddedGames.steamInstallDirs(root.host)
|
||||
for (folder in root.host.listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() }.orEmpty()) {
|
||||
if (AddedGames.isSteamLibraryFolder(folder.name) || folder.name.lowercase() in steamInstalls) continue
|
||||
if (folder.name.equals("steamapps", ignoreCase = true) || folder.name.lowercase() in steamInstalls) continue
|
||||
if (folder.absolutePath in storeRoots || StoreGameSidecar.read(folder) != null) continue
|
||||
if (folder.path in entries) continue
|
||||
val picked = SessionPrefs.addedGameExe(context, folder.path).takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.isFile }
|
||||
|
||||
@@ -17,15 +17,10 @@ object Library {
|
||||
val source: String = SOURCE_STEAM,
|
||||
val storeId: String? = null,
|
||||
) {
|
||||
fun withArt(art: File?, hero: File?, icon: File?) =
|
||||
SteamGame(appId, name, art, library, gameId, hero, lastPlayed, gameFiles, protonPrefix, icon, source, storeId)
|
||||
|
||||
val gameIdString: String get() = java.lang.Long.toUnsignedString(gameId)
|
||||
val profileKey: String get() = if (library == ADDED) java.lang.Integer.toUnsignedString(appId) else appId.toString()
|
||||
}
|
||||
const val ADDED = "added"
|
||||
const val EPIC_LAUNCHER = "epic-launcher"
|
||||
const val EPIC_LAUNCHER_NAME = "Epic Games Launcher"
|
||||
const val SOURCE_STEAM = "steam"
|
||||
|
||||
class Rom(val name: String, val hostPath: File, val guestPath: String, val emulatorId: String, val art: File? = null)
|
||||
@@ -72,17 +67,13 @@ 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>> {
|
||||
val root = SessionPrefs.steamRoot(context, arch)
|
||||
// The session gives the x86-64 client only its card folder; the games folders stay the arm64 client's.
|
||||
val card = GameStorage.effective(context)
|
||||
if (arch == "x86_64") return listOfNotNull(root to "internal", card?.let { File(it.path, GameStorage.X64_CARD_DIR) to it.label })
|
||||
private fun steamLibraries(context: Context): List<Pair<File, String>> {
|
||||
val root = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam")
|
||||
return listOfNotNull(
|
||||
root to "internal",
|
||||
card?.let { File(it.path) to it.label },
|
||||
GameStorage.effective(context)?.let { File(it.path) to it.label },
|
||||
) + GameStorage.gamesFolderLibraries(context).map { it.host to it.label }
|
||||
}
|
||||
|
||||
@@ -102,10 +93,10 @@ object Library {
|
||||
.firstOrNull { it.isDirectory }
|
||||
}
|
||||
|
||||
fun steamGames(context: Context, strictRead: Boolean = false, arch: String = SessionPrefs.activeSteamClientArch(context)): List<SteamGame> {
|
||||
val root = SessionPrefs.steamRoot(context, arch)
|
||||
fun steamGames(context: Context, strictRead: Boolean = false): List<SteamGame> {
|
||||
val root = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam")
|
||||
val cache = File(root, "appcache/librarycache")
|
||||
val libraries = steamLibraries(context, arch)
|
||||
val libraries = steamLibraries(context)
|
||||
val roots = libraries.map { it.first }
|
||||
val out = LinkedHashMap<Int, SteamGame>()
|
||||
for ((library, label) in libraries) {
|
||||
@@ -125,76 +116,31 @@ object Library {
|
||||
val installDir = INSTALL_DIR.find(text)?.groupValues?.get(1)?.trim()
|
||||
?.takeIf { it.isNotEmpty() && it != "." && it != ".." && '/' !in it && '\\' !in it }
|
||||
val gameFiles = installDir?.let { File(steamapps, "common/$it").takeIf(File::isDirectory) }
|
||||
out[appId] = SteamGameArt.withOverrides(context, SteamGame(
|
||||
out[appId] = SteamGame(
|
||||
appId, name, art, label, hero = hero, lastPlayed = lastPlayed,
|
||||
gameFiles = gameFiles,
|
||||
protonPrefix = protonPrefix(context, appId.toLong(), library, roots),
|
||||
icon = steamCacheImage(cache, appId, listOf("icon.jpg", "icon.png"))
|
||||
?: File(cache, appId.toString()).listFiles()?.firstOrNull { it.isFile && STEAM_ICON.matches(it.name) },
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
return out.values.toList()
|
||||
}
|
||||
|
||||
fun steamLibraryArt(context: Context): List<Pair<Int, File>> {
|
||||
val root = SessionPrefs.activeSteamRoot(context)
|
||||
val cache = File(root, "appcache/librarycache")
|
||||
val concealed = concealedSteamApps(root)
|
||||
val cache = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/appcache/librarycache")
|
||||
return cache.listFiles().orEmpty().mapNotNull { dir ->
|
||||
val appId = dir.name.toIntOrNull()?.takeIf { it !in NOT_GAMES && it.toLong() !in concealed } ?: return@mapNotNull null
|
||||
val appId = dir.name.toIntOrNull()?.takeIf { it !in NOT_GAMES } ?: 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 launchableGames(context: Context, strictRead: Boolean = false, added: List<AddedGames.Game>? = null): List<SteamGame> {
|
||||
val roots = steamLibraries(context).map { it.first }
|
||||
return (steamGames(context, strictRead) + (added ?: AddedGames.scan(context)).map { g -> addedGame(context, g, roots) }).distinctBy { it.gameId }
|
||||
}
|
||||
|
||||
fun fromSteamClient(g: SteamGame): Boolean = g.library != ADDED && g.source == SOURCE_STEAM
|
||||
|
||||
fun launchableGames(
|
||||
context: Context, strictRead: Boolean = false, added: List<AddedGames.Game>? = null,
|
||||
arch: String = SessionPrefs.activeSteamClientArch(context),
|
||||
): List<SteamGame> {
|
||||
val roots = steamLibraries(context, arch).map { it.first }
|
||||
val list = added ?: AddedGames.scan(context)
|
||||
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)
|
||||
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(
|
||||
@@ -206,7 +152,7 @@ object Library {
|
||||
)
|
||||
}
|
||||
|
||||
internal fun steamCacheImage(cache: File, appId: Int, names: List<String>): File? {
|
||||
private fun steamCacheImage(cache: File, appId: Int, names: List<String>): File? {
|
||||
val appDir = File(cache, appId.toString())
|
||||
val dirs = listOf(appDir) + appDir.listFiles()
|
||||
.orEmpty().filter { it.isDirectory }.sortedBy { it.name }
|
||||
@@ -239,8 +185,7 @@ object Library {
|
||||
if (program == com.droiddeck.launcher.runtime.FlatpakManager.LAUNCHER || program == com.droiddeck.launcher.runtime.AppImageManager.LAUNCHER ||
|
||||
program == com.droiddeck.launcher.runtime.UserApps.SCRIPT_LAUNCHER) {
|
||||
com.droiddeck.launcher.session.SessionState.programArgs.firstOrNull()?.let { flatpakNames[it] ?: it.substringAfterLast('.') }
|
||||
} else com.droiddeck.launcher.session.NoSteamLaunch.nameFor(program, com.droiddeck.launcher.session.SessionState.programArgs)
|
||||
?: specs.firstOrNull { it.program == program }?.name
|
||||
} else specs.firstOrNull { it.program == program }?.name
|
||||
|
||||
val flatpakNames = java.util.concurrent.ConcurrentHashMap<String, String>()
|
||||
|
||||
|
||||
@@ -10,14 +10,10 @@ object LibraryCache {
|
||||
private const val TAG = "LibraryCache"
|
||||
private fun file(context: Context) = File(context.filesDir, "library-cache.json")
|
||||
|
||||
/** The cached games and whether they are the x86-64 client's; a cache written as a bare list is arm64's. */
|
||||
@Synchronized
|
||||
fun snapshot(context: Context): Pair<List<Library.SteamGame>, Boolean> = runCatching {
|
||||
fun load(context: Context): List<Library.SteamGame> = runCatching {
|
||||
val f = file(context)
|
||||
if (!f.isFile) return Pair(emptyList(), false)
|
||||
val text = f.readText().trim()
|
||||
val (array, x64) = if (text.startsWith("{")) JSONObject(text).let { it.getJSONArray("games") to (it.optString("client") == "x86_64") }
|
||||
else JSONArray(text) to false
|
||||
if (!f.isFile) return emptyList()
|
||||
val array = JSONArray(f.readText())
|
||||
List(array.length()) { i ->
|
||||
val o = array.getJSONObject(i)
|
||||
Library.SteamGame(
|
||||
@@ -28,18 +24,11 @@ object LibraryCache {
|
||||
source = o.optString("source").ifEmpty { if (o.getString("library") == Library.ADDED) Library.ADDED else Library.SOURCE_STEAM },
|
||||
storeId = o.optString("storeId").ifEmpty { null },
|
||||
)
|
||||
} to x64
|
||||
}.getOrElse { e -> Log.w(TAG, "unreadable, ignored: ${e.message}"); Pair(emptyList(), false) }
|
||||
}
|
||||
}.getOrElse { e -> Log.w(TAG, "unreadable, ignored: ${e.message}"); emptyList() }
|
||||
|
||||
fun load(context: Context): List<Library.SteamGame> = snapshot(context).first
|
||||
|
||||
fun listedX64(context: Context): Boolean = snapshot(context).second
|
||||
|
||||
/** Without [x64] the list keeps the client already recorded. */
|
||||
@Synchronized
|
||||
fun save(context: Context, games: List<Library.SteamGame>, x64: Boolean? = null) {
|
||||
fun save(context: Context, games: List<Library.SteamGame>) {
|
||||
runCatching {
|
||||
val client = x64 ?: listedX64(context)
|
||||
val array = JSONArray()
|
||||
for (g in games) array.put(
|
||||
JSONObject().put("appId", g.appId).put("name", g.name).put("library", g.library).put("gameId", g.gameId)
|
||||
@@ -49,12 +38,8 @@ object LibraryCache {
|
||||
)
|
||||
val f = file(context)
|
||||
val tmp = File(f.path + ".tmp")
|
||||
tmp.writeText(JSONObject().put("client", if (client) "x86_64" else "arm64").put("games", array).toString())
|
||||
if (!tmp.renameTo(f)) {
|
||||
tmp.delete()
|
||||
f.delete()
|
||||
Log.w(TAG, "not saved: rename failed; cache dropped so the next start scans")
|
||||
}
|
||||
tmp.writeText(array.toString())
|
||||
tmp.renameTo(f)
|
||||
}.onFailure { e -> Log.w(TAG, "not saved: ${e.message}") }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
/** Where Steam pins a game's logo on its hero image and how big it draws it; the same JSON Steam keeps in `<appid>.json`. */
|
||||
object LogoPosition {
|
||||
enum class Pin(val steam: String) {
|
||||
UPPER_LEFT("UpperLeft"), UPPER_CENTER("UpperCenter"), CENTER("CenterCenter"), BOTTOM_LEFT("BottomLeft"), BOTTOM_CENTER("BottomCenter");
|
||||
|
||||
companion object {
|
||||
fun of(steam: String): Pin? = values().firstOrNull { it.steam == steam }
|
||||
}
|
||||
}
|
||||
|
||||
data class Value(val pin: Pin, val widthPct: Int, val heightPct: Int)
|
||||
|
||||
const val MIN_PCT = 10
|
||||
const val MAX_PCT = 100
|
||||
|
||||
/** What Steam draws when a game has no custom position. */
|
||||
val DEFAULT = Value(Pin.BOTTOM_LEFT, 50, 50)
|
||||
|
||||
private const val VERSION = 1
|
||||
private const val KEY = "logoPosition"
|
||||
|
||||
fun sized(pin: Pin, size: Int): Value = size.coerceIn(MIN_PCT, MAX_PCT).let { Value(pin, it, it) }
|
||||
|
||||
/** `{"nVersion":1,"logoPosition":{...}}`, with whatever else [into] already held kept as it was. */
|
||||
fun toJson(value: Value, into: JSONObject = JSONObject()): JSONObject = into
|
||||
.put("nVersion", VERSION)
|
||||
.put(KEY, JSONObject().put("pinnedPosition", value.pin.steam).put("nWidthPct", value.widthPct).put("nHeightPct", value.heightPct))
|
||||
|
||||
fun parse(text: String): Value? = runCatching { parse(JSONObject(text)) }.getOrNull()
|
||||
|
||||
fun parse(json: JSONObject): Value? {
|
||||
val inner = json.optJSONObject(KEY) ?: return null
|
||||
val pin = Pin.of(inner.optString("pinnedPosition")) ?: return null
|
||||
if (!inner.has("nWidthPct") || !inner.has("nHeightPct")) return null
|
||||
return Value(pin, inner.optInt("nWidthPct").coerceIn(MIN_PCT, MAX_PCT), inner.optInt("nHeightPct").coerceIn(MIN_PCT, MAX_PCT))
|
||||
}
|
||||
|
||||
/** [existing] without the logo position; null when nothing but the version number would be left. */
|
||||
fun without(existing: JSONObject): JSONObject? {
|
||||
existing.remove(KEY)
|
||||
val rest = existing.keys().asSequence().filter { it != "nVersion" }.toList()
|
||||
return if (rest.isEmpty()) null else existing
|
||||
}
|
||||
}
|
||||
@@ -25,16 +25,9 @@ object SessionInstalls {
|
||||
STATE.find(text)?.groupValues?.get(1)?.toIntOrNull() == 4, file.parentFile!!)
|
||||
}.getOrNull()
|
||||
|
||||
internal fun steamappsDirs(context: Context): List<File> = (
|
||||
if (com.droiddeck.launcher.session.SessionPrefs.activeSteamClientArch(context) == "x86_64")
|
||||
listOfNotNull(
|
||||
File(com.droiddeck.launcher.session.SessionPrefs.activeSteamRoot(context), "steamapps"),
|
||||
GameStorage.x64CardLibrary(context)?.let { File(it, "steamapps") },
|
||||
)
|
||||
else listOfNotNull(
|
||||
File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps"),
|
||||
GameStorage.effective(context)?.let { File(it.path, "steamapps") },
|
||||
)
|
||||
internal fun steamappsDirs(context: Context): List<File> = listOfNotNull(
|
||||
File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps"),
|
||||
GameStorage.effective(context)?.let { File(it.path, "steamapps") },
|
||||
).filter { it.isDirectory }
|
||||
|
||||
internal class Watch(private val dirs: () -> List<File>) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.droiddeck.launcher.frontend
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.DataInputStream
|
||||
@@ -19,7 +20,7 @@ object SteamAppInfo {
|
||||
|
||||
class Info(val name: String, val sharedDepots: List<String>)
|
||||
|
||||
fun appinfoFile(context: Context) = File(com.droiddeck.launcher.session.SessionPrefs.activeSteamRoot(context), "appcache/appinfo.vdf")
|
||||
fun appinfoFile(context: Context) = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/appcache/appinfo.vdf")
|
||||
|
||||
@Synchronized
|
||||
fun info(context: Context, appId: Int, appinfo: File = appinfoFile(context)): Info? {
|
||||
@@ -153,7 +154,7 @@ object SteamAppInfo {
|
||||
return String(bytes, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
internal fun readMap(b: ByteBuffer, strings: List<String>?): Map<String, Any> {
|
||||
private 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
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import com.droiddeck.launcher.stores.SteamLiveShortcuts
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import java.io.File
|
||||
|
||||
object SteamGameArt {
|
||||
internal fun dir(context: Context, appId: Int) = File(context.filesDir, "steam-art/$appId")
|
||||
|
||||
private fun sourceFile(context: Context, appId: Int, slot: AddedGameArt.Slot) = File(dir(context, appId), "source-${slot.key}")
|
||||
|
||||
internal fun setSource(context: Context, appId: Int, slot: AddedGameArt.Slot, value: String) {
|
||||
val file = sourceFile(context, appId, slot)
|
||||
if (value.isEmpty()) file.delete() else file.apply { parentFile?.mkdirs() }.writeText(value)
|
||||
}
|
||||
|
||||
fun chosenFile(context: Context, appId: Int, slot: AddedGameArt.Slot): File? = AddedGameArt.chosen(dir(context, appId), slot)
|
||||
|
||||
fun withOverrides(context: Context, game: Library.SteamGame): Library.SteamGame {
|
||||
if (game.library == Library.ADDED || !dir(context, game.appId).isDirectory) return game
|
||||
val cover = chosenFile(context, game.appId, AddedGameArt.Slot.COVER)
|
||||
val hero = chosenFile(context, game.appId, AddedGameArt.Slot.BACKGROUND)
|
||||
val icon = chosenFile(context, game.appId, AddedGameArt.Slot.ICON)
|
||||
if (cover == null && hero == null && icon == null) return game
|
||||
return Library.SteamGame(
|
||||
game.appId, game.name, cover ?: game.art, game.library, game.gameId,
|
||||
hero ?: game.hero, game.lastPlayed, game.gameFiles, game.protonPrefix, icon ?: game.icon, game.source, game.storeId,
|
||||
)
|
||||
}
|
||||
|
||||
fun chosenSource(context: Context, appId: Int, slot: AddedGameArt.Slot): Pair<String, String> {
|
||||
val value = sourceFile(context, appId, slot).takeIf { it.isFile }?.readText()?.trim().orEmpty()
|
||||
return value.substringBefore('|') to value.substringAfter('|', "")
|
||||
}
|
||||
|
||||
fun describe(context: Context, game: Library.SteamGame): List<AddedGameArt.SlotState> {
|
||||
val cache = File(SessionPrefs.activeSteamRoot(context), "appcache/librarycache")
|
||||
val own = withOverrides(context, game)
|
||||
return AddedGameArt.Slot.values().map { slot ->
|
||||
chosenFile(context, game.appId, slot)?.let {
|
||||
return@map AddedGameArt.SlotState(slot, it, chosenSource(context, game.appId, slot).first.ifEmpty { AddedGameArt.FILE }, true)
|
||||
}
|
||||
val auto = when (slot) {
|
||||
AddedGameArt.Slot.COVER -> own.art
|
||||
AddedGameArt.Slot.WIDE -> Library.steamCacheImage(cache, game.appId, listOf("library_header.jpg", "header.jpg"))
|
||||
AddedGameArt.Slot.BACKGROUND -> own.hero
|
||||
AddedGameArt.Slot.ICON -> own.icon
|
||||
AddedGameArt.Slot.LOGO -> Library.steamCacheImage(cache, game.appId, listOf("logo.png"))
|
||||
}
|
||||
AddedGameArt.SlotState(slot, auto, if (auto != null) AddedGameArt.STEAM else AddedGameArt.AUTO, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun choices(context: Context, game: Library.SteamGame, slot: AddedGameArt.Slot): AddedGameArt.Choices {
|
||||
val out = ArrayList<AddedGameArt.Option>()
|
||||
AddedGameArt.steamOption(game.appId.toString(), slot)?.let { out.add(it) }
|
||||
val (found, status) = AddedGameArt.sgdbChoices(context, File(dir(context, game.appId), "sgdb-id"), game.appId, game.name, slot)
|
||||
out.addAll(found)
|
||||
return AddedGameArt.Choices(out, status)
|
||||
}
|
||||
|
||||
fun choose(context: Context, game: Library.SteamGame, slot: AddedGameArt.Slot, option: AddedGameArt.Option): Boolean {
|
||||
return synchronized(SteamGrid.lock) {
|
||||
val folder = dir(context, game.appId)
|
||||
val fresh = AddedGameArt.storeNew(folder, slot, option) ?: return@synchronized false
|
||||
if (!push(context, game, slot, fresh)) {
|
||||
fresh.delete()
|
||||
return@synchronized false
|
||||
}
|
||||
AddedGameArt.dropOlder(folder, slot, fresh)
|
||||
setSource(context, game.appId, slot, option.group + "|" + option.full)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun reset(context: Context, game: Library.SteamGame, slot: AddedGameArt.Slot) {
|
||||
synchronized(SteamGrid.lock) {
|
||||
if (!push(context, game, slot, null)) return@synchronized
|
||||
AddedGameArt.chosen(dir(context, game.appId), slot)?.delete()
|
||||
setSource(context, game.appId, slot, "")
|
||||
}
|
||||
}
|
||||
|
||||
fun position(context: Context, game: Library.SteamGame): LogoPosition.Value? = SteamGrid.position(SteamGrid.target(game, context))
|
||||
|
||||
fun setPosition(context: Context, game: Library.SteamGame, value: LogoPosition.Value?): Boolean {
|
||||
if (!SteamGrid.writePosition(context, SteamGrid.target(game, context), value)) return false
|
||||
AddedGamesPublisher.later(context) { SteamLiveShortcuts.setLogoPosition(context, game.appId.toLong(), value) }
|
||||
return true
|
||||
}
|
||||
|
||||
private fun push(context: Context, game: Library.SteamGame, slot: AddedGameArt.Slot, file: File?): Boolean {
|
||||
val type = slot.assetType ?: return true
|
||||
if (!SteamGrid.write(context, SteamGrid.target(game, context), slot, file)) return false
|
||||
AddedGamesPublisher.later(context) { SteamLiveShortcuts.setArt(context, game.appId.toLong(), type, file) }
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
/** Steam keeps a game's custom art in `userdata/<account>/config/grid`; this is where DroidDeck's choices and Steam's meet. */
|
||||
object SteamGrid {
|
||||
private const val TAG = "SteamGrid"
|
||||
|
||||
/** Every change to a game's art, here or in a scan, takes this first so two of them never interleave. */
|
||||
internal val lock = Any()
|
||||
private const val MANIFEST = ".droiddeck-art.json"
|
||||
private val SUFFIX = mapOf(AddedGameArt.Slot.COVER to "p", AddedGameArt.Slot.WIDE to "", AddedGameArt.Slot.BACKGROUND to "_hero", AddedGameArt.Slot.LOGO to "_logo")
|
||||
private val EXTS = listOf("png", "jpg", "jpeg", "webp")
|
||||
|
||||
class Target(val artDir: File, val id: Long, val setSource: (AddedGameArt.Slot, String) -> Unit)
|
||||
|
||||
fun target(game: Library.SteamGame, context: Context) = Target(SteamGameArt.dir(context, game.appId), game.appId.toLong()) { slot, value ->
|
||||
SteamGameArt.setSource(context, game.appId, slot, value)
|
||||
}
|
||||
|
||||
fun target(game: AddedGames.Game, context: Context) = Target(AddedGameArt.cacheDir(context, game), game.appId and 0xFFFFFFFFL) { slot, value ->
|
||||
SessionPrefs.setAddedGameArtSource(context, game.folder.path, slot.key, value)
|
||||
}
|
||||
|
||||
internal fun dir(context: Context): File? {
|
||||
return AddedGames.accountConfig(SessionPrefs.activeSteamRoot(context))?.let { File(it, "grid") }
|
||||
}
|
||||
|
||||
private fun files(grid: File, id: Long, slot: AddedGameArt.Slot): List<File> =
|
||||
EXTS.map { File(grid, "$id${SUFFIX[slot]}.$it") }.filter { it.isFile }
|
||||
|
||||
private fun signature(file: File?) = if (file == null) "" else "${file.length()}:${file.lastModified()}"
|
||||
|
||||
/** Each Steam client has its own grid folder, so what was last seen there is kept per client. */
|
||||
private fun sigName(context: Context, key: String) =
|
||||
if (SessionPrefs.activeSteamClientArch(context) == "x86_64") "grid-x86_64-$key" else "grid-$key"
|
||||
|
||||
private fun sigFile(context: Context, target: Target, slot: AddedGameArt.Slot) = File(target.artDir, sigName(context, slot.key))
|
||||
|
||||
private fun recorded(context: Context, target: Target, slot: AddedGameArt.Slot): String? =
|
||||
sigFile(context, target, slot).takeIf { it.isFile }?.readText()
|
||||
|
||||
private fun record(context: Context, target: Target, slot: AddedGameArt.Slot, value: String) {
|
||||
target.artDir.mkdirs()
|
||||
sigFile(context, target, slot).writeText(value)
|
||||
}
|
||||
|
||||
private fun writtenByDroidDeck(grid: File): Set<String> = runCatching {
|
||||
val manifest = JSONObject(File(grid, MANIFEST).readText())
|
||||
manifest.keys().asSequence().filter { name -> File(grid, name).let { it.isFile && it.length() == manifest.optLong(name, -1L) } }.toSet()
|
||||
}.getOrDefault(emptySet())
|
||||
|
||||
/** Puts a choice (or, with null, the lack of one) where Steam reads it, whether or not Steam is running. */
|
||||
fun write(context: Context, target: Target, slot: AddedGameArt.Slot, file: File?): Boolean = synchronized(lock) { place(context, target, slot, file) }
|
||||
|
||||
private fun place(context: Context, target: Target, slot: AddedGameArt.Slot, file: File?): Boolean {
|
||||
val suffix = SUFFIX[slot] ?: return true
|
||||
val grid = dir(context) ?: return true
|
||||
if (file == null) {
|
||||
if (files(grid, target.id, slot).map { it.delete() }.any { !it }) return false
|
||||
record(context, target, slot, "")
|
||||
return true
|
||||
}
|
||||
val ext = file.extension.lowercase().let { if (it == "jpeg") "jpg" else it }
|
||||
if (ext !in EXTS) return false
|
||||
grid.mkdirs()
|
||||
val dst = File(grid, "${target.id}$suffix.$ext")
|
||||
val tmp = File(grid, dst.name + ".tmp")
|
||||
val placed = runCatching { file.copyTo(tmp, overwrite = true); if (tmp.renameTo(dst)) dst else null }.getOrNull()
|
||||
if (placed == null) {
|
||||
tmp.delete()
|
||||
return false
|
||||
}
|
||||
if (files(grid, target.id, slot).filter { it != dst }.map { it.delete() }.any { !it }) return false
|
||||
record(context, target, slot, signature(placed))
|
||||
return true
|
||||
}
|
||||
|
||||
private const val POSITION = "logo-position"
|
||||
private const val POSITION_SIG = "json"
|
||||
|
||||
private fun positionFile(grid: File, id: Long) = File(grid, "$id.json")
|
||||
|
||||
private fun positionIn(file: File): LogoPosition.Value? = file.takeIf { it.isFile }?.let { runCatching { it.readText() }.getOrNull() }?.let(LogoPosition::parse)
|
||||
|
||||
/** The logo position chosen here (or taken over from Steam) for a game; null when Steam's own default applies. */
|
||||
fun position(target: Target): LogoPosition.Value? = positionIn(File(target.artDir, POSITION))
|
||||
|
||||
private fun positionRecorded(context: Context, target: Target): String? =
|
||||
File(target.artDir, sigName(context, POSITION_SIG)).takeIf { it.isFile }?.readText()
|
||||
|
||||
private fun recordPosition(context: Context, target: Target, value: String) {
|
||||
target.artDir.mkdirs()
|
||||
File(target.artDir, sigName(context, POSITION_SIG)).writeText(value)
|
||||
}
|
||||
|
||||
private fun storePosition(target: Target, value: LogoPosition.Value?): Boolean {
|
||||
val file = File(target.artDir, POSITION)
|
||||
if (value == null) return !file.exists() || file.delete()
|
||||
target.artDir.mkdirs()
|
||||
return runCatching { file.writeText(LogoPosition.toJson(value).toString()) }.isSuccess
|
||||
}
|
||||
|
||||
/** Sets (or with null, clears) a game's logo position here and in the client's grid folder, whether or not Steam is running. */
|
||||
fun writePosition(context: Context, target: Target, value: LogoPosition.Value?): Boolean = synchronized(lock) {
|
||||
placePosition(context, target, value) && storePosition(target, value)
|
||||
}
|
||||
|
||||
/** Steam keeps the position in `<id>.json`; anything else that file holds is left as it was. */
|
||||
private fun placePosition(context: Context, target: Target, value: LogoPosition.Value?): Boolean {
|
||||
val grid = dir(context) ?: return true
|
||||
val file = positionFile(grid, target.id)
|
||||
val existing = if (file.isFile) runCatching { JSONObject(file.readText()) }.getOrNull() else null
|
||||
if (file.isFile && existing == null) return value == null
|
||||
val next = if (value == null) existing?.let(LogoPosition::without) else LogoPosition.toJson(value, existing ?: JSONObject())
|
||||
if (next == null) {
|
||||
if (file.isFile && !file.delete()) return false
|
||||
recordPosition(context, target, "")
|
||||
return true
|
||||
}
|
||||
grid.mkdirs()
|
||||
val tmp = File(grid, file.name + ".tmp")
|
||||
val placed = runCatching { tmp.writeText(next.toString()); tmp.renameTo(file) }.getOrDefault(false)
|
||||
if (!placed) {
|
||||
tmp.delete()
|
||||
return false
|
||||
}
|
||||
recordPosition(context, target, signature(file))
|
||||
return true
|
||||
}
|
||||
|
||||
/** A `logoPosition` this version cannot read (a pin it does not know, say) is left alone rather than read as "removed". */
|
||||
private fun hasUnknownPosition(file: File): Boolean {
|
||||
val json = runCatching { JSONObject(file.readText()) }.getOrNull() ?: return false
|
||||
return json.has("logoPosition") && LogoPosition.parse(json) == null
|
||||
}
|
||||
|
||||
private fun reconcilePosition(context: Context, grid: File, target: Target) {
|
||||
val file = positionFile(grid, target.id)
|
||||
if (hasUnknownPosition(file)) return
|
||||
val theirs = positionIn(file)
|
||||
val now = if (theirs != null) signature(file) else ""
|
||||
val before = positionRecorded(context, target)
|
||||
val own = position(target)
|
||||
when {
|
||||
before == null && own != null && theirs == null -> placePosition(context, target, own)
|
||||
before == null || now != before -> {
|
||||
// Recorded only once the local side agrees, so a failed take-over is tried again on the next scan.
|
||||
if (theirs != null) {
|
||||
if (own == theirs || storePosition(target, theirs)) recordPosition(context, target, now)
|
||||
} else {
|
||||
if (own == null || storePosition(target, null)) recordPosition(context, target, "")
|
||||
}
|
||||
}
|
||||
own != null && own != theirs -> placePosition(context, target, own)
|
||||
}
|
||||
}
|
||||
|
||||
/** Takes over what changed in Steam since the last look; returns true when a choice was added, replaced or dropped. */
|
||||
fun sync(context: Context, targets: List<Target>): Boolean = synchronized(lock) { reconcile(context, targets) }
|
||||
|
||||
private fun reconcile(context: Context, targets: List<Target>): Boolean {
|
||||
val grid = dir(context) ?: return false
|
||||
val ours = writtenByDroidDeck(grid)
|
||||
var changed = false
|
||||
for (target in targets) for (slot in SUFFIX.keys) {
|
||||
try {
|
||||
val own = AddedGameArt.chosen(target.artDir, slot)
|
||||
val present = files(grid, target.id, slot)
|
||||
val theirs = present.filter { it.name !in ours }.maxByOrNull { it.lastModified() }
|
||||
val now = signature(theirs)
|
||||
val before = recorded(context, target, slot)
|
||||
when {
|
||||
before == null && own != null && theirs == null -> write(context, target, slot, own)
|
||||
before == null || now != before -> {
|
||||
if (theirs != null) {
|
||||
if (own != null && own.length() == theirs.length() && own.readBytes().contentEquals(theirs.readBytes())) record(context, target, slot, now)
|
||||
else if (AddedGameArt.store(target.artDir, slot, AddedGameArt.Option(AddedGameArt.STEAM, theirs.path, theirs.path))) {
|
||||
target.setSource(slot, AddedGameArt.STEAM + "|steam-client")
|
||||
record(context, target, slot, now)
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
if (own != null && present.isEmpty()) { own.delete(); target.setSource(slot, ""); changed = true }
|
||||
record(context, target, slot, "")
|
||||
}
|
||||
}
|
||||
own != null && theirs != null && now == before && own.length() != theirs.length() -> write(context, target, slot, own)
|
||||
own != null && present.isEmpty() -> write(context, target, slot, own)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "${target.id} ${slot.key}: ${e.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
for (target in targets) {
|
||||
try {
|
||||
reconcilePosition(context, grid, target)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "${target.id} logo position: ${e.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
if (changed) Log.i(TAG, "took over art changed in Steam")
|
||||
return changed
|
||||
}
|
||||
}
|
||||
@@ -118,42 +118,23 @@ object SteamGridDb {
|
||||
.filter { it.optInt("id") > 0 && it.optString("name").isNotEmpty() }
|
||||
.map { Game(it.optInt("id"), it.optString("name"), it.optBoolean("verified")) }
|
||||
|
||||
internal fun path(id: Int, slot: AddedGameArt.Slot): String =
|
||||
when (slot) {
|
||||
internal fun path(id: Int, slot: AddedGameArt.Slot, page: Int): String {
|
||||
val base = when (slot) {
|
||||
AddedGameArt.Slot.COVER -> "grids/game/$id?dimensions=600x900&$QUERY"
|
||||
AddedGameArt.Slot.WIDE -> "grids/game/$id?dimensions=460x215,920x430&$QUERY"
|
||||
AddedGameArt.Slot.BACKGROUND -> "heroes/game/$id?$QUERY"
|
||||
AddedGameArt.Slot.LOGO -> "logos/game/$id?$QUERY"
|
||||
AddedGameArt.Slot.ICON -> "icons/game/$id?$QUERY"
|
||||
}
|
||||
return if (page > 0) "$base&page=$page" else base
|
||||
}
|
||||
|
||||
fun page(key: String, id: Int, slot: AddedGameArt.Slot): List<Pair<String, String>> {
|
||||
val items = json(key, "$API/${path(id, slot)}")?.optJSONArray("data") ?: return emptyList()
|
||||
fun page(key: String, id: Int, slot: AddedGameArt.Slot, page: Int): List<Pair<String, String>> {
|
||||
val items = json(key, "$API/${path(id, slot, page)}")?.optJSONArray("data") ?: return emptyList()
|
||||
return ranked(items, pngOnly = slot == AddedGameArt.Slot.LOGO || slot == AddedGameArt.Slot.ICON)
|
||||
}
|
||||
|
||||
private const val PAGE_SIZE = 50
|
||||
private const val MAX_PAGES = 4
|
||||
|
||||
fun all(key: String, id: Int, slot: AddedGameArt.Slot): List<Pair<String, String>> {
|
||||
val raw = JSONArray()
|
||||
for (n in 0 until MAX_PAGES) {
|
||||
val url = "$API/${path(id, slot)}" + if (n > 0) "&page=$n" else ""
|
||||
val data: JSONArray? = try {
|
||||
json(key, url)?.optJSONArray("data")
|
||||
} catch (e: Failed) {
|
||||
if (n == 0) throw e
|
||||
null
|
||||
}
|
||||
val items = data ?: break
|
||||
for (i in 0 until items.length()) raw.put(items.get(i))
|
||||
if (items.length() < PAGE_SIZE) break
|
||||
}
|
||||
return ranked(raw, pngOnly = slot == AddedGameArt.Slot.LOGO || slot == AddedGameArt.Slot.ICON)
|
||||
}
|
||||
|
||||
fun images(key: String, id: Int, slot: AddedGameArt.Slot, limit: Int): List<Pair<String, String>> =
|
||||
page(key, id, slot).take(limit)
|
||||
page(key, id, slot, 0).take(limit)
|
||||
|
||||
internal fun ranked(items: JSONArray, pngOnly: Boolean): List<Pair<String, String>> =
|
||||
(0 until items.length()).map { items.getJSONObject(it) }
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import android.content.Context
|
||||
import com.droiddeck.launcher.R
|
||||
import kotlin.math.abs
|
||||
|
||||
internal data class CalibrationDistanceResult(val accepted: Boolean, val message: String)
|
||||
|
||||
internal object CalibrationDistanceCheck {
|
||||
fun evaluate(context: Context, expected: Float, corners: Int, points: List<SpatialVector>?): CalibrationDistanceResult {
|
||||
require(expected.isFinite() && expected in 0.15f..1.5f)
|
||||
if (corners != 24) return CalibrationDistanceResult(false,
|
||||
context.getString(R.string.spatial_distance_corners, corners))
|
||||
if (points?.size != 24 || points.any { !it.finite() }) return CalibrationDistanceResult(false,
|
||||
context.getString(R.string.spatial_distance_depth))
|
||||
val measured=(points.reduce { a,b -> a+b }*(1f/points.size)).length()
|
||||
if (!measured.isFinite()) return CalibrationDistanceResult(false,context.getString(R.string.spatial_distance_invalid))
|
||||
val accepted=abs(measured-expected)<0.05f
|
||||
return CalibrationDistanceResult(accepted,
|
||||
context.getString(R.string.spatial_distance_result,
|
||||
SpatialMeasurements.validation(measured), SpatialMeasurements.validation(expected),
|
||||
context.getString(if (accepted) R.string.spatial_distance_passed else R.string.spatial_distance_retry)))
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
internal data class CalibrationObservationData(
|
||||
val objects: List<DoubleArray>, val left: List<DoubleArray>, val right: List<DoubleArray>, val head: SpatialPose,
|
||||
) {
|
||||
companion object {
|
||||
fun parse(view: JSONObject): CalibrationObservationData {
|
||||
fun points(name: String, dimensions: Int): List<DoubleArray> {
|
||||
val rows=view.getJSONArray(name)
|
||||
require(rows.length() in 12..24) { "Invalid saved board corner count" }
|
||||
return (0 until rows.length()).map { row ->
|
||||
val values=rows.getJSONArray(row)
|
||||
require(values.length()==dimensions)
|
||||
DoubleArray(dimensions) { values.getDouble(it).also { value -> require(value.isFinite()) } }
|
||||
}
|
||||
}
|
||||
val objects=points("objects",3); val left=points("left",2); val right=points("right",2)
|
||||
require(objects.size==left.size && objects.size==right.size)
|
||||
val pose=view.getJSONArray("head")
|
||||
require(pose.length()==7)
|
||||
val head=SpatialPose.fromSdk(FloatArray(7) { pose.getDouble(it).toFloat() }) ?: error("Invalid saved head pose")
|
||||
return CalibrationObservationData(objects,left,right,head)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,29 +0,0 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
data class GlassesResolution(val width: Int, val height: Int) {
|
||||
val key: String get() = "${width}x$height"
|
||||
}
|
||||
|
||||
enum class GlassesDisplayProfile { TWO_D, THREE_D, VR }
|
||||
|
||||
internal object GlassesDisplayModes {
|
||||
fun profile(stereo: Boolean, vr: Boolean): GlassesDisplayProfile = when {
|
||||
vr -> GlassesDisplayProfile.VR
|
||||
stereo -> GlassesDisplayProfile.THREE_D
|
||||
else -> GlassesDisplayProfile.TWO_D
|
||||
}
|
||||
|
||||
fun choice(settings: GlassesSettings, profile: GlassesDisplayProfile): String = when (profile) {
|
||||
GlassesDisplayProfile.TWO_D -> settings.resolution2d
|
||||
GlassesDisplayProfile.THREE_D -> settings.resolution3d
|
||||
GlassesDisplayProfile.VR -> settings.resolutionVr
|
||||
}
|
||||
|
||||
fun selected(settings: GlassesSettings, state: GlassesState,
|
||||
stereo: Boolean = state.sbs, vr: Boolean = state.vrActive): GlassesResolution? {
|
||||
val selected = choice(settings, profile(stereo, vr)).ifEmpty {
|
||||
if (vr) choice(settings, profile(stereo, false)) else ""
|
||||
}
|
||||
return state.resolutions.firstOrNull { it.key == selected }
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
/** Hardware properties; display refresh is selected from Android's advertised modes. */
|
||||
interface GlassesModel {
|
||||
val name: String
|
||||
val controllable: Boolean
|
||||
val brightnessMax: Int
|
||||
val volumeMax: Int
|
||||
val diagonalFovDeg: Float
|
||||
val sbs90: Boolean
|
||||
val hasSunblock: Boolean get() = true
|
||||
val resolutions: List<GlassesResolution> get() = emptyList()
|
||||
fun mode(hz: Int, sbs: Boolean, resolution: GlassesResolution?): Int = mode(hz, sbs)
|
||||
fun mode(hz: Int, sbs: Boolean): Int
|
||||
fun modeForStep(step: Int): Int?
|
||||
}
|
||||
|
||||
/** The command IDs here are the existing glasses controller's internal vocabulary.
|
||||
* Each transport translates them to its own wire protocol; unsupported controls return false.
|
||||
*/
|
||||
internal interface GlassesLink {
|
||||
val hasImu: Boolean
|
||||
val badFrames: Long get() = 0L
|
||||
val ready: Boolean get() = true
|
||||
fun send(msgId: Int, payload: ByteArray = ByteArray(0)): Boolean
|
||||
fun close()
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import android.view.Choreographer
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inspector.WindowInspector
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class GlassesMenuCapture(private val activity: Activity, private val onSize: (Int, Int) -> Unit,
|
||||
private val onFailure: (Throwable) -> Unit = {}) : Choreographer.FrameCallback {
|
||||
private var texture: SurfaceTexture? = null
|
||||
private var surface: Surface? = null
|
||||
private var running = false
|
||||
private var framePending = false
|
||||
private var lastFrameNs = Long.MIN_VALUE
|
||||
private var failures = 0
|
||||
private var bufferWidth = 0
|
||||
private var bufferHeight = 0
|
||||
private val basePosition = IntArray(2)
|
||||
private val windowPosition = IntArray(2)
|
||||
|
||||
fun attach(texture: SurfaceTexture, surface: Surface) {
|
||||
checkMainThread()
|
||||
this.texture = texture
|
||||
this.surface = surface
|
||||
bufferWidth = 0
|
||||
bufferHeight = 0
|
||||
failures = 0
|
||||
lastFrameNs = Long.MIN_VALUE
|
||||
}
|
||||
|
||||
fun start() {
|
||||
checkMainThread()
|
||||
if (running || activity.isDestroyed || activity.isFinishing) return
|
||||
running = true
|
||||
failures = 0
|
||||
lastFrameNs = Long.MIN_VALUE
|
||||
schedule()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
checkMainThread()
|
||||
running = false
|
||||
if (framePending) Choreographer.getInstance().removeFrameCallback(this)
|
||||
framePending = false
|
||||
}
|
||||
|
||||
override fun doFrame(frameTimeNanos: Long) {
|
||||
framePending = false
|
||||
if (!running) return
|
||||
if (activity.isDestroyed || activity.isFinishing) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
if (lastFrameNs == Long.MIN_VALUE || frameTimeNanos - lastFrameNs >= FRAME_INTERVAL_NS) {
|
||||
lastFrameNs = frameTimeNanos
|
||||
try {
|
||||
capture()
|
||||
failures = 0
|
||||
} catch (error: Exception) {
|
||||
if (++failures >= MAX_FAILURES) {
|
||||
stop()
|
||||
onFailure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (running) schedule()
|
||||
}
|
||||
|
||||
private fun schedule() {
|
||||
if (framePending) return
|
||||
framePending = true
|
||||
Choreographer.getInstance().postFrameCallback(this)
|
||||
}
|
||||
|
||||
private fun capture() {
|
||||
val target = surface ?: return
|
||||
val input = texture ?: return
|
||||
val decor = activity.window.decorView
|
||||
if (!decor.isAttachedToWindow || !decor.isShown) return
|
||||
val size = captureSize(decor.width, decor.height) ?: return
|
||||
check(target.isValid)
|
||||
if (bufferWidth != size.first || bufferHeight != size.second) {
|
||||
input.setDefaultBufferSize(size.first, size.second)
|
||||
onSize(size.first, size.second)
|
||||
bufferWidth = size.first
|
||||
bufferHeight = size.second
|
||||
}
|
||||
val candidates = if (Build.VERSION.SDK_INT >= 29) WindowInspector.getGlobalWindowViews() else emptyList()
|
||||
val roots = captureRoots(activity, candidates)
|
||||
val canvas = target.lockHardwareCanvas()
|
||||
val initialSave = canvas.save()
|
||||
try {
|
||||
canvas.drawColor(Color.BLACK, PorterDuff.Mode.SRC)
|
||||
val scale = min(canvas.width.toFloat() / decor.width, canvas.height.toFloat() / decor.height)
|
||||
canvas.translate((canvas.width - decor.width * scale) / 2f, (canvas.height - decor.height * scale) / 2f)
|
||||
canvas.scale(scale, scale)
|
||||
canvas.clipRect(0, 0, decor.width, decor.height)
|
||||
decor.getLocationOnScreen(basePosition)
|
||||
for (root in roots) {
|
||||
val params = root.layoutParams as? WindowManager.LayoutParams
|
||||
if (root !== decor && params != null && params.flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND != 0) {
|
||||
canvas.drawColor(Color.argb((params.dimAmount.coerceIn(0f, 1f) * 255).roundToInt(), 0, 0, 0))
|
||||
}
|
||||
root.getLocationOnScreen(windowPosition)
|
||||
val saved = canvas.save()
|
||||
try {
|
||||
canvas.translate((windowPosition[0] - basePosition[0]).toFloat(), (windowPosition[1] - basePosition[1]).toFloat())
|
||||
root.draw(canvas)
|
||||
} finally {
|
||||
canvas.restoreToCount(saved)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
canvas.restoreToCount(initialSave)
|
||||
} finally {
|
||||
target.unlockCanvasAndPost(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkMainThread() {
|
||||
check(Looper.myLooper() == Looper.getMainLooper())
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
private const val FRAME_INTERVAL_NS = 33_333_334L
|
||||
private const val MAX_FAILURES = 3
|
||||
private const val MAX_EDGE = 1920
|
||||
|
||||
internal fun captureSize(width: Int, height: Int): Pair<Int, Int>? {
|
||||
if (width <= 0 || height <= 0) return null
|
||||
val scale = min(1.0, MAX_EDGE.toDouble() / maxOf(width, height))
|
||||
return (width * scale).roundToInt().coerceAtLeast(1) to (height * scale).roundToInt().coerceAtLeast(1)
|
||||
}
|
||||
|
||||
internal fun captureRoots(activity: Activity, candidates: List<View>): List<View> {
|
||||
val decor = activity.window.decorView
|
||||
if (activity.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE != 0 ||
|
||||
!decor.isAttachedToWindow || !decor.isShown) return emptyList()
|
||||
if (Build.VERSION.SDK_INT < 29) return listOf(decor)
|
||||
val display = decor.display?.displayId ?: return listOf(decor)
|
||||
val token = decor.applicationWindowToken
|
||||
return listOf(decor) + candidates.filter { root ->
|
||||
if (root === decor || !root.isAttachedToWindow || !root.isShown || root.display?.displayId != display) return@filter false
|
||||
val params = root.layoutParams as? WindowManager.LayoutParams ?: return@filter false
|
||||
if (params.flags and WindowManager.LayoutParams.FLAG_SECURE != 0 ||
|
||||
params.type !in WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW..WindowManager.LayoutParams.LAST_SUB_WINDOW) return@filter false
|
||||
ownsContext(root.context, activity) || (token != null && root.applicationWindowToken == token)
|
||||
}.distinct()
|
||||
}
|
||||
|
||||
private fun ownsContext(context: Context, activity: Activity): Boolean {
|
||||
var current = context
|
||||
repeat(16) {
|
||||
if (current === activity) return true
|
||||
val next = (current as? ContextWrapper)?.baseContext ?: return false
|
||||
if (next === current) return false
|
||||
current = next
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.app.Presentation
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Display
|
||||
import android.view.WindowManager
|
||||
import com.droiddeck.launcher.SessionActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object GlassesMenuMirror {
|
||||
private var controller: GlassesMenuController? = null
|
||||
|
||||
fun initialize(app: Application) {
|
||||
if (controller != null) return
|
||||
val pose = GlassesMenuPose()
|
||||
val menus = GlassesMenuController(pose,
|
||||
display = Glasses::glassesDisplay,
|
||||
observe = { active -> if (active) Glasses.addPoseObserver(pose) else Glasses.removePoseObserver(pose) },
|
||||
create = { activity, display, failure -> MenuPresentation(activity, display, pose, failure) })
|
||||
controller = menus
|
||||
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
|
||||
override fun onActivityResumed(activity: Activity) = menus.resume(activity)
|
||||
override fun onActivityPaused(activity: Activity) = menus.pause(activity)
|
||||
override fun onActivityDestroyed(activity: Activity) = menus.pause(activity)
|
||||
override fun onActivityCreated(activity: Activity, state: Bundle?) = Unit
|
||||
override fun onActivityStarted(activity: Activity) = Unit
|
||||
override fun onActivityStopped(activity: Activity) = Unit
|
||||
override fun onActivitySaveInstanceState(activity: Activity, state: Bundle) = Unit
|
||||
})
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).launch {
|
||||
combine(Glasses.state, Glasses.settings) { state, settings -> state to settings }
|
||||
.collect { (state, settings) -> menus.update(state, settings) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setSessionMenuVisible(activity: Activity, visible: Boolean) {
|
||||
controller?.setSessionMenuVisible(activity, visible)
|
||||
}
|
||||
|
||||
fun recenter() { controller?.recenter() }
|
||||
}
|
||||
|
||||
internal class GlassesMenuController(
|
||||
private val pose: GlassesMenuPose,
|
||||
private val display: () -> Display?,
|
||||
private val observe: (Boolean) -> Unit,
|
||||
private val create: (Activity, Display, (Throwable) -> Unit) -> Output,
|
||||
private val schedule: (() -> Unit) -> Unit = { android.os.Handler(android.os.Looper.getMainLooper()).post(it) },
|
||||
) {
|
||||
interface Output {
|
||||
fun configure(state: GlassesState, settings: GlassesSettings)
|
||||
fun show()
|
||||
fun close()
|
||||
}
|
||||
|
||||
private var activity: Activity? = null
|
||||
private val sessionMenus = java.util.WeakHashMap<Activity, Boolean>()
|
||||
private var state = GlassesState()
|
||||
private var settings = GlassesSettings()
|
||||
private var output: Output? = null
|
||||
private var outputDisplay = Display.INVALID_DISPLAY
|
||||
private var failed = false
|
||||
private var dismissalRetried = false
|
||||
private var generation = 0
|
||||
|
||||
fun resume(owner: Activity) {
|
||||
if (activity !== owner) close()
|
||||
activity = owner
|
||||
failed = false
|
||||
dismissalRetried = false
|
||||
reconcile()
|
||||
}
|
||||
|
||||
fun pause(owner: Activity) {
|
||||
if (activity !== owner) return
|
||||
close()
|
||||
activity = null
|
||||
}
|
||||
|
||||
fun setSessionMenuVisible(owner: Activity, visible: Boolean) {
|
||||
if (visible) sessionMenus[owner] = true else sessionMenus.remove(owner)
|
||||
if (activity === owner) reconcile()
|
||||
}
|
||||
|
||||
fun update(next: GlassesState, preferences: GlassesSettings) {
|
||||
val deviceChanged = state.canControl != next.canControl || state.model != next.model
|
||||
if (deviceChanged) pose.reset()
|
||||
if (deviceChanged || state.displayId != next.displayId || state.width != next.width ||
|
||||
state.height != next.height || state.sbs != next.sbs || settings != preferences) {
|
||||
failed = false
|
||||
dismissalRetried = false
|
||||
}
|
||||
state = next
|
||||
settings = preferences
|
||||
pose.configure(preferences)
|
||||
reconcile()
|
||||
}
|
||||
|
||||
fun recenter() = pose.recenter()
|
||||
|
||||
private fun reconcile() {
|
||||
val owner = activity
|
||||
val eligible = owner != null && !owner.isFinishing && !owner.isDestroyed && !owner.isInPictureInPictureMode &&
|
||||
owner.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE == 0 &&
|
||||
owner !is com.droiddeck.launcher.QuestXrActivity &&
|
||||
(owner !is SessionActivity || sessionMenus[owner] == true) &&
|
||||
settings.gameOnGlasses && settings.screen3dof && state.usable && state.canControl && state.supportsScreen3dof
|
||||
val target = if (eligible && !failed) display() else null
|
||||
if (target == null || owner == null || owner.windowManager.defaultDisplay.displayId == target.displayId) {
|
||||
close()
|
||||
return
|
||||
}
|
||||
if (outputDisplay != target.displayId) close()
|
||||
try {
|
||||
output?.let { it.configure(state, settings); return }
|
||||
val token = ++generation
|
||||
val next = create(owner, target) { error ->
|
||||
if (generation == token) {
|
||||
close()
|
||||
if (error is GlassesMenuDisplayStopped && !dismissalRetried) {
|
||||
dismissalRetried = true
|
||||
val retry = generation
|
||||
schedule { if (generation == retry && activity === owner) reconcile() }
|
||||
} else {
|
||||
Log.w("GlassesMenu", "Tracked menus are unavailable for this display", error)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (generation != token) {
|
||||
next.close()
|
||||
return
|
||||
}
|
||||
output = next
|
||||
outputDisplay = target.displayId
|
||||
next.configure(state, settings)
|
||||
if (generation != token) return
|
||||
observe(true)
|
||||
next.show()
|
||||
} catch (error: RuntimeException) {
|
||||
Log.w("GlassesMenu", "Could not show tracked menus", error)
|
||||
failed = true
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun close() {
|
||||
++generation
|
||||
val previous = output
|
||||
output = null
|
||||
outputDisplay = Display.INVALID_DISPLAY
|
||||
if (previous != null) {
|
||||
observe(false)
|
||||
try { previous.close() }
|
||||
catch (error: RuntimeException) { Log.w("GlassesMenu", "Could not close tracked menus", error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class GlassesMenuDisplayStopped : IllegalStateException("The glasses menu display stopped")
|
||||
|
||||
private class MenuPresentation(
|
||||
private val owner: Activity,
|
||||
display: Display,
|
||||
pose: GlassesMenuPose,
|
||||
private val onFailure: (Throwable) -> Unit,
|
||||
) : Presentation(owner, display), GlassesMenuController.Output {
|
||||
private var closed = false
|
||||
private val capture: GlassesMenuCapture = GlassesMenuCapture(owner,
|
||||
onSize = { width, height -> surface.setSourceSize(width, height) }, onFailure = onFailure)
|
||||
private val surface: GlassesMenuSurface = GlassesMenuSurface(context, pose, onFailure = onFailure) { texture, input ->
|
||||
if (!closed) {
|
||||
capture.attach(texture, input)
|
||||
capture.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setContentView(surface)
|
||||
}
|
||||
|
||||
override fun configure(state: GlassesState, settings: GlassesSettings) {
|
||||
surface.configure(settings, state)
|
||||
val mode = display.mode
|
||||
val hz = if (state.sbs) settings.sbsHz else settings.refreshHz
|
||||
val best = display.supportedModes.filter {
|
||||
it.physicalWidth == mode.physicalWidth && it.physicalHeight == mode.physicalHeight
|
||||
}.minByOrNull { kotlin.math.abs(it.refreshRate - hz) }
|
||||
if (best != null) window?.let { it.attributes = it.attributes.apply { preferredDisplayModeId = best.modeId } }
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (closed) return
|
||||
closed = true
|
||||
capture.stop()
|
||||
surface.close()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
capture.stop()
|
||||
surface.close()
|
||||
super.onStop()
|
||||
if (!closed) {
|
||||
closed = true
|
||||
onFailure(GlassesMenuDisplayStopped())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import com.droiddeck.launcher.gpu.HeadPose
|
||||
|
||||
class GlassesMenuPose internal constructor(
|
||||
private val spatialEnabled: () -> Boolean = { SpatialTracking.enabled },
|
||||
private val spatialView: (Long) -> SpatialPose? = { now ->
|
||||
synchronized(SpatialTracking.session) {
|
||||
if (SpatialTracking.session.valid(now)) SpatialTracking.session.view(true) else null
|
||||
}
|
||||
},
|
||||
private val recenterSpatial: () -> Unit = { SpatialTracking.recenter(true) },
|
||||
) : Glasses.PoseSink {
|
||||
data class Frame(
|
||||
val rotation: FloatArray,
|
||||
val position: FloatArray,
|
||||
val valid: Boolean,
|
||||
val spatial: Boolean,
|
||||
)
|
||||
|
||||
private var tracking = true
|
||||
private var positional = false
|
||||
private var reference: HeadPose.Reference? = null
|
||||
private var latest: FloatArray? = null
|
||||
private var latestTime = 0L
|
||||
private var safe = identity()
|
||||
|
||||
@Synchronized fun configure(settings: GlassesSettings) {
|
||||
if (tracking != settings.headTracking) {
|
||||
reference = null
|
||||
safe = identity()
|
||||
}
|
||||
tracking = settings.headTracking
|
||||
positional = settings.positionalTracking
|
||||
}
|
||||
|
||||
@Synchronized override fun onPose(x: Float, y: Float, z: Float, w: Float, timeNs: Long) {
|
||||
val norm = x*x + y*y + z*z + w*w
|
||||
if (!norm.isFinite() || norm !in 0.25f..2.25f || timeNs <= latestTime) return
|
||||
val matrix = HeadPose.fromGlasses(x, y, z, w)
|
||||
if (matrix.any { !it.isFinite() }) return
|
||||
latest = matrix
|
||||
latestTime = timeNs
|
||||
}
|
||||
|
||||
@Synchronized fun snapshot(now: Long = System.nanoTime()): Frame {
|
||||
if (!tracking) return identity()
|
||||
val spatial = positional && spatialEnabled()
|
||||
if (spatial) {
|
||||
val pose = spatialView(now)
|
||||
val normalized = pose?.rotation?.normalized()
|
||||
if (pose != null && pose.position.finite() && normalized != null) {
|
||||
safe = Frame(normalized.matrix(), pose.position.array(), true, true)
|
||||
return copy(safe)
|
||||
}
|
||||
} else {
|
||||
val matrix = latest
|
||||
if (matrix != null && latestTime > 0 && now-latestTime in 0..MAX_AGE_NS) {
|
||||
val anchor = reference ?: HeadPose.recenter(matrix, centerView = true).also { reference = it }
|
||||
safe = Frame(HeadPose.relative(anchor, matrix), FloatArray(3), true, false)
|
||||
return copy(safe)
|
||||
}
|
||||
}
|
||||
return copy(safe).copy(valid = false, spatial = spatial)
|
||||
}
|
||||
|
||||
@Synchronized fun recenter() {
|
||||
if (positional && spatialEnabled()) recenterSpatial()
|
||||
else reference = null
|
||||
}
|
||||
|
||||
@Synchronized fun reset() {
|
||||
reference = null
|
||||
latest = null
|
||||
latestTime = 0L
|
||||
safe = identity()
|
||||
}
|
||||
|
||||
private fun copy(frame: Frame) = frame.copy(rotation = frame.rotation.copyOf(), position = frame.position.copyOf())
|
||||
|
||||
private fun identity() = Frame(HeadPose.IDENTITY.copyOf(), FloatArray(3), false, false)
|
||||
|
||||
companion object {
|
||||
internal const val MAX_AGE_NS = 100_000_000L
|
||||
}
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
package com.droiddeck.launcher.glasses
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.opengl.GLES11Ext
|
||||
import android.opengl.GLES20
|
||||
import android.opengl.GLSurfaceView
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.microedition.khronos.egl.EGLConfig
|
||||
import javax.microedition.khronos.opengles.GL10
|
||||
import kotlin.math.tan
|
||||
|
||||
class GlassesMenuSurface(
|
||||
context: Context,
|
||||
private val pose: GlassesMenuPose,
|
||||
private val onFailure: (Throwable) -> Unit = {},
|
||||
private val onInput: (SurfaceTexture, Surface) -> Unit,
|
||||
) : GLSurfaceView(context) {
|
||||
private data class Scene(val settings: GlassesSettings, val state: GlassesState)
|
||||
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
private val closed = AtomicBoolean()
|
||||
private val failed = AtomicBoolean()
|
||||
private val generation = AtomicInteger()
|
||||
private val pendingFrame = AtomicBoolean()
|
||||
private val inputLock = Any()
|
||||
private var inputTexture: SurfaceTexture? = null
|
||||
private var inputSurface: Surface? = null
|
||||
@Volatile private var scene = Scene(GlassesSettings(), GlassesState())
|
||||
@Volatile private var sourceAspect = 16f / 9f
|
||||
private val renderer = MenuRenderer()
|
||||
|
||||
init {
|
||||
setEGLContextClientVersion(2)
|
||||
setEGLConfigChooser(8, 8, 8, 8, 0, 0)
|
||||
preserveEGLContextOnPause = true
|
||||
setRenderer(renderer)
|
||||
renderMode = RENDERMODE_CONTINUOUSLY
|
||||
}
|
||||
|
||||
fun configure(settings: GlassesSettings, state: GlassesState) {
|
||||
pose.configure(settings)
|
||||
scene = Scene(settings, state)
|
||||
}
|
||||
|
||||
fun setSourceSize(width: Int, height: Int) {
|
||||
if (width > 0 && height > 0) sourceAspect = width.toFloat() / height
|
||||
}
|
||||
|
||||
fun close() {
|
||||
if (!closed.compareAndSet(false, true)) return
|
||||
generation.incrementAndGet()
|
||||
queueEvent { renderer.release() }
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
close()
|
||||
super.onDetachedFromWindow()
|
||||
// GLSurfaceView may exit its GL thread before queued cleanup is consumed.
|
||||
releaseInput()
|
||||
}
|
||||
|
||||
private fun releaseInput() = synchronized(inputLock) {
|
||||
inputTexture?.setOnFrameAvailableListener(null)
|
||||
inputSurface?.release()
|
||||
inputTexture?.release()
|
||||
inputSurface = null
|
||||
inputTexture = null
|
||||
pendingFrame.set(false)
|
||||
}
|
||||
|
||||
private fun fail(error: Throwable) {
|
||||
if (!failed.compareAndSet(false, true)) return
|
||||
generation.incrementAndGet()
|
||||
Log.e("GlassesMenu", "Menu rendering is unavailable", error)
|
||||
main.post { if (!closed.get()) onFailure(error) }
|
||||
}
|
||||
|
||||
private inner class MenuRenderer : Renderer {
|
||||
private val vertices = ByteBuffer.allocateDirect(8 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().apply {
|
||||
put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f)); position(0)
|
||||
}
|
||||
private val textureMatrix = FloatArray(16)
|
||||
private var texture = 0
|
||||
private var program = 0
|
||||
private var width = 1
|
||||
private var height = 1
|
||||
private var hasFrame = false
|
||||
private var positionLocation = 0
|
||||
private var textureLocation = 0
|
||||
private var transformLocation = 0
|
||||
private var rotationLocation = IntArray(3)
|
||||
private var headLocation = 0
|
||||
private var screenLocation = 0
|
||||
private var opticsLocation = 0
|
||||
|
||||
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
|
||||
releaseInput()
|
||||
texture = 0
|
||||
program = 0
|
||||
hasFrame = false
|
||||
if (closed.get() || failed.get()) return
|
||||
val token = generation.incrementAndGet()
|
||||
try {
|
||||
program = linkProgram()
|
||||
positionLocation = GLES20.glGetAttribLocation(program, "aPosition")
|
||||
textureLocation = GLES20.glGetUniformLocation(program, "uTexture")
|
||||
transformLocation = GLES20.glGetUniformLocation(program, "uTextureMatrix")
|
||||
rotationLocation = IntArray(3) { GLES20.glGetUniformLocation(program, "uRotation$it") }
|
||||
headLocation = GLES20.glGetUniformLocation(program, "uHead")
|
||||
screenLocation = GLES20.glGetUniformLocation(program, "uScreen")
|
||||
opticsLocation = GLES20.glGetUniformLocation(program, "uOptics")
|
||||
val names = IntArray(1)
|
||||
GLES20.glGenTextures(1, names, 0)
|
||||
texture = names[0]
|
||||
check(texture != 0) { "Could not allocate the menu texture" }
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture)
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
|
||||
checkGl("initialize menu texture")
|
||||
val incoming = SurfaceTexture(texture)
|
||||
val surface = synchronized(inputLock) {
|
||||
inputTexture = incoming
|
||||
Surface(incoming).also { inputSurface = it }
|
||||
}
|
||||
incoming.setOnFrameAvailableListener({
|
||||
if (!closed.get() && generation.get() == token) pendingFrame.set(true)
|
||||
}, main)
|
||||
main.post {
|
||||
if (!closed.get() && !failed.get() && generation.get() == token) onInput(incoming, surface)
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
fail(e)
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
|
||||
this.width = width.coerceAtLeast(1)
|
||||
this.height = height.coerceAtLeast(1)
|
||||
GLES20.glViewport(0, 0, this.width, this.height)
|
||||
}
|
||||
|
||||
override fun onDrawFrame(gl: GL10?) {
|
||||
if (closed.get() || failed.get()) return
|
||||
GLES20.glClearColor(0.015f, 0.015f, 0.02f, 1f)
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
||||
if (program == 0) return
|
||||
if (pendingFrame.getAndSet(false)) synchronized(inputLock) {
|
||||
inputTexture?.let {
|
||||
try {
|
||||
it.updateTexImage()
|
||||
it.getTransformMatrix(textureMatrix)
|
||||
checkGl("update menu texture")
|
||||
hasFrame = true
|
||||
} catch (e: RuntimeException) {
|
||||
fail(e)
|
||||
release()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasFrame) return
|
||||
val current = scene
|
||||
val frame = pose.snapshot()
|
||||
val halfWidth = current.settings.screenWidthDm.coerceIn(5, 200) / 20f
|
||||
val distance = current.settings.screenDistanceDm.coerceIn(5, 200) / 10f
|
||||
val mono = !current.state.sbs
|
||||
val fov = current.state.verticalFovDeg.takeIf { it.isFinite() }?.coerceIn(20f, 140f) ?: 23.3f
|
||||
val tangent = tan(Math.toRadians(fov.toDouble()) / 2).toFloat()
|
||||
GLES20.glUseProgram(program)
|
||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture)
|
||||
GLES20.glUniform1i(textureLocation, 0)
|
||||
GLES20.glUniformMatrix4fv(transformLocation, 1, false, textureMatrix, 0)
|
||||
for (row in 0..2) GLES20.glUniform3fv(rotationLocation[row], 1, frame.rotation, row * 3)
|
||||
GLES20.glUniform3fv(headLocation, 1, frame.position, 0)
|
||||
GLES20.glUniform3f(screenLocation, halfWidth, distance, sourceAspect)
|
||||
GLES20.glUniform4f(opticsLocation, tangent, width.toFloat() / height / (if (mono) 1f else 2f),
|
||||
if (mono) 0f else IPD_METERS / 2f, if (mono) 1f else 0f)
|
||||
vertices.position(0)
|
||||
GLES20.glEnableVertexAttribArray(positionLocation)
|
||||
GLES20.glVertexAttribPointer(positionLocation, 2, GLES20.GL_FLOAT, false, 0, vertices)
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
|
||||
GLES20.glDisableVertexAttribArray(positionLocation)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
releaseInput()
|
||||
if (texture != 0) GLES20.glDeleteTextures(1, intArrayOf(texture), 0)
|
||||
if (program != 0) GLES20.glDeleteProgram(program)
|
||||
texture = 0
|
||||
program = 0
|
||||
hasFrame = false
|
||||
}
|
||||
|
||||
private fun checkGl(operation: String) {
|
||||
val error = GLES20.glGetError()
|
||||
check(error == GLES20.GL_NO_ERROR) { "Could not $operation: GL error 0x${error.toString(16)}" }
|
||||
}
|
||||
|
||||
private fun shader(type: Int, source: String): Int {
|
||||
val shader = GLES20.glCreateShader(type)
|
||||
GLES20.glShaderSource(shader, source)
|
||||
GLES20.glCompileShader(shader)
|
||||
val status = IntArray(1)
|
||||
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0)
|
||||
if (status[0] == 0) {
|
||||
val error = GLES20.glGetShaderInfoLog(shader)
|
||||
GLES20.glDeleteShader(shader)
|
||||
throw IllegalStateException(error)
|
||||
}
|
||||
return shader
|
||||
}
|
||||
|
||||
private fun linkProgram(): Int {
|
||||
val vertex = shader(GLES20.GL_VERTEX_SHADER, VERTEX)
|
||||
var fragment = 0
|
||||
var linked = 0
|
||||
try {
|
||||
fragment = shader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT)
|
||||
linked = GLES20.glCreateProgram()
|
||||
GLES20.glAttachShader(linked, vertex)
|
||||
GLES20.glAttachShader(linked, fragment)
|
||||
GLES20.glLinkProgram(linked)
|
||||
val status = IntArray(1)
|
||||
GLES20.glGetProgramiv(linked, GLES20.GL_LINK_STATUS, status, 0)
|
||||
check(status[0] != 0) { GLES20.glGetProgramInfoLog(linked) }
|
||||
return linked
|
||||
} catch (e: RuntimeException) {
|
||||
if (linked != 0) GLES20.glDeleteProgram(linked)
|
||||
throw e
|
||||
} finally {
|
||||
GLES20.glDeleteShader(vertex)
|
||||
if (fragment != 0) GLES20.glDeleteShader(fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val IPD_METERS = 0.064f
|
||||
private const val VERTEX = """
|
||||
attribute vec2 aPosition;
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
gl_Position = vec4(aPosition, 0.0, 1.0);
|
||||
vUv = aPosition * 0.5 + 0.5;
|
||||
}
|
||||
"""
|
||||
private const val FRAGMENT = """
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
precision highp float;
|
||||
uniform samplerExternalOES uTexture;
|
||||
uniform mat4 uTextureMatrix;
|
||||
uniform vec3 uRotation0, uRotation1, uRotation2, uHead, uScreen;
|
||||
uniform vec4 uOptics;
|
||||
varying vec2 vUv;
|
||||
vec3 rotate(vec3 v) {
|
||||
return vec3(dot(uRotation0, v), dot(uRotation1, v), dot(uRotation2, v));
|
||||
}
|
||||
void main() {
|
||||
bool mono = uOptics.w > 0.5;
|
||||
float eye = mono || vUv.x < 0.5 ? 0.0 : 1.0;
|
||||
vec2 local = vec2(mono ? vUv.x : vUv.x * 2.0 - eye, 1.0 - vUv.y);
|
||||
vec2 ray = (local - 0.5) * vec2(uOptics.y, 1.0) * (2.0 * uOptics.x);
|
||||
vec3 direction = rotate(vec3(ray.x, -ray.y, -1.0));
|
||||
vec3 origin = uHead + rotate(vec3((eye < 0.5 ? -1.0 : 1.0) * uOptics.z, 0.0, 0.0));
|
||||
vec3 color = vec3(0.015, 0.015, 0.02);
|
||||
if (direction.z < -0.0001) {
|
||||
float distance = (-uScreen.y - origin.z) / direction.z;
|
||||
vec3 hit = origin + direction * distance;
|
||||
vec2 source = vec2(hit.x / uScreen.x, -hit.y * uScreen.z / uScreen.x) * 0.5 + 0.5;
|
||||
if (distance > 0.0 && all(greaterThanEqual(source, vec2(0.0))) && all(lessThanEqual(source, vec2(1.0)))) {
|
||||
vec2 uv = (uTextureMatrix * vec4(source.x, 1.0 - source.y, 0.0, 1.0)).xy;
|
||||
color = texture2D(uTexture, uv).rgb;
|
||||
}
|
||||
}
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
Loaded 100 of 822 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user